diff --git a/docs/apm/api.asciidoc b/docs/apm/api.asciidoc index 93733f5990a46e..b26c7446b91d11 100644 --- a/docs/apm/api.asciidoc +++ b/docs/apm/api.asciidoc @@ -60,7 +60,7 @@ The following Agent configuration APIs are available: ====== `settings`:: -(required) Key/value object with settings and their corresponding value. +(required) Key/value object with option name and option value. `agent_name`:: (optional) The agent name is used by the UI to determine which settings to display. @@ -73,14 +73,14 @@ The following Agent configuration APIs are available: -------------------------------------------------- PUT /api/apm/settings/agent-configuration { - "service" : { - "name" : "frontend", - "environment" : "production" + "service": { + "name": "frontend", + "environment": "production" }, - "settings" : { - "transaction_sample_rate" : 0.4, - "capture_body" : "off", - "transaction_max_spans" : 500 + "settings": { + "transaction_sample_rate": "0.4", + "capture_body": "off", + "transaction_max_spans": "500" }, "agent_name": "nodejs" } @@ -124,7 +124,7 @@ PUT /api/apm/settings/agent-configuration DELETE /api/apm/settings/agent-configuration { "service" : { - "name" : "frontend", + "name": "frontend", "environment": "production" } } @@ -157,9 +157,9 @@ DELETE /api/apm/settings/agent-configuration "environment": "production" }, "settings": { - "transaction_sample_rate": 1, + "transaction_sample_rate": "1", "capture_body": "off", - "transaction_max_spans": 200 + "transaction_max_spans": "200" }, "@timestamp": 1581934104843, "applied_by_agent": false, @@ -171,9 +171,9 @@ DELETE /api/apm/settings/agent-configuration "name": "opbeans-go" }, "settings": { - "transaction_sample_rate": 1, + "transaction_sample_rate": "1", "capture_body": "off", - "transaction_max_spans": 300 + "transaction_max_spans": "300" }, "@timestamp": 1581934111727, "applied_by_agent": false, @@ -185,7 +185,7 @@ DELETE /api/apm/settings/agent-configuration "name": "frontend" }, "settings": { - "transaction_sample_rate": 1, + "transaction_sample_rate": "1", }, "@timestamp": 1582031336265, "applied_by_agent": false, @@ -250,7 +250,7 @@ GET /api/apm/settings/agent-configuration "name": "frontend" }, "settings": { - "transaction_sample_rate": 1, + "transaction_sample_rate": "1", }, "@timestamp": 1582031336265, "applied_by_agent": false, @@ -266,9 +266,9 @@ GET /api/apm/settings/agent-configuration -------------------------------------------------- POST /api/apm/settings/agent-configuration/search { - "etag" : "1e58c178efeebae15c25c539da740d21dee422fc", + "etag": "1e58c178efeebae15c25c539da740d21dee422fc", "service" : { - "name" : "frontend", + "name": "frontend", "environment": "production" } } diff --git a/docs/canvas/canvas-elements.asciidoc b/docs/canvas/canvas-elements.asciidoc index a25460a20eb50c..4149039a3f87b1 100644 --- a/docs/canvas/canvas-elements.asciidoc +++ b/docs/canvas/canvas-elements.asciidoc @@ -31,7 +31,7 @@ By default, most of the elements you create use demo data until you change the d [[canvas-add-object]] ==== Add a saved object -Add a <>, such as a map or Lens visualization, then customize it to fit your display needs. +Add a <>, then customize it to fit your display needs. . Click *Embed object*. diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.fields.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.fields.md index db774a055e5281..4f88b635ca2f7c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.fields.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.fields.md @@ -10,6 +10,7 @@ fields?: { [subfield: string]: { type: string; + ignore_above?: number; }; }; ``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.md index dbc7d0ca431cec..9a31d37b3ff30e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectscorefieldmapping.md @@ -17,7 +17,7 @@ export interface SavedObjectsCoreFieldMapping | Property | Type | Description | | --- | --- | --- | | [enabled](./kibana-plugin-core-server.savedobjectscorefieldmapping.enabled.md) | boolean | | -| [fields](./kibana-plugin-core-server.savedobjectscorefieldmapping.fields.md) | {
[subfield: string]: {
type: string;
};
} | | +| [fields](./kibana-plugin-core-server.savedobjectscorefieldmapping.fields.md) | {
[subfield: string]: {
type: string;
ignore_above?: number;
};
} | | | [index](./kibana-plugin-core-server.savedobjectscorefieldmapping.index.md) | boolean | | | [null\_value](./kibana-plugin-core-server.savedobjectscorefieldmapping.null_value.md) | number | boolean | string | | | [type](./kibana-plugin-core-server.savedobjectscorefieldmapping.type.md) | string | | diff --git a/docs/user/alerting/action-types.asciidoc b/docs/user/alerting/action-types.asciidoc index 09878b3059ac87..e8dcf689df8e4a 100644 --- a/docs/user/alerting/action-types.asciidoc +++ b/docs/user/alerting/action-types.asciidoc @@ -41,9 +41,9 @@ see https://www.elastic.co/subscriptions[the subscription page]. [float] [[create-connectors]] -=== Preconfigured connectors and action types +=== Preconfigured actions and connectors -For out-of-the-box and standardized connectors, you can <> +For out-of-the-box and standardized connectors, you can <> before {kib} starts. If you preconfigure a connector, you can also <>. @@ -54,4 +54,4 @@ include::action-types/pagerduty.asciidoc[] include::action-types/server-log.asciidoc[] include::action-types/slack.asciidoc[] include::action-types/webhook.asciidoc[] -include::pre-configured-connectors.asciidoc[] +include::action-types/pre-configured-connectors.asciidoc[] diff --git a/docs/user/alerting/action-types/email.asciidoc b/docs/user/alerting/action-types/email.asciidoc index 81b4e210961f6e..4fb8a816d1ec90 100644 --- a/docs/user/alerting/action-types/email.asciidoc +++ b/docs/user/alerting/action-types/email.asciidoc @@ -28,27 +28,46 @@ Password:: password for 'login' type authentication. name: preconfigured-email-action-type actionTypeId: .email config: - from: testsender@test.com <1.1> - host: validhostname <1.2> - port: 8080 <1.3> - secure: false <1.4> + from: testsender@test.com + host: validhostname + port: 8080 + secure: false secrets: - user: testuser <2.1> - password: passwordkeystorevalue <2.2> + user: testuser + password: passwordkeystorevalue -- `config` defines the action type specific to the configuration and contains the following properties: -<1.1> `from:` is an email address and correspond to *Sender*. -<1.2> `host:` is a string and correspond to *Host*. -<1.3> `port:` is a number and correspond to *Port*. -<1.4> `secure:` is a boolean and correspond to *Secure*. +[cols="2*<"] +|=== -`secrets` defines action type sensitive configuration: +| `from` +| An email address that corresponds to *Sender*. -<2.1> `user:` is a string and correspond to *User*. -<2.2> `password:` is a string and correspond to *Password*. Should be stored in the <>. +| `host` +| A string that corresponds to *Host*. +| `port` +| A number that corresponds to *Port*. + +| `secure` +| A boolean that corresponds to *Secure*. + +|=== + +`secrets` defines sensitive information for the action type: + +[cols="2*<"] +|=== + +| `user` +| A string that corresponds to *User*. + +| `password` +| A string that corresponds to *Password*. Should be stored in the <>. + +|=== [[email-action-configuration]] ==== Action configuration diff --git a/docs/user/alerting/action-types/index.asciidoc b/docs/user/alerting/action-types/index.asciidoc index c71412210c535f..115423086bae3d 100644 --- a/docs/user/alerting/action-types/index.asciidoc +++ b/docs/user/alerting/action-types/index.asciidoc @@ -25,16 +25,26 @@ Execution time field:: This field will be automatically set to the time the ale name: action-type-index actionTypeId: .index config: - index: .kibana <1> - refresh: true <2> - executionTimeField: somedate <3> + index: .kibana + refresh: true + executionTimeField: somedate -- `config` defines the action type specific to the configuration and contains the following properties: -<1> `index:` is a string and correspond to *Index*. -<2> `refresh:` is a boolean and correspond to *Refresh*. -<3> `executionTimeField:` is a string and correspond to *Execution time field*. +[cols="2*<"] +|=== + +|`index` +| A string that corresponds to *Index*. + +|`refresh` +| A boolean that corresponds to *Refresh*. + +|`executionTimeField` +| A string that corresponds to *Execution time field*. + +|=== [float] diff --git a/docs/user/alerting/action-types/pagerduty.asciidoc b/docs/user/alerting/action-types/pagerduty.asciidoc index cd51ec2e3301e9..0468ab042e57eb 100644 --- a/docs/user/alerting/action-types/pagerduty.asciidoc +++ b/docs/user/alerting/action-types/pagerduty.asciidoc @@ -145,18 +145,19 @@ Integration Key:: A 32 character PagerDuty Integration Key for an integration name: preconfigured-pagerduty-action-type actionTypeId: .pagerduty config: - apiUrl: https://test.host <1.1> + apiUrl: https://test.host secrets: - routingKey: testroutingkey <2.1> + routingKey: testroutingkey -- -`config` defines the action type specific to the configuration and contains the following properties: +`config` defines the action type specific to the configuration. +`config` contains +`apiURL`, a string that corresponds to *API URL*. -<1.1> `apiUrl:` is URL string and correspond to *API URL*. +`secrets` defines sensitive information for the action type. +`secrets` contains +`routingKey`, a string that corresponds to *Integration Key*. -`secrets` defines action type sensitive configuration: - -<2.1> `routingKey:` is a string and correspond to *Integration Key*. [float] [[pagerduty-action-configuration]] diff --git a/docs/user/alerting/action-types/pre-configured-connectors.asciidoc b/docs/user/alerting/action-types/pre-configured-connectors.asciidoc new file mode 100644 index 00000000000000..b3e401256f27be --- /dev/null +++ b/docs/user/alerting/action-types/pre-configured-connectors.asciidoc @@ -0,0 +1,121 @@ +[role="xpack"] +[[pre-configured-action-types-and-connectors]] + +=== Preconfigured connectors and action types + +You can preconfigure a connector or action type to have all the information it needs prior to startup +by adding it to the `kibana.yml` file. + +Preconfigured connectors offer the following capabilities: + +- Require no setup. Configuration and credentials needed to execute an +action are predefined, including the connector name and ID. +- Appear in all spaces because they are not saved objects. +- Cannot be edited or deleted. + +A preconfigured action type has only preconfigured connectors. Preconfigured +connectors can belong to either the preconfigured action type or to the regular action type. + +[float] +[[preconfigured-connector-example]] +==== Preconfigured connectors + +This example shows a valid configuration for +two out-of-the box connectors: <> and <>. + +```js + xpack.actions.preconfigured: + my-slack1: <1> + actionTypeId: .slack <2> + name: 'Slack #xyz' <3> + config: <4> + webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz' + webhook-service: + actionTypeId: .webhook + name: 'Email service' + config: + url: 'https://email-alert-service.elastic.co' + method: post + headers: + header1: value1 + header2: value2 + secrets: <5> + user: elastic + password: changeme +``` + +<1> The key is the action connector identifier, `my-slack1` in this example. +<2> `actionTypeId` is the action type identifier. +<3> `name` is the name of the preconfigured connector. +<4> `config` is the action type specific to the configuration. +<5> `secrets` is sensitive configuration, such as username, password, and keys. + +[NOTE] +============================================== +Sensitive properties, such as passwords, can also be stored in the <>. +============================================== + +//// +[float] +[[managing-pre-configured-connectors]] +==== View preconfigured connectors +//// + +In *Management > Alerts and Actions*, preconfigured connectors +appear in the <>, +regardless of which space you are in. +They are tagged as “preconfigured”, and you cannot delete them. + +[role="screenshot"] +image::images/pre-configured-connectors-managing.png[Connectors managing tab with pre-cofigured] + +Clicking a preconfigured connector shows the description, but not the configuration. +A message indicates that this is a preconfigured connector. + +[role="screenshot"] +image::images/pre-configured-connectors-view-screen.png[Pre-configured connector view details] + +The connector details preview is disabled for preconfigured connectors +of a preconfigured action type. + +[role="screenshot"] +image::images/pre-configured-action-type-managing.png[Connectors managing tab with pre-cofigured] + +[float] +[[preconfigured-action-type-example]] +==== Preconfigured action type + +This example shows a preconfigured action type with one out-of-the box connector. + +```js + xpack.actions.enabledActionTypes: ['.slack', '.email', '.index'] <1> + xpack.actions.preconfigured: <2> + my-server-log: + actionTypeId: .server-log + name: 'Server log #xyz' +``` + +<1> `enabledActionTypes` excludes the preconfigured action type to prevent creating and deleting connectors. +<2> `preconfigured` is the setting for defining the list of available connectors for the preconfigured action type. + +[[managing-pre-configured-action-types]] +To attach a preconfigured action to an alert: + +. In *Management > Alerts and Actions*, open the *Connectors* tab. + +. Click *Create connector.* + +. In the list of available action types, select the preconfigured action type you want. ++ +[role="screenshot"] +image::images/pre-configured-action-type-select-type.png[Pre-configured connector create menu] + +. In *Create alert*, open the connector dropdown, and then select the preconfigured +connector. ++ +The `preconfigured` label distinguishes it from a space-aware connector. ++ +[role="screenshot"] +image::images/alert-pre-configured-connectors-dropdown.png[Dropdown list with pre-cofigured connectors] + +. Click *Add action*. diff --git a/docs/user/alerting/action-types/slack.asciidoc b/docs/user/alerting/action-types/slack.asciidoc index afa616ba77b3a7..5bad8a53f898c4 100644 --- a/docs/user/alerting/action-types/slack.asciidoc +++ b/docs/user/alerting/action-types/slack.asciidoc @@ -23,12 +23,12 @@ Webhook URL:: The URL of the incoming webhook. See https://api.slack.com/messa name: preconfigured-slack-action-type actionTypeId: .slack config: - webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz' <1> + webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz' -- -`config` defines the action type specific to the configuration and contains the following properties: - -<1> `webhookUrl:` is URL string and correspond to *Webhook URL*. +`config` defines the action type specific to the configuration. +`config` contains +`webhookUrl`, a string that corresponds to *Webhook URL*. [float] diff --git a/docs/user/alerting/action-types/webhook.asciidoc b/docs/user/alerting/action-types/webhook.asciidoc index 27609652288b5a..c91c24430e982c 100644 --- a/docs/user/alerting/action-types/webhook.asciidoc +++ b/docs/user/alerting/action-types/webhook.asciidoc @@ -19,7 +19,7 @@ Password:: An optional password. If set, HTTP basic authentication is used. Cur [float] [[Preconfigured-webhook-configuration]] -==== Preconfigured action type +==== Preconfigured action type [source,text] -- @@ -27,25 +27,44 @@ Password:: An optional password. If set, HTTP basic authentication is used. Cur name: preconfigured-webhook-action-type actionTypeId: .webhook config: - url: https://test.host <1.1> - method: POST <1.2> - headers: <1.3> + url: https://test.host + method: POST + headers: testheader: testvalue secrets: - user: testuser <2.1> - password: passwordkeystorevalue <2.2> + user: testuser + password: passwordkeystorevalue -- `config` defines the action type specific to the configuration and contains the following properties: -<1.1> `url:` is URL string and correspond to *URL*. -<1.2> `method:` is a string and correspond to *Method*. -<1.3> `headers:` is Record and correspond to *Headers*. +[cols="2*<"] +|=== -`secrets` defines action type sensitive configuration: +|`url` +| A URL string that corresponds to *URL*. + +|`method` +| A string that corresponds to *Method*. + +|`headers` +|A record that corresponds to *Headers*. + +|=== + +`secrets` defines sensitive information for the action type: + +[cols="2*<"] +|=== + +|`user` +|A string that corresponds to *User*. + +|`password` +|A string that corresponds to *Password*. Should be stored in the <>. + +|=== -<2.1> `user:` is a string and correspond to *User*. -<2.2> `password:` is a string and correspond to *Password*. Should be stored in the <>. [float] [[webhook-action-configuration]] diff --git a/docs/user/alerting/images/alert-pre-configured-connectors-dropdown.png b/docs/user/alerting/images/alert-pre-configured-connectors-dropdown.png index 4e6c713298626f..081688758eb484 100644 Binary files a/docs/user/alerting/images/alert-pre-configured-connectors-dropdown.png and b/docs/user/alerting/images/alert-pre-configured-connectors-dropdown.png differ diff --git a/docs/user/alerting/images/alert-pre-configured-slack-connector.png b/docs/user/alerting/images/alert-pre-configured-slack-connector.png index de05e2074dddeb..e9d81877fbf4f9 100644 Binary files a/docs/user/alerting/images/alert-pre-configured-slack-connector.png and b/docs/user/alerting/images/alert-pre-configured-slack-connector.png differ diff --git a/docs/user/alerting/images/pre-configured-action-type-select-type.png b/docs/user/alerting/images/pre-configured-action-type-select-type.png index 29e5a29edc7c06..91ca831840ce92 100644 Binary files a/docs/user/alerting/images/pre-configured-action-type-select-type.png and b/docs/user/alerting/images/pre-configured-action-type-select-type.png differ diff --git a/docs/user/alerting/images/pre-configured-connectors-view-screen.png b/docs/user/alerting/images/pre-configured-connectors-view-screen.png index 43ac44e7536d8f..9c75f86498bebe 100644 Binary files a/docs/user/alerting/images/pre-configured-connectors-view-screen.png and b/docs/user/alerting/images/pre-configured-connectors-view-screen.png differ diff --git a/docs/user/alerting/pre-configured-connectors.asciidoc b/docs/user/alerting/pre-configured-connectors.asciidoc deleted file mode 100644 index d5c20d1853d421..00000000000000 --- a/docs/user/alerting/pre-configured-connectors.asciidoc +++ /dev/null @@ -1,128 +0,0 @@ -[role="xpack"] -[[pre-configured-action-types-and-connectors]] - -== Preconfigured connectors and action types - -You can preconfigure an action type or a connector to have all the information it needs prior to startup -by adding it to the `kibana.yml` file. - -Preconfigured connectors offer the following capabilities: - -- Require no setup. Configuration and credentials needed to execute an -action are predefined, including the connector name and ID. -- Appear in all spaces because they are not saved objects. -- Cannot be edited or deleted. - -Sensitive configuration information, such as credentials, can use the <>. - -A preconfigured action types has only preconfigured connectors. Preconfigured connectors can belong to either the preconfigured action type or to the regular action type. - -[float] -[[preconfigured-connector-example]] -=== Creating a preconfigured connector - -The following example shows a valid configuration of two out-of-the box connectors: <> and <>. - -```js - xpack.actions.preconfigured: - my-slack1: <1> - actionTypeId: .slack <2> - name: 'Slack #xyz' <3> - config: <4> - webhookUrl: 'https://hooks.slack.com/services/abcd/efgh/ijklmnopqrstuvwxyz' - webhook-service: - actionTypeId: .webhook - name: 'Email service' - config: - url: 'https://email-alert-service.elastic.co' - method: post - headers: - header1: value1 - header2: value2 - secrets: <5> - user: elastic - password: changeme -``` - -<1> the key is the action connector identifier, eg `my-slack1` in this example. -<2> `actionTypeId` is the action type identifier. -<3> `name` is the name of the preconfigured connector. -<4> `config` is the action type specific to the configuration. -<5> `secrets` is sensitive configuration, such as username, password, and keys. - -[NOTE] -============================================== -Sensitive properties, such as passwords, can also be stored in the <>. -============================================== - -[float] -[[preconfigured-action-type-example]] -=== Creating a preconfigured action type - -In the `kibana.yml` file: - -. Exclude the action type from `xpack.actions.enabledActionTypes`. -. Add all its preconfigured connectors. - -The following example shows a valid configuration of preconfigured action type with one out-of-the box connector. - -```js - xpack.actions.enabledActionTypes: ['.slack', '.email', '.index'] <1> - xpack.actions.preconfigured: <2> - my-server-log: - actionTypeId: .server-log - name: 'Server log #xyz' -``` - -<1> `enabledActionTypes` should exclude preconfigured action type to prevent creating and deleting connectors. -<2> `preconfigured` is the setting for defining the list of available connectors for the preconfigured action type. - -[float] -[[managing-pre-configured-connectors]] -=== Managing preconfigured connectors - -Preconfigured connectors appear in the connector list, regardless of which space the user is in. -They are tagged as “preconfigured” and cannot be deleted. - -[role="screenshot"] -image::images/pre-configured-connectors-managing.png[Connectors managing tab with pre-cofigured] - -Clicking on a preconfigured connector shows the description, but not any of the configuration. -A message indicates that this is a preconfigured connector. - -[role="screenshot"] -image::images/pre-configured-connectors-view-screen.png[Pre-configured connector view details] - -The connector details preview is disabled for preconfigured connectors. - -[role="screenshot"] -image::images/pre-configured-action-type-managing.png[Connectors managing tab with pre-cofigured] - - -[float] -[[managing-pre-configured-action-types]] -=== Managing preconfigured action types - -Clicking *Create connector* shows the list of available action types. -Disabled action types are not included. - -[role="screenshot"] -image::images/pre-configured-action-type-select-type.png[Pre-configured connector create menu] - -[float] -[[pre-configured-connector-alert-form]] -=== Alert with a preconfigured connector - -When attaching an action to an alert, -select from a list of available action types, and -then select the Slack or Webhook type. Those action types were configured previously. -The preconfigured connector is installed and is automatically selected. - -[role="screenshot"] -image::images/alert-pre-configured-slack-connector.png[Create alert with selected Slack action type] - -The dropdown is populated with additional preconfigured Slack connectors. -The `preconfigured` label distinguishes them from space-aware connectors that use saved objects. - -[role="screenshot"] -image::images/alert-pre-configured-connectors-dropdown.png[Dropdown list with pre-cofigured connectors] diff --git a/docs/user/plugins.asciidoc b/docs/user/plugins.asciidoc index 83c1ab1a842bb4..a96fe811dc84f9 100644 --- a/docs/user/plugins.asciidoc +++ b/docs/user/plugins.asciidoc @@ -33,12 +33,17 @@ $ bin/kibana-plugin install x-pack === Install plugins from an arbitrary URL You can download official Elastic plugins simply by specifying their name. You -can alternatively specify a URL to a specific plugin, as in the following -example: +can alternatively specify a URL or file path to a specific plugin, as in the following +examples: ["source","shell",subs="attributes"] $ bin/kibana-plugin install https://artifacts.elastic.co/downloads/packs/x-pack/x-pack-{version}.zip +or + +["source","shell",subs="attributes"] +$ bin/kibana-plugin install file:///local/path/to/custom_plugin.zip + You can specify URLs that use the HTTP, HTTPS, or `file` protocols. [float] diff --git a/packages/kbn-dev-utils/src/proc_runner/proc_runner.ts b/packages/kbn-dev-utils/src/proc_runner/proc_runner.ts index 14c87766213cd1..1759ca2840c5be 100644 --- a/packages/kbn-dev-utils/src/proc_runner/proc_runner.ts +++ b/packages/kbn-dev-utils/src/proc_runner/proc_runner.ts @@ -18,17 +18,22 @@ */ import moment from 'moment'; -import { filter, first, catchError } from 'rxjs/operators'; +import * as Rx from 'rxjs'; +import { filter, first, catchError, map } from 'rxjs/operators'; import exitHook from 'exit-hook'; import { ToolingLog } from '../tooling_log'; import { createCliError } from './errors'; import { Proc, ProcOptions, startProc } from './proc'; +const SECOND = 1000; +const MINUTE = 60 * SECOND; + const noop = () => {}; interface RunOptions extends ProcOptions { wait: true | RegExp; + waitTimeout?: number | false; } /** @@ -71,6 +76,7 @@ export class ProcRunner { cwd = process.cwd(), stdin = undefined, wait = false, + waitTimeout = 15 * MINUTE, env = process.env, } = options; @@ -97,8 +103,8 @@ export class ProcRunner { try { if (wait instanceof RegExp) { // wait for process to log matching line - await proc.lines$ - .pipe( + await Rx.race( + proc.lines$.pipe( filter(line => wait.test(line)), first(), catchError(err => { @@ -108,8 +114,18 @@ export class ProcRunner { throw err; } }) - ) - .toPromise(); + ), + waitTimeout === false + ? Rx.NEVER + : Rx.timer(waitTimeout).pipe( + map(() => { + const sec = waitTimeout / SECOND; + throw createCliError( + `[${name}] failed to match pattern within ${sec} seconds [pattern=${wait}]` + ); + }) + ) + ).toPromise(); } if (wait === true) { diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 1b70cced4a5c9c..1b670eb8cd8169 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -4838,10 +4838,13 @@ exports.withProcRunner = withProcRunner; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = __webpack_require__(36); const moment_1 = tslib_1.__importDefault(__webpack_require__(40)); -const operators_1 = __webpack_require__(169); -const exit_hook_1 = tslib_1.__importDefault(__webpack_require__(348)); -const errors_1 = __webpack_require__(349); -const proc_1 = __webpack_require__(350); +const Rx = tslib_1.__importStar(__webpack_require__(169)); +const operators_1 = __webpack_require__(270); +const exit_hook_1 = tslib_1.__importDefault(__webpack_require__(368)); +const errors_1 = __webpack_require__(369); +const proc_1 = __webpack_require__(370); +const SECOND = 1000; +const MINUTE = 60 * SECOND; const noop = () => { }; /** * Helper for starting and managing processes. In many ways it resembles the @@ -4875,7 +4878,7 @@ class ProcRunner { * @return {Promise} */ async run(name, options) { - const { cmd, args = [], cwd = process.cwd(), stdin = undefined, wait = false, env = process.env, } = options; + const { cmd, args = [], cwd = process.cwd(), stdin = undefined, wait = false, waitTimeout = 15 * MINUTE, env = process.env, } = options; if (this.closing) { throw new Error('ProcRunner is closing'); } @@ -4895,16 +4898,19 @@ class ProcRunner { try { if (wait instanceof RegExp) { // wait for process to log matching line - await proc.lines$ - .pipe(operators_1.filter(line => wait.test(line)), operators_1.first(), operators_1.catchError(err => { + await Rx.race(proc.lines$.pipe(operators_1.filter(line => wait.test(line)), operators_1.first(), operators_1.catchError(err => { if (err.name !== 'EmptyError') { throw errors_1.createCliError(`[${name}] exited without matching pattern: ${wait}`); } else { throw err; } - })) - .toPromise(); + })), waitTimeout === false + ? Rx.NEVER + : Rx.timer(waitTimeout).pipe(operators_1.map(() => { + const sec = waitTimeout / SECOND; + throw errors_1.createCliError(`[${name}] failed to match pattern within ${sec} seconds [pattern=${wait}]`); + }))).toPromise(); } if (wait === true) { // wait for process to complete @@ -21959,316 +21965,172 @@ webpackContext.id = 41; "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony import */ var _internal_operators_audit__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(170); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "audit", function() { return _internal_operators_audit__WEBPACK_IMPORTED_MODULE_0__["audit"]; }); - -/* harmony import */ var _internal_operators_auditTime__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(198); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "auditTime", function() { return _internal_operators_auditTime__WEBPACK_IMPORTED_MODULE_1__["auditTime"]; }); - -/* harmony import */ var _internal_operators_buffer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(207); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buffer", function() { return _internal_operators_buffer__WEBPACK_IMPORTED_MODULE_2__["buffer"]; }); - -/* harmony import */ var _internal_operators_bufferCount__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(208); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferCount", function() { return _internal_operators_bufferCount__WEBPACK_IMPORTED_MODULE_3__["bufferCount"]; }); - -/* harmony import */ var _internal_operators_bufferTime__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(209); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferTime", function() { return _internal_operators_bufferTime__WEBPACK_IMPORTED_MODULE_4__["bufferTime"]; }); - -/* harmony import */ var _internal_operators_bufferToggle__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(210); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferToggle", function() { return _internal_operators_bufferToggle__WEBPACK_IMPORTED_MODULE_5__["bufferToggle"]; }); - -/* harmony import */ var _internal_operators_bufferWhen__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(211); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferWhen", function() { return _internal_operators_bufferWhen__WEBPACK_IMPORTED_MODULE_6__["bufferWhen"]; }); - -/* harmony import */ var _internal_operators_catchError__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(212); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "catchError", function() { return _internal_operators_catchError__WEBPACK_IMPORTED_MODULE_7__["catchError"]; }); - -/* harmony import */ var _internal_operators_combineAll__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(213); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "combineAll", function() { return _internal_operators_combineAll__WEBPACK_IMPORTED_MODULE_8__["combineAll"]; }); - -/* harmony import */ var _internal_operators_combineLatest__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(217); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "combineLatest", function() { return _internal_operators_combineLatest__WEBPACK_IMPORTED_MODULE_9__["combineLatest"]; }); - -/* harmony import */ var _internal_operators_concat__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(225); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concat", function() { return _internal_operators_concat__WEBPACK_IMPORTED_MODULE_10__["concat"]; }); - -/* harmony import */ var _internal_operators_concatAll__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(228); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concatAll", function() { return _internal_operators_concatAll__WEBPACK_IMPORTED_MODULE_11__["concatAll"]; }); - -/* harmony import */ var _internal_operators_concatMap__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(233); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concatMap", function() { return _internal_operators_concatMap__WEBPACK_IMPORTED_MODULE_12__["concatMap"]; }); - -/* harmony import */ var _internal_operators_concatMapTo__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(234); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concatMapTo", function() { return _internal_operators_concatMapTo__WEBPACK_IMPORTED_MODULE_13__["concatMapTo"]; }); - -/* harmony import */ var _internal_operators_count__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(235); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "count", function() { return _internal_operators_count__WEBPACK_IMPORTED_MODULE_14__["count"]; }); - -/* harmony import */ var _internal_operators_debounce__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(236); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "debounce", function() { return _internal_operators_debounce__WEBPACK_IMPORTED_MODULE_15__["debounce"]; }); - -/* harmony import */ var _internal_operators_debounceTime__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(237); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "debounceTime", function() { return _internal_operators_debounceTime__WEBPACK_IMPORTED_MODULE_16__["debounceTime"]; }); - -/* harmony import */ var _internal_operators_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(238); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "defaultIfEmpty", function() { return _internal_operators_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_17__["defaultIfEmpty"]; }); - -/* harmony import */ var _internal_operators_delay__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(239); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "delay", function() { return _internal_operators_delay__WEBPACK_IMPORTED_MODULE_18__["delay"]; }); - -/* harmony import */ var _internal_operators_delayWhen__WEBPACK_IMPORTED_MODULE_19__ = __webpack_require__(244); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "delayWhen", function() { return _internal_operators_delayWhen__WEBPACK_IMPORTED_MODULE_19__["delayWhen"]; }); - -/* harmony import */ var _internal_operators_dematerialize__WEBPACK_IMPORTED_MODULE_20__ = __webpack_require__(245); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "dematerialize", function() { return _internal_operators_dematerialize__WEBPACK_IMPORTED_MODULE_20__["dematerialize"]; }); - -/* harmony import */ var _internal_operators_distinct__WEBPACK_IMPORTED_MODULE_21__ = __webpack_require__(246); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "distinct", function() { return _internal_operators_distinct__WEBPACK_IMPORTED_MODULE_21__["distinct"]; }); - -/* harmony import */ var _internal_operators_distinctUntilChanged__WEBPACK_IMPORTED_MODULE_22__ = __webpack_require__(247); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "distinctUntilChanged", function() { return _internal_operators_distinctUntilChanged__WEBPACK_IMPORTED_MODULE_22__["distinctUntilChanged"]; }); - -/* harmony import */ var _internal_operators_distinctUntilKeyChanged__WEBPACK_IMPORTED_MODULE_23__ = __webpack_require__(248); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "distinctUntilKeyChanged", function() { return _internal_operators_distinctUntilKeyChanged__WEBPACK_IMPORTED_MODULE_23__["distinctUntilKeyChanged"]; }); - -/* harmony import */ var _internal_operators_elementAt__WEBPACK_IMPORTED_MODULE_24__ = __webpack_require__(249); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "elementAt", function() { return _internal_operators_elementAt__WEBPACK_IMPORTED_MODULE_24__["elementAt"]; }); - -/* harmony import */ var _internal_operators_endWith__WEBPACK_IMPORTED_MODULE_25__ = __webpack_require__(255); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "endWith", function() { return _internal_operators_endWith__WEBPACK_IMPORTED_MODULE_25__["endWith"]; }); - -/* harmony import */ var _internal_operators_every__WEBPACK_IMPORTED_MODULE_26__ = __webpack_require__(256); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "every", function() { return _internal_operators_every__WEBPACK_IMPORTED_MODULE_26__["every"]; }); - -/* harmony import */ var _internal_operators_exhaust__WEBPACK_IMPORTED_MODULE_27__ = __webpack_require__(257); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "exhaust", function() { return _internal_operators_exhaust__WEBPACK_IMPORTED_MODULE_27__["exhaust"]; }); - -/* harmony import */ var _internal_operators_exhaustMap__WEBPACK_IMPORTED_MODULE_28__ = __webpack_require__(258); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "exhaustMap", function() { return _internal_operators_exhaustMap__WEBPACK_IMPORTED_MODULE_28__["exhaustMap"]; }); - -/* harmony import */ var _internal_operators_expand__WEBPACK_IMPORTED_MODULE_29__ = __webpack_require__(259); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "expand", function() { return _internal_operators_expand__WEBPACK_IMPORTED_MODULE_29__["expand"]; }); - -/* harmony import */ var _internal_operators_filter__WEBPACK_IMPORTED_MODULE_30__ = __webpack_require__(251); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "filter", function() { return _internal_operators_filter__WEBPACK_IMPORTED_MODULE_30__["filter"]; }); - -/* harmony import */ var _internal_operators_finalize__WEBPACK_IMPORTED_MODULE_31__ = __webpack_require__(260); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "finalize", function() { return _internal_operators_finalize__WEBPACK_IMPORTED_MODULE_31__["finalize"]; }); - -/* harmony import */ var _internal_operators_find__WEBPACK_IMPORTED_MODULE_32__ = __webpack_require__(261); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "find", function() { return _internal_operators_find__WEBPACK_IMPORTED_MODULE_32__["find"]; }); - -/* harmony import */ var _internal_operators_findIndex__WEBPACK_IMPORTED_MODULE_33__ = __webpack_require__(262); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "findIndex", function() { return _internal_operators_findIndex__WEBPACK_IMPORTED_MODULE_33__["findIndex"]; }); - -/* harmony import */ var _internal_operators_first__WEBPACK_IMPORTED_MODULE_34__ = __webpack_require__(263); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "first", function() { return _internal_operators_first__WEBPACK_IMPORTED_MODULE_34__["first"]; }); - -/* harmony import */ var _internal_operators_groupBy__WEBPACK_IMPORTED_MODULE_35__ = __webpack_require__(264); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "groupBy", function() { return _internal_operators_groupBy__WEBPACK_IMPORTED_MODULE_35__["groupBy"]; }); - -/* harmony import */ var _internal_operators_ignoreElements__WEBPACK_IMPORTED_MODULE_36__ = __webpack_require__(268); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "ignoreElements", function() { return _internal_operators_ignoreElements__WEBPACK_IMPORTED_MODULE_36__["ignoreElements"]; }); - -/* harmony import */ var _internal_operators_isEmpty__WEBPACK_IMPORTED_MODULE_37__ = __webpack_require__(269); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "isEmpty", function() { return _internal_operators_isEmpty__WEBPACK_IMPORTED_MODULE_37__["isEmpty"]; }); - -/* harmony import */ var _internal_operators_last__WEBPACK_IMPORTED_MODULE_38__ = __webpack_require__(270); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "last", function() { return _internal_operators_last__WEBPACK_IMPORTED_MODULE_38__["last"]; }); - -/* harmony import */ var _internal_operators_map__WEBPACK_IMPORTED_MODULE_39__ = __webpack_require__(231); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "map", function() { return _internal_operators_map__WEBPACK_IMPORTED_MODULE_39__["map"]; }); - -/* harmony import */ var _internal_operators_mapTo__WEBPACK_IMPORTED_MODULE_40__ = __webpack_require__(272); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mapTo", function() { return _internal_operators_mapTo__WEBPACK_IMPORTED_MODULE_40__["mapTo"]; }); - -/* harmony import */ var _internal_operators_materialize__WEBPACK_IMPORTED_MODULE_41__ = __webpack_require__(273); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "materialize", function() { return _internal_operators_materialize__WEBPACK_IMPORTED_MODULE_41__["materialize"]; }); - -/* harmony import */ var _internal_operators_max__WEBPACK_IMPORTED_MODULE_42__ = __webpack_require__(274); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "max", function() { return _internal_operators_max__WEBPACK_IMPORTED_MODULE_42__["max"]; }); - -/* harmony import */ var _internal_operators_merge__WEBPACK_IMPORTED_MODULE_43__ = __webpack_require__(277); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "merge", function() { return _internal_operators_merge__WEBPACK_IMPORTED_MODULE_43__["merge"]; }); - -/* harmony import */ var _internal_operators_mergeAll__WEBPACK_IMPORTED_MODULE_44__ = __webpack_require__(229); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mergeAll", function() { return _internal_operators_mergeAll__WEBPACK_IMPORTED_MODULE_44__["mergeAll"]; }); - -/* harmony import */ var _internal_operators_mergeMap__WEBPACK_IMPORTED_MODULE_45__ = __webpack_require__(230); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mergeMap", function() { return _internal_operators_mergeMap__WEBPACK_IMPORTED_MODULE_45__["mergeMap"]; }); - -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "flatMap", function() { return _internal_operators_mergeMap__WEBPACK_IMPORTED_MODULE_45__["mergeMap"]; }); - -/* harmony import */ var _internal_operators_mergeMapTo__WEBPACK_IMPORTED_MODULE_46__ = __webpack_require__(279); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mergeMapTo", function() { return _internal_operators_mergeMapTo__WEBPACK_IMPORTED_MODULE_46__["mergeMapTo"]; }); +/* harmony import */ var _internal_Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(170); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "Observable", function() { return _internal_Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"]; }); -/* harmony import */ var _internal_operators_mergeScan__WEBPACK_IMPORTED_MODULE_47__ = __webpack_require__(280); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mergeScan", function() { return _internal_operators_mergeScan__WEBPACK_IMPORTED_MODULE_47__["mergeScan"]; }); +/* harmony import */ var _internal_observable_ConnectableObservable__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(186); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "ConnectableObservable", function() { return _internal_observable_ConnectableObservable__WEBPACK_IMPORTED_MODULE_1__["ConnectableObservable"]; }); -/* harmony import */ var _internal_operators_min__WEBPACK_IMPORTED_MODULE_48__ = __webpack_require__(281); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "min", function() { return _internal_operators_min__WEBPACK_IMPORTED_MODULE_48__["min"]; }); +/* harmony import */ var _internal_operators_groupBy__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(191); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "GroupedObservable", function() { return _internal_operators_groupBy__WEBPACK_IMPORTED_MODULE_2__["GroupedObservable"]; }); -/* harmony import */ var _internal_operators_multicast__WEBPACK_IMPORTED_MODULE_49__ = __webpack_require__(282); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "multicast", function() { return _internal_operators_multicast__WEBPACK_IMPORTED_MODULE_49__["multicast"]; }); +/* harmony import */ var _internal_symbol_observable__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(183); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "observable", function() { return _internal_symbol_observable__WEBPACK_IMPORTED_MODULE_3__["observable"]; }); -/* harmony import */ var _internal_operators_observeOn__WEBPACK_IMPORTED_MODULE_50__ = __webpack_require__(285); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "observeOn", function() { return _internal_operators_observeOn__WEBPACK_IMPORTED_MODULE_50__["observeOn"]; }); +/* harmony import */ var _internal_Subject__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(187); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "Subject", function() { return _internal_Subject__WEBPACK_IMPORTED_MODULE_4__["Subject"]; }); -/* harmony import */ var _internal_operators_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_51__ = __webpack_require__(286); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "onErrorResumeNext", function() { return _internal_operators_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_51__["onErrorResumeNext"]; }); +/* harmony import */ var _internal_BehaviorSubject__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(192); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "BehaviorSubject", function() { return _internal_BehaviorSubject__WEBPACK_IMPORTED_MODULE_5__["BehaviorSubject"]; }); -/* harmony import */ var _internal_operators_pairwise__WEBPACK_IMPORTED_MODULE_52__ = __webpack_require__(287); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "pairwise", function() { return _internal_operators_pairwise__WEBPACK_IMPORTED_MODULE_52__["pairwise"]; }); +/* harmony import */ var _internal_ReplaySubject__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(193); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "ReplaySubject", function() { return _internal_ReplaySubject__WEBPACK_IMPORTED_MODULE_6__["ReplaySubject"]; }); -/* harmony import */ var _internal_operators_partition__WEBPACK_IMPORTED_MODULE_53__ = __webpack_require__(288); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "partition", function() { return _internal_operators_partition__WEBPACK_IMPORTED_MODULE_53__["partition"]; }); +/* harmony import */ var _internal_AsyncSubject__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(210); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "AsyncSubject", function() { return _internal_AsyncSubject__WEBPACK_IMPORTED_MODULE_7__["AsyncSubject"]; }); -/* harmony import */ var _internal_operators_pluck__WEBPACK_IMPORTED_MODULE_54__ = __webpack_require__(290); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "pluck", function() { return _internal_operators_pluck__WEBPACK_IMPORTED_MODULE_54__["pluck"]; }); +/* harmony import */ var _internal_scheduler_asap__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(211); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "asapScheduler", function() { return _internal_scheduler_asap__WEBPACK_IMPORTED_MODULE_8__["asap"]; }); -/* harmony import */ var _internal_operators_publish__WEBPACK_IMPORTED_MODULE_55__ = __webpack_require__(291); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publish", function() { return _internal_operators_publish__WEBPACK_IMPORTED_MODULE_55__["publish"]; }); +/* harmony import */ var _internal_scheduler_async__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(215); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "asyncScheduler", function() { return _internal_scheduler_async__WEBPACK_IMPORTED_MODULE_9__["async"]; }); -/* harmony import */ var _internal_operators_publishBehavior__WEBPACK_IMPORTED_MODULE_56__ = __webpack_require__(292); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publishBehavior", function() { return _internal_operators_publishBehavior__WEBPACK_IMPORTED_MODULE_56__["publishBehavior"]; }); +/* harmony import */ var _internal_scheduler_queue__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(194); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "queueScheduler", function() { return _internal_scheduler_queue__WEBPACK_IMPORTED_MODULE_10__["queue"]; }); -/* harmony import */ var _internal_operators_publishLast__WEBPACK_IMPORTED_MODULE_57__ = __webpack_require__(294); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publishLast", function() { return _internal_operators_publishLast__WEBPACK_IMPORTED_MODULE_57__["publishLast"]; }); +/* harmony import */ var _internal_scheduler_animationFrame__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(216); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "animationFrameScheduler", function() { return _internal_scheduler_animationFrame__WEBPACK_IMPORTED_MODULE_11__["animationFrame"]; }); -/* harmony import */ var _internal_operators_publishReplay__WEBPACK_IMPORTED_MODULE_58__ = __webpack_require__(296); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publishReplay", function() { return _internal_operators_publishReplay__WEBPACK_IMPORTED_MODULE_58__["publishReplay"]; }); +/* harmony import */ var _internal_scheduler_VirtualTimeScheduler__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(219); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "VirtualTimeScheduler", function() { return _internal_scheduler_VirtualTimeScheduler__WEBPACK_IMPORTED_MODULE_12__["VirtualTimeScheduler"]; }); -/* harmony import */ var _internal_operators_race__WEBPACK_IMPORTED_MODULE_59__ = __webpack_require__(301); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "race", function() { return _internal_operators_race__WEBPACK_IMPORTED_MODULE_59__["race"]; }); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "VirtualAction", function() { return _internal_scheduler_VirtualTimeScheduler__WEBPACK_IMPORTED_MODULE_12__["VirtualAction"]; }); -/* harmony import */ var _internal_operators_reduce__WEBPACK_IMPORTED_MODULE_60__ = __webpack_require__(275); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "reduce", function() { return _internal_operators_reduce__WEBPACK_IMPORTED_MODULE_60__["reduce"]; }); +/* harmony import */ var _internal_Scheduler__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(200); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "Scheduler", function() { return _internal_Scheduler__WEBPACK_IMPORTED_MODULE_13__["Scheduler"]; }); -/* harmony import */ var _internal_operators_repeat__WEBPACK_IMPORTED_MODULE_61__ = __webpack_require__(303); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "repeat", function() { return _internal_operators_repeat__WEBPACK_IMPORTED_MODULE_61__["repeat"]; }); +/* harmony import */ var _internal_Subscription__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(177); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "Subscription", function() { return _internal_Subscription__WEBPACK_IMPORTED_MODULE_14__["Subscription"]; }); -/* harmony import */ var _internal_operators_repeatWhen__WEBPACK_IMPORTED_MODULE_62__ = __webpack_require__(304); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "repeatWhen", function() { return _internal_operators_repeatWhen__WEBPACK_IMPORTED_MODULE_62__["repeatWhen"]; }); +/* harmony import */ var _internal_Subscriber__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(172); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "Subscriber", function() { return _internal_Subscriber__WEBPACK_IMPORTED_MODULE_15__["Subscriber"]; }); -/* harmony import */ var _internal_operators_retry__WEBPACK_IMPORTED_MODULE_63__ = __webpack_require__(305); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "retry", function() { return _internal_operators_retry__WEBPACK_IMPORTED_MODULE_63__["retry"]; }); +/* harmony import */ var _internal_Notification__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(202); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "Notification", function() { return _internal_Notification__WEBPACK_IMPORTED_MODULE_16__["Notification"]; }); -/* harmony import */ var _internal_operators_retryWhen__WEBPACK_IMPORTED_MODULE_64__ = __webpack_require__(306); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "retryWhen", function() { return _internal_operators_retryWhen__WEBPACK_IMPORTED_MODULE_64__["retryWhen"]; }); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "NotificationKind", function() { return _internal_Notification__WEBPACK_IMPORTED_MODULE_16__["NotificationKind"]; }); -/* harmony import */ var _internal_operators_refCount__WEBPACK_IMPORTED_MODULE_65__ = __webpack_require__(284); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "refCount", function() { return _internal_operators_refCount__WEBPACK_IMPORTED_MODULE_65__["refCount"]; }); +/* harmony import */ var _internal_util_pipe__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(184); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "pipe", function() { return _internal_util_pipe__WEBPACK_IMPORTED_MODULE_17__["pipe"]; }); -/* harmony import */ var _internal_operators_sample__WEBPACK_IMPORTED_MODULE_66__ = __webpack_require__(307); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "sample", function() { return _internal_operators_sample__WEBPACK_IMPORTED_MODULE_66__["sample"]; }); +/* harmony import */ var _internal_util_noop__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(185); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "noop", function() { return _internal_util_noop__WEBPACK_IMPORTED_MODULE_18__["noop"]; }); -/* harmony import */ var _internal_operators_sampleTime__WEBPACK_IMPORTED_MODULE_67__ = __webpack_require__(308); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "sampleTime", function() { return _internal_operators_sampleTime__WEBPACK_IMPORTED_MODULE_67__["sampleTime"]; }); +/* harmony import */ var _internal_util_identity__WEBPACK_IMPORTED_MODULE_19__ = __webpack_require__(220); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "identity", function() { return _internal_util_identity__WEBPACK_IMPORTED_MODULE_19__["identity"]; }); -/* harmony import */ var _internal_operators_scan__WEBPACK_IMPORTED_MODULE_68__ = __webpack_require__(276); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "scan", function() { return _internal_operators_scan__WEBPACK_IMPORTED_MODULE_68__["scan"]; }); +/* harmony import */ var _internal_util_isObservable__WEBPACK_IMPORTED_MODULE_20__ = __webpack_require__(221); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "isObservable", function() { return _internal_util_isObservable__WEBPACK_IMPORTED_MODULE_20__["isObservable"]; }); -/* harmony import */ var _internal_operators_sequenceEqual__WEBPACK_IMPORTED_MODULE_69__ = __webpack_require__(309); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "sequenceEqual", function() { return _internal_operators_sequenceEqual__WEBPACK_IMPORTED_MODULE_69__["sequenceEqual"]; }); +/* harmony import */ var _internal_util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_21__ = __webpack_require__(222); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "ArgumentOutOfRangeError", function() { return _internal_util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_21__["ArgumentOutOfRangeError"]; }); -/* harmony import */ var _internal_operators_share__WEBPACK_IMPORTED_MODULE_70__ = __webpack_require__(310); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "share", function() { return _internal_operators_share__WEBPACK_IMPORTED_MODULE_70__["share"]; }); +/* harmony import */ var _internal_util_EmptyError__WEBPACK_IMPORTED_MODULE_22__ = __webpack_require__(223); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "EmptyError", function() { return _internal_util_EmptyError__WEBPACK_IMPORTED_MODULE_22__["EmptyError"]; }); -/* harmony import */ var _internal_operators_shareReplay__WEBPACK_IMPORTED_MODULE_71__ = __webpack_require__(311); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "shareReplay", function() { return _internal_operators_shareReplay__WEBPACK_IMPORTED_MODULE_71__["shareReplay"]; }); +/* harmony import */ var _internal_util_ObjectUnsubscribedError__WEBPACK_IMPORTED_MODULE_23__ = __webpack_require__(188); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "ObjectUnsubscribedError", function() { return _internal_util_ObjectUnsubscribedError__WEBPACK_IMPORTED_MODULE_23__["ObjectUnsubscribedError"]; }); -/* harmony import */ var _internal_operators_single__WEBPACK_IMPORTED_MODULE_72__ = __webpack_require__(312); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "single", function() { return _internal_operators_single__WEBPACK_IMPORTED_MODULE_72__["single"]; }); +/* harmony import */ var _internal_util_UnsubscriptionError__WEBPACK_IMPORTED_MODULE_24__ = __webpack_require__(180); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "UnsubscriptionError", function() { return _internal_util_UnsubscriptionError__WEBPACK_IMPORTED_MODULE_24__["UnsubscriptionError"]; }); -/* harmony import */ var _internal_operators_skip__WEBPACK_IMPORTED_MODULE_73__ = __webpack_require__(313); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skip", function() { return _internal_operators_skip__WEBPACK_IMPORTED_MODULE_73__["skip"]; }); +/* harmony import */ var _internal_util_TimeoutError__WEBPACK_IMPORTED_MODULE_25__ = __webpack_require__(224); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "TimeoutError", function() { return _internal_util_TimeoutError__WEBPACK_IMPORTED_MODULE_25__["TimeoutError"]; }); -/* harmony import */ var _internal_operators_skipLast__WEBPACK_IMPORTED_MODULE_74__ = __webpack_require__(314); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skipLast", function() { return _internal_operators_skipLast__WEBPACK_IMPORTED_MODULE_74__["skipLast"]; }); +/* harmony import */ var _internal_observable_bindCallback__WEBPACK_IMPORTED_MODULE_26__ = __webpack_require__(225); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bindCallback", function() { return _internal_observable_bindCallback__WEBPACK_IMPORTED_MODULE_26__["bindCallback"]; }); -/* harmony import */ var _internal_operators_skipUntil__WEBPACK_IMPORTED_MODULE_75__ = __webpack_require__(315); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skipUntil", function() { return _internal_operators_skipUntil__WEBPACK_IMPORTED_MODULE_75__["skipUntil"]; }); +/* harmony import */ var _internal_observable_bindNodeCallback__WEBPACK_IMPORTED_MODULE_27__ = __webpack_require__(227); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bindNodeCallback", function() { return _internal_observable_bindNodeCallback__WEBPACK_IMPORTED_MODULE_27__["bindNodeCallback"]; }); -/* harmony import */ var _internal_operators_skipWhile__WEBPACK_IMPORTED_MODULE_76__ = __webpack_require__(316); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skipWhile", function() { return _internal_operators_skipWhile__WEBPACK_IMPORTED_MODULE_76__["skipWhile"]; }); +/* harmony import */ var _internal_observable_combineLatest__WEBPACK_IMPORTED_MODULE_28__ = __webpack_require__(228); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "combineLatest", function() { return _internal_observable_combineLatest__WEBPACK_IMPORTED_MODULE_28__["combineLatest"]; }); -/* harmony import */ var _internal_operators_startWith__WEBPACK_IMPORTED_MODULE_77__ = __webpack_require__(317); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "startWith", function() { return _internal_operators_startWith__WEBPACK_IMPORTED_MODULE_77__["startWith"]; }); +/* harmony import */ var _internal_observable_concat__WEBPACK_IMPORTED_MODULE_29__ = __webpack_require__(239); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concat", function() { return _internal_observable_concat__WEBPACK_IMPORTED_MODULE_29__["concat"]; }); -/* harmony import */ var _internal_operators_subscribeOn__WEBPACK_IMPORTED_MODULE_78__ = __webpack_require__(318); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "subscribeOn", function() { return _internal_operators_subscribeOn__WEBPACK_IMPORTED_MODULE_78__["subscribeOn"]; }); +/* harmony import */ var _internal_observable_defer__WEBPACK_IMPORTED_MODULE_30__ = __webpack_require__(250); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "defer", function() { return _internal_observable_defer__WEBPACK_IMPORTED_MODULE_30__["defer"]; }); -/* harmony import */ var _internal_operators_switchAll__WEBPACK_IMPORTED_MODULE_79__ = __webpack_require__(324); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "switchAll", function() { return _internal_operators_switchAll__WEBPACK_IMPORTED_MODULE_79__["switchAll"]; }); +/* harmony import */ var _internal_observable_empty__WEBPACK_IMPORTED_MODULE_31__ = __webpack_require__(203); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "empty", function() { return _internal_observable_empty__WEBPACK_IMPORTED_MODULE_31__["empty"]; }); -/* harmony import */ var _internal_operators_switchMap__WEBPACK_IMPORTED_MODULE_80__ = __webpack_require__(325); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "switchMap", function() { return _internal_operators_switchMap__WEBPACK_IMPORTED_MODULE_80__["switchMap"]; }); +/* harmony import */ var _internal_observable_forkJoin__WEBPACK_IMPORTED_MODULE_32__ = __webpack_require__(251); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "forkJoin", function() { return _internal_observable_forkJoin__WEBPACK_IMPORTED_MODULE_32__["forkJoin"]; }); -/* harmony import */ var _internal_operators_switchMapTo__WEBPACK_IMPORTED_MODULE_81__ = __webpack_require__(326); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "switchMapTo", function() { return _internal_operators_switchMapTo__WEBPACK_IMPORTED_MODULE_81__["switchMapTo"]; }); +/* harmony import */ var _internal_observable_from__WEBPACK_IMPORTED_MODULE_33__ = __webpack_require__(243); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "from", function() { return _internal_observable_from__WEBPACK_IMPORTED_MODULE_33__["from"]; }); -/* harmony import */ var _internal_operators_take__WEBPACK_IMPORTED_MODULE_82__ = __webpack_require__(254); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "take", function() { return _internal_operators_take__WEBPACK_IMPORTED_MODULE_82__["take"]; }); +/* harmony import */ var _internal_observable_fromEvent__WEBPACK_IMPORTED_MODULE_34__ = __webpack_require__(252); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "fromEvent", function() { return _internal_observable_fromEvent__WEBPACK_IMPORTED_MODULE_34__["fromEvent"]; }); -/* harmony import */ var _internal_operators_takeLast__WEBPACK_IMPORTED_MODULE_83__ = __webpack_require__(271); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "takeLast", function() { return _internal_operators_takeLast__WEBPACK_IMPORTED_MODULE_83__["takeLast"]; }); +/* harmony import */ var _internal_observable_fromEventPattern__WEBPACK_IMPORTED_MODULE_35__ = __webpack_require__(253); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "fromEventPattern", function() { return _internal_observable_fromEventPattern__WEBPACK_IMPORTED_MODULE_35__["fromEventPattern"]; }); -/* harmony import */ var _internal_operators_takeUntil__WEBPACK_IMPORTED_MODULE_84__ = __webpack_require__(327); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "takeUntil", function() { return _internal_operators_takeUntil__WEBPACK_IMPORTED_MODULE_84__["takeUntil"]; }); +/* harmony import */ var _internal_observable_generate__WEBPACK_IMPORTED_MODULE_36__ = __webpack_require__(254); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "generate", function() { return _internal_observable_generate__WEBPACK_IMPORTED_MODULE_36__["generate"]; }); -/* harmony import */ var _internal_operators_takeWhile__WEBPACK_IMPORTED_MODULE_85__ = __webpack_require__(328); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "takeWhile", function() { return _internal_operators_takeWhile__WEBPACK_IMPORTED_MODULE_85__["takeWhile"]; }); +/* harmony import */ var _internal_observable_iif__WEBPACK_IMPORTED_MODULE_37__ = __webpack_require__(255); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "iif", function() { return _internal_observable_iif__WEBPACK_IMPORTED_MODULE_37__["iif"]; }); -/* harmony import */ var _internal_operators_tap__WEBPACK_IMPORTED_MODULE_86__ = __webpack_require__(329); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "tap", function() { return _internal_operators_tap__WEBPACK_IMPORTED_MODULE_86__["tap"]; }); +/* harmony import */ var _internal_observable_interval__WEBPACK_IMPORTED_MODULE_38__ = __webpack_require__(256); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "interval", function() { return _internal_observable_interval__WEBPACK_IMPORTED_MODULE_38__["interval"]; }); -/* harmony import */ var _internal_operators_throttle__WEBPACK_IMPORTED_MODULE_87__ = __webpack_require__(330); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "throttle", function() { return _internal_operators_throttle__WEBPACK_IMPORTED_MODULE_87__["throttle"]; }); +/* harmony import */ var _internal_observable_merge__WEBPACK_IMPORTED_MODULE_39__ = __webpack_require__(258); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "merge", function() { return _internal_observable_merge__WEBPACK_IMPORTED_MODULE_39__["merge"]; }); -/* harmony import */ var _internal_operators_throttleTime__WEBPACK_IMPORTED_MODULE_88__ = __webpack_require__(331); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "throttleTime", function() { return _internal_operators_throttleTime__WEBPACK_IMPORTED_MODULE_88__["throttleTime"]; }); +/* harmony import */ var _internal_observable_never__WEBPACK_IMPORTED_MODULE_40__ = __webpack_require__(259); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "never", function() { return _internal_observable_never__WEBPACK_IMPORTED_MODULE_40__["never"]; }); -/* harmony import */ var _internal_operators_throwIfEmpty__WEBPACK_IMPORTED_MODULE_89__ = __webpack_require__(252); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "throwIfEmpty", function() { return _internal_operators_throwIfEmpty__WEBPACK_IMPORTED_MODULE_89__["throwIfEmpty"]; }); +/* harmony import */ var _internal_observable_of__WEBPACK_IMPORTED_MODULE_41__ = __webpack_require__(204); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "of", function() { return _internal_observable_of__WEBPACK_IMPORTED_MODULE_41__["of"]; }); -/* harmony import */ var _internal_operators_timeInterval__WEBPACK_IMPORTED_MODULE_90__ = __webpack_require__(332); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timeInterval", function() { return _internal_operators_timeInterval__WEBPACK_IMPORTED_MODULE_90__["timeInterval"]; }); +/* harmony import */ var _internal_observable_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_42__ = __webpack_require__(260); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "onErrorResumeNext", function() { return _internal_observable_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_42__["onErrorResumeNext"]; }); -/* harmony import */ var _internal_operators_timeout__WEBPACK_IMPORTED_MODULE_91__ = __webpack_require__(334); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timeout", function() { return _internal_operators_timeout__WEBPACK_IMPORTED_MODULE_91__["timeout"]; }); +/* harmony import */ var _internal_observable_pairs__WEBPACK_IMPORTED_MODULE_43__ = __webpack_require__(261); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "pairs", function() { return _internal_observable_pairs__WEBPACK_IMPORTED_MODULE_43__["pairs"]; }); -/* harmony import */ var _internal_operators_timeoutWith__WEBPACK_IMPORTED_MODULE_92__ = __webpack_require__(336); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timeoutWith", function() { return _internal_operators_timeoutWith__WEBPACK_IMPORTED_MODULE_92__["timeoutWith"]; }); +/* harmony import */ var _internal_observable_partition__WEBPACK_IMPORTED_MODULE_44__ = __webpack_require__(262); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "partition", function() { return _internal_observable_partition__WEBPACK_IMPORTED_MODULE_44__["partition"]; }); -/* harmony import */ var _internal_operators_timestamp__WEBPACK_IMPORTED_MODULE_93__ = __webpack_require__(337); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timestamp", function() { return _internal_operators_timestamp__WEBPACK_IMPORTED_MODULE_93__["timestamp"]; }); +/* harmony import */ var _internal_observable_race__WEBPACK_IMPORTED_MODULE_45__ = __webpack_require__(265); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "race", function() { return _internal_observable_race__WEBPACK_IMPORTED_MODULE_45__["race"]; }); -/* harmony import */ var _internal_operators_toArray__WEBPACK_IMPORTED_MODULE_94__ = __webpack_require__(338); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "toArray", function() { return _internal_operators_toArray__WEBPACK_IMPORTED_MODULE_94__["toArray"]; }); +/* harmony import */ var _internal_observable_range__WEBPACK_IMPORTED_MODULE_46__ = __webpack_require__(266); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "range", function() { return _internal_observable_range__WEBPACK_IMPORTED_MODULE_46__["range"]; }); -/* harmony import */ var _internal_operators_window__WEBPACK_IMPORTED_MODULE_95__ = __webpack_require__(339); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "window", function() { return _internal_operators_window__WEBPACK_IMPORTED_MODULE_95__["window"]; }); +/* harmony import */ var _internal_observable_throwError__WEBPACK_IMPORTED_MODULE_47__ = __webpack_require__(209); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "throwError", function() { return _internal_observable_throwError__WEBPACK_IMPORTED_MODULE_47__["throwError"]; }); -/* harmony import */ var _internal_operators_windowCount__WEBPACK_IMPORTED_MODULE_96__ = __webpack_require__(340); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowCount", function() { return _internal_operators_windowCount__WEBPACK_IMPORTED_MODULE_96__["windowCount"]; }); +/* harmony import */ var _internal_observable_timer__WEBPACK_IMPORTED_MODULE_48__ = __webpack_require__(267); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timer", function() { return _internal_observable_timer__WEBPACK_IMPORTED_MODULE_48__["timer"]; }); -/* harmony import */ var _internal_operators_windowTime__WEBPACK_IMPORTED_MODULE_97__ = __webpack_require__(341); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowTime", function() { return _internal_operators_windowTime__WEBPACK_IMPORTED_MODULE_97__["windowTime"]; }); +/* harmony import */ var _internal_observable_using__WEBPACK_IMPORTED_MODULE_49__ = __webpack_require__(268); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "using", function() { return _internal_observable_using__WEBPACK_IMPORTED_MODULE_49__["using"]; }); -/* harmony import */ var _internal_operators_windowToggle__WEBPACK_IMPORTED_MODULE_98__ = __webpack_require__(342); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowToggle", function() { return _internal_operators_windowToggle__WEBPACK_IMPORTED_MODULE_98__["windowToggle"]; }); +/* harmony import */ var _internal_observable_zip__WEBPACK_IMPORTED_MODULE_50__ = __webpack_require__(269); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "zip", function() { return _internal_observable_zip__WEBPACK_IMPORTED_MODULE_50__["zip"]; }); -/* harmony import */ var _internal_operators_windowWhen__WEBPACK_IMPORTED_MODULE_99__ = __webpack_require__(343); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowWhen", function() { return _internal_operators_windowWhen__WEBPACK_IMPORTED_MODULE_99__["windowWhen"]; }); +/* harmony import */ var _internal_scheduled_scheduled__WEBPACK_IMPORTED_MODULE_51__ = __webpack_require__(244); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "scheduled", function() { return _internal_scheduled_scheduled__WEBPACK_IMPORTED_MODULE_51__["scheduled"]; }); -/* harmony import */ var _internal_operators_withLatestFrom__WEBPACK_IMPORTED_MODULE_100__ = __webpack_require__(344); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "withLatestFrom", function() { return _internal_operators_withLatestFrom__WEBPACK_IMPORTED_MODULE_100__["withLatestFrom"]; }); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "EMPTY", function() { return _internal_observable_empty__WEBPACK_IMPORTED_MODULE_31__["EMPTY"]; }); -/* harmony import */ var _internal_operators_zip__WEBPACK_IMPORTED_MODULE_101__ = __webpack_require__(345); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "zip", function() { return _internal_operators_zip__WEBPACK_IMPORTED_MODULE_101__["zip"]; }); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "NEVER", function() { return _internal_observable_never__WEBPACK_IMPORTED_MODULE_40__["NEVER"]; }); -/* harmony import */ var _internal_operators_zipAll__WEBPACK_IMPORTED_MODULE_102__ = __webpack_require__(347); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "zipAll", function() { return _internal_operators_zipAll__WEBPACK_IMPORTED_MODULE_102__["zipAll"]; }); +/* harmony import */ var _internal_config__WEBPACK_IMPORTED_MODULE_52__ = __webpack_require__(175); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "config", function() { return _internal_config__WEBPACK_IMPORTED_MODULE_52__["config"]; }); /** PURE_IMPORTS_START PURE_IMPORTS_END */ @@ -22305,55 +22167,6 @@ __webpack_require__.r(__webpack_exports__); - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -22384,79 +22197,128 @@ __webpack_require__.r(__webpack_exports__); "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "audit", function() { return audit; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(171); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(182); -/** PURE_IMPORTS_START tslib,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Observable", function() { return Observable; }); +/* harmony import */ var _util_canReportError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(171); +/* harmony import */ var _util_toSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(182); +/* harmony import */ var _symbol_observable__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(183); +/* harmony import */ var _util_pipe__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(184); +/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(175); +/** PURE_IMPORTS_START _util_canReportError,_util_toSubscriber,_symbol_observable,_util_pipe,_config PURE_IMPORTS_END */ -function audit(durationSelector) { - return function auditOperatorFunction(source) { - return source.lift(new AuditOperator(durationSelector)); - }; -} -var AuditOperator = /*@__PURE__*/ (function () { - function AuditOperator(durationSelector) { - this.durationSelector = durationSelector; + + +var Observable = /*@__PURE__*/ (function () { + function Observable(subscribe) { + this._isScalar = false; + if (subscribe) { + this._subscribe = subscribe; + } } - AuditOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new AuditSubscriber(subscriber, this.durationSelector)); + Observable.prototype.lift = function (operator) { + var observable = new Observable(); + observable.source = this; + observable.operator = operator; + return observable; }; - return AuditOperator; -}()); -var AuditSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](AuditSubscriber, _super); - function AuditSubscriber(destination, durationSelector) { - var _this = _super.call(this, destination) || this; - _this.durationSelector = durationSelector; - _this.hasValue = false; - return _this; - } - AuditSubscriber.prototype._next = function (value) { - this.value = value; - this.hasValue = true; - if (!this.throttled) { - var duration = void 0; - try { - var durationSelector = this.durationSelector; - duration = durationSelector(value); + Observable.prototype.subscribe = function (observerOrNext, error, complete) { + var operator = this.operator; + var sink = Object(_util_toSubscriber__WEBPACK_IMPORTED_MODULE_1__["toSubscriber"])(observerOrNext, error, complete); + if (operator) { + sink.add(operator.call(sink, this.source)); + } + else { + sink.add(this.source || (_config__WEBPACK_IMPORTED_MODULE_4__["config"].useDeprecatedSynchronousErrorHandling && !sink.syncErrorThrowable) ? + this._subscribe(sink) : + this._trySubscribe(sink)); + } + if (_config__WEBPACK_IMPORTED_MODULE_4__["config"].useDeprecatedSynchronousErrorHandling) { + if (sink.syncErrorThrowable) { + sink.syncErrorThrowable = false; + if (sink.syncErrorThrown) { + throw sink.syncErrorValue; + } } - catch (err) { - return this.destination.error(err); + } + return sink; + }; + Observable.prototype._trySubscribe = function (sink) { + try { + return this._subscribe(sink); + } + catch (err) { + if (_config__WEBPACK_IMPORTED_MODULE_4__["config"].useDeprecatedSynchronousErrorHandling) { + sink.syncErrorThrown = true; + sink.syncErrorValue = err; } - var innerSubscription = Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__["subscribeToResult"])(this, duration); - if (!innerSubscription || innerSubscription.closed) { - this.clearThrottle(); + if (Object(_util_canReportError__WEBPACK_IMPORTED_MODULE_0__["canReportError"])(sink)) { + sink.error(err); } else { - this.add(this.throttled = innerSubscription); + console.warn(err); } } }; - AuditSubscriber.prototype.clearThrottle = function () { - var _a = this, value = _a.value, hasValue = _a.hasValue, throttled = _a.throttled; - if (throttled) { - this.remove(throttled); - this.throttled = null; - throttled.unsubscribe(); + Observable.prototype.forEach = function (next, promiseCtor) { + var _this = this; + promiseCtor = getPromiseCtor(promiseCtor); + return new promiseCtor(function (resolve, reject) { + var subscription; + subscription = _this.subscribe(function (value) { + try { + next(value); + } + catch (err) { + reject(err); + if (subscription) { + subscription.unsubscribe(); + } + } + }, reject, resolve); + }); + }; + Observable.prototype._subscribe = function (subscriber) { + var source = this.source; + return source && source.subscribe(subscriber); + }; + Observable.prototype[_symbol_observable__WEBPACK_IMPORTED_MODULE_2__["observable"]] = function () { + return this; + }; + Observable.prototype.pipe = function () { + var operations = []; + for (var _i = 0; _i < arguments.length; _i++) { + operations[_i] = arguments[_i]; } - if (hasValue) { - this.value = null; - this.hasValue = false; - this.destination.next(value); + if (operations.length === 0) { + return this; } + return Object(_util_pipe__WEBPACK_IMPORTED_MODULE_3__["pipeFromArray"])(operations)(this); }; - AuditSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex) { - this.clearThrottle(); + Observable.prototype.toPromise = function (promiseCtor) { + var _this = this; + promiseCtor = getPromiseCtor(promiseCtor); + return new promiseCtor(function (resolve, reject) { + var value; + _this.subscribe(function (x) { return value = x; }, function (err) { return reject(err); }, function () { return resolve(value); }); + }); }; - AuditSubscriber.prototype.notifyComplete = function () { - this.clearThrottle(); + Observable.create = function (subscribe) { + return new Observable(subscribe); }; - return AuditSubscriber; -}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__["OuterSubscriber"])); -//# sourceMappingURL=audit.js.map + return Observable; +}()); + +function getPromiseCtor(promiseCtor) { + if (!promiseCtor) { + promiseCtor = _config__WEBPACK_IMPORTED_MODULE_4__["config"].Promise || Promise; + } + if (!promiseCtor) { + throw new Error('no Promise impl found'); + } + return promiseCtor; +} +//# sourceMappingURL=Observable.js.map /***/ }), @@ -22465,30 +22327,26 @@ var AuditSubscriber = /*@__PURE__*/ (function (_super) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "OuterSubscriber", function() { return OuterSubscriber; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); -/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ - +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "canReportError", function() { return canReportError; }); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(172); +/** PURE_IMPORTS_START _Subscriber PURE_IMPORTS_END */ -var OuterSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](OuterSubscriber, _super); - function OuterSubscriber() { - return _super !== null && _super.apply(this, arguments) || this; +function canReportError(observer) { + while (observer) { + var _a = observer, closed_1 = _a.closed, destination = _a.destination, isStopped = _a.isStopped; + if (closed_1 || isStopped) { + return false; + } + else if (destination && destination instanceof _Subscriber__WEBPACK_IMPORTED_MODULE_0__["Subscriber"]) { + observer = destination; + } + else { + observer = null; + } } - OuterSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { - this.destination.next(innerValue); - }; - OuterSubscriber.prototype.notifyError = function (error, innerSub) { - this.destination.error(error); - }; - OuterSubscriber.prototype.notifyComplete = function (innerSub) { - this.destination.complete(); - }; - return OuterSubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); - -//# sourceMappingURL=OuterSubscriber.js.map + return true; +} +//# sourceMappingURL=canReportError.js.map /***/ }), @@ -23048,27 +22906,29 @@ var $$rxSubscriber = rxSubscriber; "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "subscribeToResult", function() { return subscribeToResult; }); -/* harmony import */ var _InnerSubscriber__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(183); -/* harmony import */ var _subscribeTo__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(184); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(193); -/** PURE_IMPORTS_START _InnerSubscriber,_subscribeTo,_Observable PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "toSubscriber", function() { return toSubscriber; }); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(172); +/* harmony import */ var _symbol_rxSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(181); +/* harmony import */ var _Observer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(174); +/** PURE_IMPORTS_START _Subscriber,_symbol_rxSubscriber,_Observer PURE_IMPORTS_END */ -function subscribeToResult(outerSubscriber, result, outerValue, outerIndex, destination) { - if (destination === void 0) { - destination = new _InnerSubscriber__WEBPACK_IMPORTED_MODULE_0__["InnerSubscriber"](outerSubscriber, outerValue, outerIndex); - } - if (destination.closed) { - return undefined; +function toSubscriber(nextOrObserver, error, complete) { + if (nextOrObserver) { + if (nextOrObserver instanceof _Subscriber__WEBPACK_IMPORTED_MODULE_0__["Subscriber"]) { + return nextOrObserver; + } + if (nextOrObserver[_symbol_rxSubscriber__WEBPACK_IMPORTED_MODULE_1__["rxSubscriber"]]) { + return nextOrObserver[_symbol_rxSubscriber__WEBPACK_IMPORTED_MODULE_1__["rxSubscriber"]](); + } } - if (result instanceof _Observable__WEBPACK_IMPORTED_MODULE_2__["Observable"]) { - return result.subscribe(destination); + if (!nextOrObserver && !error && !complete) { + return new _Subscriber__WEBPACK_IMPORTED_MODULE_0__["Subscriber"](_Observer__WEBPACK_IMPORTED_MODULE_2__["empty"]); } - return Object(_subscribeTo__WEBPACK_IMPORTED_MODULE_1__["subscribeTo"])(result)(destination); + return new _Subscriber__WEBPACK_IMPORTED_MODULE_0__["Subscriber"](nextOrObserver, error, complete); } -//# sourceMappingURL=subscribeToResult.js.map +//# sourceMappingURL=toSubscriber.js.map /***/ }), @@ -23077,37 +22937,10 @@ function subscribeToResult(outerSubscriber, result, outerValue, outerIndex, dest "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "InnerSubscriber", function() { return InnerSubscriber; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); -/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ - - -var InnerSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](InnerSubscriber, _super); - function InnerSubscriber(parent, outerValue, outerIndex) { - var _this = _super.call(this) || this; - _this.parent = parent; - _this.outerValue = outerValue; - _this.outerIndex = outerIndex; - _this.index = 0; - return _this; - } - InnerSubscriber.prototype._next = function (value) { - this.parent.notifyNext(this.outerValue, value, this.outerIndex, this.index++, this); - }; - InnerSubscriber.prototype._error = function (error) { - this.parent.notifyError(error, this); - this.unsubscribe(); - }; - InnerSubscriber.prototype._complete = function () { - this.parent.notifyComplete(this); - this.unsubscribe(); - }; - return InnerSubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); - -//# sourceMappingURL=InnerSubscriber.js.map +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "observable", function() { return observable; }); +/** PURE_IMPORTS_START PURE_IMPORTS_END */ +var observable = /*@__PURE__*/ (function () { return typeof Symbol === 'function' && Symbol.observable || '@@observable'; })(); +//# sourceMappingURL=observable.js.map /***/ }), @@ -23116,47 +22949,30 @@ var InnerSubscriber = /*@__PURE__*/ (function (_super) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "subscribeTo", function() { return subscribeTo; }); -/* harmony import */ var _subscribeToArray__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(185); -/* harmony import */ var _subscribeToPromise__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(186); -/* harmony import */ var _subscribeToIterable__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(187); -/* harmony import */ var _subscribeToObservable__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(189); -/* harmony import */ var _isArrayLike__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(191); -/* harmony import */ var _isPromise__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(192); -/* harmony import */ var _isObject__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(179); -/* harmony import */ var _symbol_iterator__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(188); -/* harmony import */ var _symbol_observable__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(190); -/** PURE_IMPORTS_START _subscribeToArray,_subscribeToPromise,_subscribeToIterable,_subscribeToObservable,_isArrayLike,_isPromise,_isObject,_symbol_iterator,_symbol_observable PURE_IMPORTS_END */ - - - - - - - - +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "pipe", function() { return pipe; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "pipeFromArray", function() { return pipeFromArray; }); +/* harmony import */ var _noop__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(185); +/** PURE_IMPORTS_START _noop PURE_IMPORTS_END */ -var subscribeTo = function (result) { - if (!!result && typeof result[_symbol_observable__WEBPACK_IMPORTED_MODULE_8__["observable"]] === 'function') { - return Object(_subscribeToObservable__WEBPACK_IMPORTED_MODULE_3__["subscribeToObservable"])(result); - } - else if (Object(_isArrayLike__WEBPACK_IMPORTED_MODULE_4__["isArrayLike"])(result)) { - return Object(_subscribeToArray__WEBPACK_IMPORTED_MODULE_0__["subscribeToArray"])(result); - } - else if (Object(_isPromise__WEBPACK_IMPORTED_MODULE_5__["isPromise"])(result)) { - return Object(_subscribeToPromise__WEBPACK_IMPORTED_MODULE_1__["subscribeToPromise"])(result); +function pipe() { + var fns = []; + for (var _i = 0; _i < arguments.length; _i++) { + fns[_i] = arguments[_i]; } - else if (!!result && typeof result[_symbol_iterator__WEBPACK_IMPORTED_MODULE_7__["iterator"]] === 'function') { - return Object(_subscribeToIterable__WEBPACK_IMPORTED_MODULE_2__["subscribeToIterable"])(result); + return pipeFromArray(fns); +} +function pipeFromArray(fns) { + if (!fns) { + return _noop__WEBPACK_IMPORTED_MODULE_0__["noop"]; } - else { - var value = Object(_isObject__WEBPACK_IMPORTED_MODULE_6__["isObject"])(result) ? 'an invalid object' : "'" + result + "'"; - var msg = "You provided " + value + " where a stream was expected." - + ' You can provide an Observable, Promise, Array, or Iterable.'; - throw new TypeError(msg); + if (fns.length === 1) { + return fns[0]; } -}; -//# sourceMappingURL=subscribeTo.js.map + return function piped(input) { + return fns.reduce(function (prev, fn) { return fn(prev); }, input); + }; +} +//# sourceMappingURL=pipe.js.map /***/ }), @@ -23165,17 +22981,10 @@ var subscribeTo = function (result) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "subscribeToArray", function() { return subscribeToArray; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "noop", function() { return noop; }); /** PURE_IMPORTS_START PURE_IMPORTS_END */ -var subscribeToArray = function (array) { - return function (subscriber) { - for (var i = 0, len = array.length; i < len && !subscriber.closed; i++) { - subscriber.next(array[i]); - } - subscriber.complete(); - }; -}; -//# sourceMappingURL=subscribeToArray.js.map +function noop() { } +//# sourceMappingURL=noop.js.map /***/ }), @@ -23184,2009 +22993,2195 @@ var subscribeToArray = function (array) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "subscribeToPromise", function() { return subscribeToPromise; }); -/* harmony import */ var _hostReportError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(176); -/** PURE_IMPORTS_START _hostReportError PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ConnectableObservable", function() { return ConnectableObservable; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "connectableObservableDescriptor", function() { return connectableObservableDescriptor; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(187); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(170); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(172); +/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(177); +/* harmony import */ var _operators_refCount__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(190); +/** PURE_IMPORTS_START tslib,_Subject,_Observable,_Subscriber,_Subscription,_operators_refCount PURE_IMPORTS_END */ -var subscribeToPromise = function (promise) { - return function (subscriber) { - promise.then(function (value) { - if (!subscriber.closed) { - subscriber.next(value); - subscriber.complete(); - } - }, function (err) { return subscriber.error(err); }) - .then(null, _hostReportError__WEBPACK_IMPORTED_MODULE_0__["hostReportError"]); - return subscriber; - }; -}; -//# sourceMappingURL=subscribeToPromise.js.map -/***/ }), -/* 187 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "subscribeToIterable", function() { return subscribeToIterable; }); -/* harmony import */ var _symbol_iterator__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(188); -/** PURE_IMPORTS_START _symbol_iterator PURE_IMPORTS_END */ -var subscribeToIterable = function (iterable) { - return function (subscriber) { - var iterator = iterable[_symbol_iterator__WEBPACK_IMPORTED_MODULE_0__["iterator"]](); - do { - var item = iterator.next(); - if (item.done) { - subscriber.complete(); - break; - } - subscriber.next(item.value); - if (subscriber.closed) { - break; + +var ConnectableObservable = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](ConnectableObservable, _super); + function ConnectableObservable(source, subjectFactory) { + var _this = _super.call(this) || this; + _this.source = source; + _this.subjectFactory = subjectFactory; + _this._refCount = 0; + _this._isComplete = false; + return _this; + } + ConnectableObservable.prototype._subscribe = function (subscriber) { + return this.getSubject().subscribe(subscriber); + }; + ConnectableObservable.prototype.getSubject = function () { + var subject = this._subject; + if (!subject || subject.isStopped) { + this._subject = this.subjectFactory(); + } + return this._subject; + }; + ConnectableObservable.prototype.connect = function () { + var connection = this._connection; + if (!connection) { + this._isComplete = false; + connection = this._connection = new _Subscription__WEBPACK_IMPORTED_MODULE_4__["Subscription"](); + connection.add(this.source + .subscribe(new ConnectableSubscriber(this.getSubject(), this))); + if (connection.closed) { + this._connection = null; + connection = _Subscription__WEBPACK_IMPORTED_MODULE_4__["Subscription"].EMPTY; } - } while (true); - if (typeof iterator.return === 'function') { - subscriber.add(function () { - if (iterator.return) { - iterator.return(); - } - }); } - return subscriber; + return connection; }; -}; -//# sourceMappingURL=subscribeToIterable.js.map - - -/***/ }), -/* 188 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { + ConnectableObservable.prototype.refCount = function () { + return Object(_operators_refCount__WEBPACK_IMPORTED_MODULE_5__["refCount"])()(this); + }; + return ConnectableObservable; +}(_Observable__WEBPACK_IMPORTED_MODULE_2__["Observable"])); -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "getSymbolIterator", function() { return getSymbolIterator; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "iterator", function() { return iterator; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "$$iterator", function() { return $$iterator; }); -/** PURE_IMPORTS_START PURE_IMPORTS_END */ -function getSymbolIterator() { - if (typeof Symbol !== 'function' || !Symbol.iterator) { - return '@@iterator'; +var connectableObservableDescriptor = /*@__PURE__*/ (function () { + var connectableProto = ConnectableObservable.prototype; + return { + operator: { value: null }, + _refCount: { value: 0, writable: true }, + _subject: { value: null, writable: true }, + _connection: { value: null, writable: true }, + _subscribe: { value: connectableProto._subscribe }, + _isComplete: { value: connectableProto._isComplete, writable: true }, + getSubject: { value: connectableProto.getSubject }, + connect: { value: connectableProto.connect }, + refCount: { value: connectableProto.refCount } + }; +})(); +var ConnectableSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](ConnectableSubscriber, _super); + function ConnectableSubscriber(destination, connectable) { + var _this = _super.call(this, destination) || this; + _this.connectable = connectable; + return _this; } - return Symbol.iterator; -} -var iterator = /*@__PURE__*/ getSymbolIterator(); -var $$iterator = iterator; -//# sourceMappingURL=iterator.js.map - - -/***/ }), -/* 189 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "subscribeToObservable", function() { return subscribeToObservable; }); -/* harmony import */ var _symbol_observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(190); -/** PURE_IMPORTS_START _symbol_observable PURE_IMPORTS_END */ - -var subscribeToObservable = function (obj) { - return function (subscriber) { - var obs = obj[_symbol_observable__WEBPACK_IMPORTED_MODULE_0__["observable"]](); - if (typeof obs.subscribe !== 'function') { - throw new TypeError('Provided object does not correctly implement Symbol.observable'); + ConnectableSubscriber.prototype._error = function (err) { + this._unsubscribe(); + _super.prototype._error.call(this, err); + }; + ConnectableSubscriber.prototype._complete = function () { + this.connectable._isComplete = true; + this._unsubscribe(); + _super.prototype._complete.call(this); + }; + ConnectableSubscriber.prototype._unsubscribe = function () { + var connectable = this.connectable; + if (connectable) { + this.connectable = null; + var connection = connectable._connection; + connectable._refCount = 0; + connectable._subject = null; + connectable._connection = null; + if (connection) { + connection.unsubscribe(); + } } - else { - return obs.subscribe(subscriber); + }; + return ConnectableSubscriber; +}(_Subject__WEBPACK_IMPORTED_MODULE_1__["SubjectSubscriber"])); +var RefCountOperator = /*@__PURE__*/ (function () { + function RefCountOperator(connectable) { + this.connectable = connectable; + } + RefCountOperator.prototype.call = function (subscriber, source) { + var connectable = this.connectable; + connectable._refCount++; + var refCounter = new RefCountSubscriber(subscriber, connectable); + var subscription = source.subscribe(refCounter); + if (!refCounter.closed) { + refCounter.connection = connectable.connect(); } + return subscription; }; -}; -//# sourceMappingURL=subscribeToObservable.js.map - - -/***/ }), -/* 190 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "observable", function() { return observable; }); -/** PURE_IMPORTS_START PURE_IMPORTS_END */ -var observable = /*@__PURE__*/ (function () { return typeof Symbol === 'function' && Symbol.observable || '@@observable'; })(); -//# sourceMappingURL=observable.js.map - - -/***/ }), -/* 191 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isArrayLike", function() { return isArrayLike; }); -/** PURE_IMPORTS_START PURE_IMPORTS_END */ -var isArrayLike = (function (x) { return x && typeof x.length === 'number' && typeof x !== 'function'; }); -//# sourceMappingURL=isArrayLike.js.map + return RefCountOperator; +}()); +var RefCountSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](RefCountSubscriber, _super); + function RefCountSubscriber(destination, connectable) { + var _this = _super.call(this, destination) || this; + _this.connectable = connectable; + return _this; + } + RefCountSubscriber.prototype._unsubscribe = function () { + var connectable = this.connectable; + if (!connectable) { + this.connection = null; + return; + } + this.connectable = null; + var refCount = connectable._refCount; + if (refCount <= 0) { + this.connection = null; + return; + } + connectable._refCount = refCount - 1; + if (refCount > 1) { + this.connection = null; + return; + } + var connection = this.connection; + var sharedConnection = connectable._connection; + this.connection = null; + if (sharedConnection && (!connection || sharedConnection === connection)) { + sharedConnection.unsubscribe(); + } + }; + return RefCountSubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_3__["Subscriber"])); +//# sourceMappingURL=ConnectableObservable.js.map /***/ }), -/* 192 */ +/* 187 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isPromise", function() { return isPromise; }); -/** PURE_IMPORTS_START PURE_IMPORTS_END */ -function isPromise(value) { - return !!value && typeof value.subscribe !== 'function' && typeof value.then === 'function'; -} -//# sourceMappingURL=isPromise.js.map +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SubjectSubscriber", function() { return SubjectSubscriber; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Subject", function() { return Subject; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "AnonymousSubject", function() { return AnonymousSubject; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(170); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(172); +/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(177); +/* harmony import */ var _util_ObjectUnsubscribedError__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(188); +/* harmony import */ var _SubjectSubscription__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(189); +/* harmony import */ var _internal_symbol_rxSubscriber__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(181); +/** PURE_IMPORTS_START tslib,_Observable,_Subscriber,_Subscription,_util_ObjectUnsubscribedError,_SubjectSubscription,_internal_symbol_rxSubscriber PURE_IMPORTS_END */ -/***/ }), -/* 193 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Observable", function() { return Observable; }); -/* harmony import */ var _util_canReportError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(194); -/* harmony import */ var _util_toSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(195); -/* harmony import */ var _symbol_observable__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(190); -/* harmony import */ var _util_pipe__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(196); -/* harmony import */ var _config__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(175); -/** PURE_IMPORTS_START _util_canReportError,_util_toSubscriber,_symbol_observable,_util_pipe,_config PURE_IMPORTS_END */ +var SubjectSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](SubjectSubscriber, _super); + function SubjectSubscriber(destination) { + var _this = _super.call(this, destination) || this; + _this.destination = destination; + return _this; + } + return SubjectSubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_2__["Subscriber"])); -var Observable = /*@__PURE__*/ (function () { - function Observable(subscribe) { - this._isScalar = false; - if (subscribe) { - this._subscribe = subscribe; - } +var Subject = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](Subject, _super); + function Subject() { + var _this = _super.call(this) || this; + _this.observers = []; + _this.closed = false; + _this.isStopped = false; + _this.hasError = false; + _this.thrownError = null; + return _this; } - Observable.prototype.lift = function (operator) { - var observable = new Observable(); - observable.source = this; - observable.operator = operator; - return observable; + Subject.prototype[_internal_symbol_rxSubscriber__WEBPACK_IMPORTED_MODULE_6__["rxSubscriber"]] = function () { + return new SubjectSubscriber(this); }; - Observable.prototype.subscribe = function (observerOrNext, error, complete) { - var operator = this.operator; - var sink = Object(_util_toSubscriber__WEBPACK_IMPORTED_MODULE_1__["toSubscriber"])(observerOrNext, error, complete); - if (operator) { - sink.add(operator.call(sink, this.source)); - } - else { - sink.add(this.source || (_config__WEBPACK_IMPORTED_MODULE_4__["config"].useDeprecatedSynchronousErrorHandling && !sink.syncErrorThrowable) ? - this._subscribe(sink) : - this._trySubscribe(sink)); + Subject.prototype.lift = function (operator) { + var subject = new AnonymousSubject(this, this); + subject.operator = operator; + return subject; + }; + Subject.prototype.next = function (value) { + if (this.closed) { + throw new _util_ObjectUnsubscribedError__WEBPACK_IMPORTED_MODULE_4__["ObjectUnsubscribedError"](); } - if (_config__WEBPACK_IMPORTED_MODULE_4__["config"].useDeprecatedSynchronousErrorHandling) { - if (sink.syncErrorThrowable) { - sink.syncErrorThrowable = false; - if (sink.syncErrorThrown) { - throw sink.syncErrorValue; - } + if (!this.isStopped) { + var observers = this.observers; + var len = observers.length; + var copy = observers.slice(); + for (var i = 0; i < len; i++) { + copy[i].next(value); } } - return sink; }; - Observable.prototype._trySubscribe = function (sink) { - try { - return this._subscribe(sink); + Subject.prototype.error = function (err) { + if (this.closed) { + throw new _util_ObjectUnsubscribedError__WEBPACK_IMPORTED_MODULE_4__["ObjectUnsubscribedError"](); } - catch (err) { - if (_config__WEBPACK_IMPORTED_MODULE_4__["config"].useDeprecatedSynchronousErrorHandling) { - sink.syncErrorThrown = true; - sink.syncErrorValue = err; - } - if (Object(_util_canReportError__WEBPACK_IMPORTED_MODULE_0__["canReportError"])(sink)) { - sink.error(err); - } - else { - console.warn(err); - } + this.hasError = true; + this.thrownError = err; + this.isStopped = true; + var observers = this.observers; + var len = observers.length; + var copy = observers.slice(); + for (var i = 0; i < len; i++) { + copy[i].error(err); } + this.observers.length = 0; }; - Observable.prototype.forEach = function (next, promiseCtor) { - var _this = this; - promiseCtor = getPromiseCtor(promiseCtor); - return new promiseCtor(function (resolve, reject) { - var subscription; - subscription = _this.subscribe(function (value) { - try { - next(value); - } - catch (err) { - reject(err); - if (subscription) { - subscription.unsubscribe(); - } - } - }, reject, resolve); - }); - }; - Observable.prototype._subscribe = function (subscriber) { - var source = this.source; - return source && source.subscribe(subscriber); - }; - Observable.prototype[_symbol_observable__WEBPACK_IMPORTED_MODULE_2__["observable"]] = function () { - return this; - }; - Observable.prototype.pipe = function () { - var operations = []; - for (var _i = 0; _i < arguments.length; _i++) { - operations[_i] = arguments[_i]; + Subject.prototype.complete = function () { + if (this.closed) { + throw new _util_ObjectUnsubscribedError__WEBPACK_IMPORTED_MODULE_4__["ObjectUnsubscribedError"](); } - if (operations.length === 0) { - return this; + this.isStopped = true; + var observers = this.observers; + var len = observers.length; + var copy = observers.slice(); + for (var i = 0; i < len; i++) { + copy[i].complete(); } - return Object(_util_pipe__WEBPACK_IMPORTED_MODULE_3__["pipeFromArray"])(operations)(this); + this.observers.length = 0; }; - Observable.prototype.toPromise = function (promiseCtor) { - var _this = this; - promiseCtor = getPromiseCtor(promiseCtor); - return new promiseCtor(function (resolve, reject) { - var value; - _this.subscribe(function (x) { return value = x; }, function (err) { return reject(err); }, function () { return resolve(value); }); - }); + Subject.prototype.unsubscribe = function () { + this.isStopped = true; + this.closed = true; + this.observers = null; }; - Observable.create = function (subscribe) { - return new Observable(subscribe); + Subject.prototype._trySubscribe = function (subscriber) { + if (this.closed) { + throw new _util_ObjectUnsubscribedError__WEBPACK_IMPORTED_MODULE_4__["ObjectUnsubscribedError"](); + } + else { + return _super.prototype._trySubscribe.call(this, subscriber); + } }; - return Observable; -}()); - -function getPromiseCtor(promiseCtor) { - if (!promiseCtor) { - promiseCtor = _config__WEBPACK_IMPORTED_MODULE_4__["config"].Promise || Promise; - } - if (!promiseCtor) { - throw new Error('no Promise impl found'); - } - return promiseCtor; -} -//# sourceMappingURL=Observable.js.map - - -/***/ }), -/* 194 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "canReportError", function() { return canReportError; }); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(172); -/** PURE_IMPORTS_START _Subscriber PURE_IMPORTS_END */ - -function canReportError(observer) { - while (observer) { - var _a = observer, closed_1 = _a.closed, destination = _a.destination, isStopped = _a.isStopped; - if (closed_1 || isStopped) { - return false; + Subject.prototype._subscribe = function (subscriber) { + if (this.closed) { + throw new _util_ObjectUnsubscribedError__WEBPACK_IMPORTED_MODULE_4__["ObjectUnsubscribedError"](); } - else if (destination && destination instanceof _Subscriber__WEBPACK_IMPORTED_MODULE_0__["Subscriber"]) { - observer = destination; + else if (this.hasError) { + subscriber.error(this.thrownError); + return _Subscription__WEBPACK_IMPORTED_MODULE_3__["Subscription"].EMPTY; + } + else if (this.isStopped) { + subscriber.complete(); + return _Subscription__WEBPACK_IMPORTED_MODULE_3__["Subscription"].EMPTY; } else { - observer = null; + this.observers.push(subscriber); + return new _SubjectSubscription__WEBPACK_IMPORTED_MODULE_5__["SubjectSubscription"](this, subscriber); } - } - return true; -} -//# sourceMappingURL=canReportError.js.map - - -/***/ }), -/* 195 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "toSubscriber", function() { return toSubscriber; }); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(172); -/* harmony import */ var _symbol_rxSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(181); -/* harmony import */ var _Observer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(174); -/** PURE_IMPORTS_START _Subscriber,_symbol_rxSubscriber,_Observer PURE_IMPORTS_END */ - - + }; + Subject.prototype.asObservable = function () { + var observable = new _Observable__WEBPACK_IMPORTED_MODULE_1__["Observable"](); + observable.source = this; + return observable; + }; + Subject.create = function (destination, source) { + return new AnonymousSubject(destination, source); + }; + return Subject; +}(_Observable__WEBPACK_IMPORTED_MODULE_1__["Observable"])); -function toSubscriber(nextOrObserver, error, complete) { - if (nextOrObserver) { - if (nextOrObserver instanceof _Subscriber__WEBPACK_IMPORTED_MODULE_0__["Subscriber"]) { - return nextOrObserver; +var AnonymousSubject = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](AnonymousSubject, _super); + function AnonymousSubject(destination, source) { + var _this = _super.call(this) || this; + _this.destination = destination; + _this.source = source; + return _this; + } + AnonymousSubject.prototype.next = function (value) { + var destination = this.destination; + if (destination && destination.next) { + destination.next(value); } - if (nextOrObserver[_symbol_rxSubscriber__WEBPACK_IMPORTED_MODULE_1__["rxSubscriber"]]) { - return nextOrObserver[_symbol_rxSubscriber__WEBPACK_IMPORTED_MODULE_1__["rxSubscriber"]](); + }; + AnonymousSubject.prototype.error = function (err) { + var destination = this.destination; + if (destination && destination.error) { + this.destination.error(err); } - } - if (!nextOrObserver && !error && !complete) { - return new _Subscriber__WEBPACK_IMPORTED_MODULE_0__["Subscriber"](_Observer__WEBPACK_IMPORTED_MODULE_2__["empty"]); - } - return new _Subscriber__WEBPACK_IMPORTED_MODULE_0__["Subscriber"](nextOrObserver, error, complete); -} -//# sourceMappingURL=toSubscriber.js.map - - -/***/ }), -/* 196 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "pipe", function() { return pipe; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "pipeFromArray", function() { return pipeFromArray; }); -/* harmony import */ var _noop__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(197); -/** PURE_IMPORTS_START _noop PURE_IMPORTS_END */ - -function pipe() { - var fns = []; - for (var _i = 0; _i < arguments.length; _i++) { - fns[_i] = arguments[_i]; - } - return pipeFromArray(fns); -} -function pipeFromArray(fns) { - if (!fns) { - return _noop__WEBPACK_IMPORTED_MODULE_0__["noop"]; - } - if (fns.length === 1) { - return fns[0]; - } - return function piped(input) { - return fns.reduce(function (prev, fn) { return fn(prev); }, input); }; -} -//# sourceMappingURL=pipe.js.map - - -/***/ }), -/* 197 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { + AnonymousSubject.prototype.complete = function () { + var destination = this.destination; + if (destination && destination.complete) { + this.destination.complete(); + } + }; + AnonymousSubject.prototype._subscribe = function (subscriber) { + var source = this.source; + if (source) { + return this.source.subscribe(subscriber); + } + else { + return _Subscription__WEBPACK_IMPORTED_MODULE_3__["Subscription"].EMPTY; + } + }; + return AnonymousSubject; +}(Subject)); -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "noop", function() { return noop; }); -/** PURE_IMPORTS_START PURE_IMPORTS_END */ -function noop() { } -//# sourceMappingURL=noop.js.map +//# sourceMappingURL=Subject.js.map /***/ }), -/* 198 */ +/* 188 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "auditTime", function() { return auditTime; }); -/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(199); -/* harmony import */ var _audit__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(170); -/* harmony import */ var _observable_timer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(204); -/** PURE_IMPORTS_START _scheduler_async,_audit,_observable_timer PURE_IMPORTS_END */ - - - -function auditTime(duration, scheduler) { - if (scheduler === void 0) { - scheduler = _scheduler_async__WEBPACK_IMPORTED_MODULE_0__["async"]; +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ObjectUnsubscribedError", function() { return ObjectUnsubscribedError; }); +/** PURE_IMPORTS_START PURE_IMPORTS_END */ +var ObjectUnsubscribedErrorImpl = /*@__PURE__*/ (function () { + function ObjectUnsubscribedErrorImpl() { + Error.call(this); + this.message = 'object unsubscribed'; + this.name = 'ObjectUnsubscribedError'; + return this; } - return Object(_audit__WEBPACK_IMPORTED_MODULE_1__["audit"])(function () { return Object(_observable_timer__WEBPACK_IMPORTED_MODULE_2__["timer"])(duration, scheduler); }); -} -//# sourceMappingURL=auditTime.js.map - - -/***/ }), -/* 199 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "async", function() { return async; }); -/* harmony import */ var _AsyncAction__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(200); -/* harmony import */ var _AsyncScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(202); -/** PURE_IMPORTS_START _AsyncAction,_AsyncScheduler PURE_IMPORTS_END */ - - -var async = /*@__PURE__*/ new _AsyncScheduler__WEBPACK_IMPORTED_MODULE_1__["AsyncScheduler"](_AsyncAction__WEBPACK_IMPORTED_MODULE_0__["AsyncAction"]); -//# sourceMappingURL=async.js.map + ObjectUnsubscribedErrorImpl.prototype = /*@__PURE__*/ Object.create(Error.prototype); + return ObjectUnsubscribedErrorImpl; +})(); +var ObjectUnsubscribedError = ObjectUnsubscribedErrorImpl; +//# sourceMappingURL=ObjectUnsubscribedError.js.map /***/ }), -/* 200 */ +/* 189 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "AsyncAction", function() { return AsyncAction; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SubjectSubscription", function() { return SubjectSubscription; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Action__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(201); -/** PURE_IMPORTS_START tslib,_Action PURE_IMPORTS_END */ +/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(177); +/** PURE_IMPORTS_START tslib,_Subscription PURE_IMPORTS_END */ -var AsyncAction = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](AsyncAction, _super); - function AsyncAction(scheduler, work) { - var _this = _super.call(this, scheduler, work) || this; - _this.scheduler = scheduler; - _this.work = work; - _this.pending = false; +var SubjectSubscription = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](SubjectSubscription, _super); + function SubjectSubscription(subject, subscriber) { + var _this = _super.call(this) || this; + _this.subject = subject; + _this.subscriber = subscriber; + _this.closed = false; return _this; } - AsyncAction.prototype.schedule = function (state, delay) { - if (delay === void 0) { - delay = 0; - } - if (this.closed) { - return this; - } - this.state = state; - var id = this.id; - var scheduler = this.scheduler; - if (id != null) { - this.id = this.recycleAsyncId(scheduler, id, delay); - } - this.pending = true; - this.delay = delay; - this.id = this.id || this.requestAsyncId(scheduler, this.id, delay); - return this; - }; - AsyncAction.prototype.requestAsyncId = function (scheduler, id, delay) { - if (delay === void 0) { - delay = 0; - } - return setInterval(scheduler.flush.bind(scheduler, this), delay); - }; - AsyncAction.prototype.recycleAsyncId = function (scheduler, id, delay) { - if (delay === void 0) { - delay = 0; - } - if (delay !== null && this.delay === delay && this.pending === false) { - return id; - } - clearInterval(id); - return undefined; - }; - AsyncAction.prototype.execute = function (state, delay) { + SubjectSubscription.prototype.unsubscribe = function () { if (this.closed) { - return new Error('executing a cancelled action'); - } - this.pending = false; - var error = this._execute(state, delay); - if (error) { - return error; - } - else if (this.pending === false && this.id != null) { - this.id = this.recycleAsyncId(this.scheduler, this.id, null); - } - }; - AsyncAction.prototype._execute = function (state, delay) { - var errored = false; - var errorValue = undefined; - try { - this.work(state); - } - catch (e) { - errored = true; - errorValue = !!e && e || new Error(e); - } - if (errored) { - this.unsubscribe(); - return errorValue; + return; } - }; - AsyncAction.prototype._unsubscribe = function () { - var id = this.id; - var scheduler = this.scheduler; - var actions = scheduler.actions; - var index = actions.indexOf(this); - this.work = null; - this.state = null; - this.pending = false; - this.scheduler = null; - if (index !== -1) { - actions.splice(index, 1); + this.closed = true; + var subject = this.subject; + var observers = subject.observers; + this.subject = null; + if (!observers || observers.length === 0 || subject.isStopped || subject.closed) { + return; } - if (id != null) { - this.id = this.recycleAsyncId(scheduler, id, null); + var subscriberIndex = observers.indexOf(this.subscriber); + if (subscriberIndex !== -1) { + observers.splice(subscriberIndex, 1); } - this.delay = null; }; - return AsyncAction; -}(_Action__WEBPACK_IMPORTED_MODULE_1__["Action"])); + return SubjectSubscription; +}(_Subscription__WEBPACK_IMPORTED_MODULE_1__["Subscription"])); -//# sourceMappingURL=AsyncAction.js.map +//# sourceMappingURL=SubjectSubscription.js.map /***/ }), -/* 201 */ +/* 190 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Action", function() { return Action; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "refCount", function() { return refCount; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(177); -/** PURE_IMPORTS_START tslib,_Subscription PURE_IMPORTS_END */ +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); +/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ -var Action = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](Action, _super); - function Action(scheduler, work) { - return _super.call(this) || this; +function refCount() { + return function refCountOperatorFunction(source) { + return source.lift(new RefCountOperator(source)); + }; +} +var RefCountOperator = /*@__PURE__*/ (function () { + function RefCountOperator(connectable) { + this.connectable = connectable; } - Action.prototype.schedule = function (state, delay) { - if (delay === void 0) { - delay = 0; + RefCountOperator.prototype.call = function (subscriber, source) { + var connectable = this.connectable; + connectable._refCount++; + var refCounter = new RefCountSubscriber(subscriber, connectable); + var subscription = source.subscribe(refCounter); + if (!refCounter.closed) { + refCounter.connection = connectable.connect(); } - return this; + return subscription; }; - return Action; -}(_Subscription__WEBPACK_IMPORTED_MODULE_1__["Subscription"])); - -//# sourceMappingURL=Action.js.map - - -/***/ }), -/* 202 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "AsyncScheduler", function() { return AsyncScheduler; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Scheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(203); -/** PURE_IMPORTS_START tslib,_Scheduler PURE_IMPORTS_END */ - - -var AsyncScheduler = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](AsyncScheduler, _super); - function AsyncScheduler(SchedulerAction, now) { - if (now === void 0) { - now = _Scheduler__WEBPACK_IMPORTED_MODULE_1__["Scheduler"].now; - } - var _this = _super.call(this, SchedulerAction, function () { - if (AsyncScheduler.delegate && AsyncScheduler.delegate !== _this) { - return AsyncScheduler.delegate.now(); - } - else { - return now(); - } - }) || this; - _this.actions = []; - _this.active = false; - _this.scheduled = undefined; + return RefCountOperator; +}()); +var RefCountSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](RefCountSubscriber, _super); + function RefCountSubscriber(destination, connectable) { + var _this = _super.call(this, destination) || this; + _this.connectable = connectable; return _this; } - AsyncScheduler.prototype.schedule = function (work, delay, state) { - if (delay === void 0) { - delay = 0; - } - if (AsyncScheduler.delegate && AsyncScheduler.delegate !== this) { - return AsyncScheduler.delegate.schedule(work, delay, state); + RefCountSubscriber.prototype._unsubscribe = function () { + var connectable = this.connectable; + if (!connectable) { + this.connection = null; + return; } - else { - return _super.prototype.schedule.call(this, work, delay, state); + this.connectable = null; + var refCount = connectable._refCount; + if (refCount <= 0) { + this.connection = null; + return; } - }; - AsyncScheduler.prototype.flush = function (action) { - var actions = this.actions; - if (this.active) { - actions.push(action); + connectable._refCount = refCount - 1; + if (refCount > 1) { + this.connection = null; return; } - var error; - this.active = true; - do { - if (error = action.execute(action.state, action.delay)) { - break; - } - } while (action = actions.shift()); - this.active = false; - if (error) { - while (action = actions.shift()) { - action.unsubscribe(); - } - throw error; + var connection = this.connection; + var sharedConnection = connectable._connection; + this.connection = null; + if (sharedConnection && (!connection || sharedConnection === connection)) { + sharedConnection.unsubscribe(); } }; - return AsyncScheduler; -}(_Scheduler__WEBPACK_IMPORTED_MODULE_1__["Scheduler"])); - -//# sourceMappingURL=AsyncScheduler.js.map + return RefCountSubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); +//# sourceMappingURL=refCount.js.map /***/ }), -/* 203 */ +/* 191 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Scheduler", function() { return Scheduler; }); -var Scheduler = /*@__PURE__*/ (function () { - function Scheduler(SchedulerAction, now) { - if (now === void 0) { - now = Scheduler.now; - } - this.SchedulerAction = SchedulerAction; - this.now = now; - } - Scheduler.prototype.schedule = function (work, delay, state) { - if (delay === void 0) { - delay = 0; - } - return new this.SchedulerAction(this, work).schedule(state, delay); - }; - Scheduler.now = function () { return Date.now(); }; - return Scheduler; -}()); - -//# sourceMappingURL=Scheduler.js.map - - -/***/ }), -/* 204 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "groupBy", function() { return groupBy; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "GroupedObservable", function() { return GroupedObservable; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); +/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(177); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(170); +/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(187); +/** PURE_IMPORTS_START tslib,_Subscriber,_Subscription,_Observable,_Subject PURE_IMPORTS_END */ -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timer", function() { return timer; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(193); -/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(199); -/* harmony import */ var _util_isNumeric__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(205); -/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(206); -/** PURE_IMPORTS_START _Observable,_scheduler_async,_util_isNumeric,_util_isScheduler PURE_IMPORTS_END */ -function timer(dueTime, periodOrScheduler, scheduler) { - if (dueTime === void 0) { - dueTime = 0; - } - var period = -1; - if (Object(_util_isNumeric__WEBPACK_IMPORTED_MODULE_2__["isNumeric"])(periodOrScheduler)) { - period = Number(periodOrScheduler) < 1 && 1 || Number(periodOrScheduler); +function groupBy(keySelector, elementSelector, durationSelector, subjectSelector) { + return function (source) { + return source.lift(new GroupByOperator(keySelector, elementSelector, durationSelector, subjectSelector)); + }; +} +var GroupByOperator = /*@__PURE__*/ (function () { + function GroupByOperator(keySelector, elementSelector, durationSelector, subjectSelector) { + this.keySelector = keySelector; + this.elementSelector = elementSelector; + this.durationSelector = durationSelector; + this.subjectSelector = subjectSelector; } - else if (Object(_util_isScheduler__WEBPACK_IMPORTED_MODULE_3__["isScheduler"])(periodOrScheduler)) { - scheduler = periodOrScheduler; + GroupByOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new GroupBySubscriber(subscriber, this.keySelector, this.elementSelector, this.durationSelector, this.subjectSelector)); + }; + return GroupByOperator; +}()); +var GroupBySubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](GroupBySubscriber, _super); + function GroupBySubscriber(destination, keySelector, elementSelector, durationSelector, subjectSelector) { + var _this = _super.call(this, destination) || this; + _this.keySelector = keySelector; + _this.elementSelector = elementSelector; + _this.durationSelector = durationSelector; + _this.subjectSelector = subjectSelector; + _this.groups = null; + _this.attemptedToUnsubscribe = false; + _this.count = 0; + return _this; } - if (!Object(_util_isScheduler__WEBPACK_IMPORTED_MODULE_3__["isScheduler"])(scheduler)) { - scheduler = _scheduler_async__WEBPACK_IMPORTED_MODULE_1__["async"]; + GroupBySubscriber.prototype._next = function (value) { + var key; + try { + key = this.keySelector(value); + } + catch (err) { + this.error(err); + return; + } + this._group(value, key); + }; + GroupBySubscriber.prototype._group = function (value, key) { + var groups = this.groups; + if (!groups) { + groups = this.groups = new Map(); + } + var group = groups.get(key); + var element; + if (this.elementSelector) { + try { + element = this.elementSelector(value); + } + catch (err) { + this.error(err); + } + } + else { + element = value; + } + if (!group) { + group = (this.subjectSelector ? this.subjectSelector() : new _Subject__WEBPACK_IMPORTED_MODULE_4__["Subject"]()); + groups.set(key, group); + var groupedObservable = new GroupedObservable(key, group, this); + this.destination.next(groupedObservable); + if (this.durationSelector) { + var duration = void 0; + try { + duration = this.durationSelector(new GroupedObservable(key, group)); + } + catch (err) { + this.error(err); + return; + } + this.add(duration.subscribe(new GroupDurationSubscriber(key, group, this))); + } + } + if (!group.closed) { + group.next(element); + } + }; + GroupBySubscriber.prototype._error = function (err) { + var groups = this.groups; + if (groups) { + groups.forEach(function (group, key) { + group.error(err); + }); + groups.clear(); + } + this.destination.error(err); + }; + GroupBySubscriber.prototype._complete = function () { + var groups = this.groups; + if (groups) { + groups.forEach(function (group, key) { + group.complete(); + }); + groups.clear(); + } + this.destination.complete(); + }; + GroupBySubscriber.prototype.removeGroup = function (key) { + this.groups.delete(key); + }; + GroupBySubscriber.prototype.unsubscribe = function () { + if (!this.closed) { + this.attemptedToUnsubscribe = true; + if (this.count === 0) { + _super.prototype.unsubscribe.call(this); + } + } + }; + return GroupBySubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); +var GroupDurationSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](GroupDurationSubscriber, _super); + function GroupDurationSubscriber(key, group, parent) { + var _this = _super.call(this, group) || this; + _this.key = key; + _this.group = group; + _this.parent = parent; + return _this; } - return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { - var due = Object(_util_isNumeric__WEBPACK_IMPORTED_MODULE_2__["isNumeric"])(dueTime) - ? dueTime - : (+dueTime - scheduler.now()); - return scheduler.schedule(dispatch, due, { - index: 0, period: period, subscriber: subscriber - }); - }); -} -function dispatch(state) { - var index = state.index, period = state.period, subscriber = state.subscriber; - subscriber.next(index); - if (subscriber.closed) { - return; + GroupDurationSubscriber.prototype._next = function (value) { + this.complete(); + }; + GroupDurationSubscriber.prototype._unsubscribe = function () { + var _a = this, parent = _a.parent, key = _a.key; + this.key = this.parent = null; + if (parent) { + parent.removeGroup(key); + } + }; + return GroupDurationSubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); +var GroupedObservable = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](GroupedObservable, _super); + function GroupedObservable(key, groupSubject, refCountSubscription) { + var _this = _super.call(this) || this; + _this.key = key; + _this.groupSubject = groupSubject; + _this.refCountSubscription = refCountSubscription; + return _this; } - else if (period === -1) { - return subscriber.complete(); + GroupedObservable.prototype._subscribe = function (subscriber) { + var subscription = new _Subscription__WEBPACK_IMPORTED_MODULE_2__["Subscription"](); + var _a = this, refCountSubscription = _a.refCountSubscription, groupSubject = _a.groupSubject; + if (refCountSubscription && !refCountSubscription.closed) { + subscription.add(new InnerRefCountSubscription(refCountSubscription)); + } + subscription.add(groupSubject.subscribe(subscriber)); + return subscription; + }; + return GroupedObservable; +}(_Observable__WEBPACK_IMPORTED_MODULE_3__["Observable"])); + +var InnerRefCountSubscription = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](InnerRefCountSubscription, _super); + function InnerRefCountSubscription(parent) { + var _this = _super.call(this) || this; + _this.parent = parent; + parent.count++; + return _this; } - state.index = index + 1; - this.schedule(state, period); -} -//# sourceMappingURL=timer.js.map + InnerRefCountSubscription.prototype.unsubscribe = function () { + var parent = this.parent; + if (!parent.closed && !this.closed) { + _super.prototype.unsubscribe.call(this); + parent.count -= 1; + if (parent.count === 0 && parent.attemptedToUnsubscribe) { + parent.unsubscribe(); + } + } + }; + return InnerRefCountSubscription; +}(_Subscription__WEBPACK_IMPORTED_MODULE_2__["Subscription"])); +//# sourceMappingURL=groupBy.js.map /***/ }), -/* 205 */ +/* 192 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isNumeric", function() { return isNumeric; }); -/* harmony import */ var _isArray__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(178); -/** PURE_IMPORTS_START _isArray PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "BehaviorSubject", function() { return BehaviorSubject; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(187); +/* harmony import */ var _util_ObjectUnsubscribedError__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(188); +/** PURE_IMPORTS_START tslib,_Subject,_util_ObjectUnsubscribedError PURE_IMPORTS_END */ -function isNumeric(val) { - return !Object(_isArray__WEBPACK_IMPORTED_MODULE_0__["isArray"])(val) && (val - parseFloat(val) + 1) >= 0; -} -//# sourceMappingURL=isNumeric.js.map -/***/ }), -/* 206 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { +var BehaviorSubject = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](BehaviorSubject, _super); + function BehaviorSubject(_value) { + var _this = _super.call(this) || this; + _this._value = _value; + return _this; + } + Object.defineProperty(BehaviorSubject.prototype, "value", { + get: function () { + return this.getValue(); + }, + enumerable: true, + configurable: true + }); + BehaviorSubject.prototype._subscribe = function (subscriber) { + var subscription = _super.prototype._subscribe.call(this, subscriber); + if (subscription && !subscription.closed) { + subscriber.next(this._value); + } + return subscription; + }; + BehaviorSubject.prototype.getValue = function () { + if (this.hasError) { + throw this.thrownError; + } + else if (this.closed) { + throw new _util_ObjectUnsubscribedError__WEBPACK_IMPORTED_MODULE_2__["ObjectUnsubscribedError"](); + } + else { + return this._value; + } + }; + BehaviorSubject.prototype.next = function (value) { + _super.prototype.next.call(this, this._value = value); + }; + return BehaviorSubject; +}(_Subject__WEBPACK_IMPORTED_MODULE_1__["Subject"])); -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isScheduler", function() { return isScheduler; }); -/** PURE_IMPORTS_START PURE_IMPORTS_END */ -function isScheduler(value) { - return value && typeof value.schedule === 'function'; -} -//# sourceMappingURL=isScheduler.js.map +//# sourceMappingURL=BehaviorSubject.js.map /***/ }), -/* 207 */ +/* 193 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buffer", function() { return buffer; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ReplaySubject", function() { return ReplaySubject; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(171); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(182); -/** PURE_IMPORTS_START tslib,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ - +/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(187); +/* harmony import */ var _scheduler_queue__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(194); +/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(177); +/* harmony import */ var _operators_observeOn__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(201); +/* harmony import */ var _util_ObjectUnsubscribedError__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(188); +/* harmony import */ var _SubjectSubscription__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(189); +/** PURE_IMPORTS_START tslib,_Subject,_scheduler_queue,_Subscription,_operators_observeOn,_util_ObjectUnsubscribedError,_SubjectSubscription PURE_IMPORTS_END */ -function buffer(closingNotifier) { - return function bufferOperatorFunction(source) { - return source.lift(new BufferOperator(closingNotifier)); - }; -} -var BufferOperator = /*@__PURE__*/ (function () { - function BufferOperator(closingNotifier) { - this.closingNotifier = closingNotifier; - } - BufferOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new BufferSubscriber(subscriber, this.closingNotifier)); - }; - return BufferOperator; -}()); -var BufferSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](BufferSubscriber, _super); - function BufferSubscriber(destination, closingNotifier) { - var _this = _super.call(this, destination) || this; - _this.buffer = []; - _this.add(Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__["subscribeToResult"])(_this, closingNotifier)); - return _this; - } - BufferSubscriber.prototype._next = function (value) { - this.buffer.push(value); - }; - BufferSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { - var buffer = this.buffer; - this.buffer = []; - this.destination.next(buffer); - }; - return BufferSubscriber; -}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__["OuterSubscriber"])); -//# sourceMappingURL=buffer.js.map -/***/ }), -/* 208 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "bufferCount", function() { return bufferCount; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); -/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ -function bufferCount(bufferSize, startBufferEvery) { - if (startBufferEvery === void 0) { - startBufferEvery = null; - } - return function bufferCountOperatorFunction(source) { - return source.lift(new BufferCountOperator(bufferSize, startBufferEvery)); - }; -} -var BufferCountOperator = /*@__PURE__*/ (function () { - function BufferCountOperator(bufferSize, startBufferEvery) { - this.bufferSize = bufferSize; - this.startBufferEvery = startBufferEvery; - if (!startBufferEvery || bufferSize === startBufferEvery) { - this.subscriberClass = BufferCountSubscriber; +var ReplaySubject = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](ReplaySubject, _super); + function ReplaySubject(bufferSize, windowTime, scheduler) { + if (bufferSize === void 0) { + bufferSize = Number.POSITIVE_INFINITY; + } + if (windowTime === void 0) { + windowTime = Number.POSITIVE_INFINITY; + } + var _this = _super.call(this) || this; + _this.scheduler = scheduler; + _this._events = []; + _this._infiniteTimeWindow = false; + _this._bufferSize = bufferSize < 1 ? 1 : bufferSize; + _this._windowTime = windowTime < 1 ? 1 : windowTime; + if (windowTime === Number.POSITIVE_INFINITY) { + _this._infiniteTimeWindow = true; + _this.next = _this.nextInfiniteTimeWindow; } else { - this.subscriberClass = BufferSkipCountSubscriber; + _this.next = _this.nextTimeWindow; } - } - BufferCountOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new this.subscriberClass(subscriber, this.bufferSize, this.startBufferEvery)); - }; - return BufferCountOperator; -}()); -var BufferCountSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](BufferCountSubscriber, _super); - function BufferCountSubscriber(destination, bufferSize) { - var _this = _super.call(this, destination) || this; - _this.bufferSize = bufferSize; - _this.buffer = []; return _this; } - BufferCountSubscriber.prototype._next = function (value) { - var buffer = this.buffer; - buffer.push(value); - if (buffer.length == this.bufferSize) { - this.destination.next(buffer); - this.buffer = []; + ReplaySubject.prototype.nextInfiniteTimeWindow = function (value) { + var _events = this._events; + _events.push(value); + if (_events.length > this._bufferSize) { + _events.shift(); } + _super.prototype.next.call(this, value); }; - BufferCountSubscriber.prototype._complete = function () { - var buffer = this.buffer; - if (buffer.length > 0) { - this.destination.next(buffer); - } - _super.prototype._complete.call(this); + ReplaySubject.prototype.nextTimeWindow = function (value) { + this._events.push(new ReplayEvent(this._getNow(), value)); + this._trimBufferThenGetEvents(); + _super.prototype.next.call(this, value); }; - return BufferCountSubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); -var BufferSkipCountSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](BufferSkipCountSubscriber, _super); - function BufferSkipCountSubscriber(destination, bufferSize, startBufferEvery) { - var _this = _super.call(this, destination) || this; - _this.bufferSize = bufferSize; - _this.startBufferEvery = startBufferEvery; - _this.buffers = []; - _this.count = 0; - return _this; - } - BufferSkipCountSubscriber.prototype._next = function (value) { - var _a = this, bufferSize = _a.bufferSize, startBufferEvery = _a.startBufferEvery, buffers = _a.buffers, count = _a.count; - this.count++; - if (count % startBufferEvery === 0) { - buffers.push([]); + ReplaySubject.prototype._subscribe = function (subscriber) { + var _infiniteTimeWindow = this._infiniteTimeWindow; + var _events = _infiniteTimeWindow ? this._events : this._trimBufferThenGetEvents(); + var scheduler = this.scheduler; + var len = _events.length; + var subscription; + if (this.closed) { + throw new _util_ObjectUnsubscribedError__WEBPACK_IMPORTED_MODULE_5__["ObjectUnsubscribedError"](); } - for (var i = buffers.length; i--;) { - var buffer = buffers[i]; - buffer.push(value); - if (buffer.length === bufferSize) { - buffers.splice(i, 1); - this.destination.next(buffer); + else if (this.isStopped || this.hasError) { + subscription = _Subscription__WEBPACK_IMPORTED_MODULE_3__["Subscription"].EMPTY; + } + else { + this.observers.push(subscriber); + subscription = new _SubjectSubscription__WEBPACK_IMPORTED_MODULE_6__["SubjectSubscription"](this, subscriber); + } + if (scheduler) { + subscriber.add(subscriber = new _operators_observeOn__WEBPACK_IMPORTED_MODULE_4__["ObserveOnSubscriber"](subscriber, scheduler)); + } + if (_infiniteTimeWindow) { + for (var i = 0; i < len && !subscriber.closed; i++) { + subscriber.next(_events[i]); + } + } + else { + for (var i = 0; i < len && !subscriber.closed; i++) { + subscriber.next(_events[i].value); } } + if (this.hasError) { + subscriber.error(this.thrownError); + } + else if (this.isStopped) { + subscriber.complete(); + } + return subscription; }; - BufferSkipCountSubscriber.prototype._complete = function () { - var _a = this, buffers = _a.buffers, destination = _a.destination; - while (buffers.length > 0) { - var buffer = buffers.shift(); - if (buffer.length > 0) { - destination.next(buffer); + ReplaySubject.prototype._getNow = function () { + return (this.scheduler || _scheduler_queue__WEBPACK_IMPORTED_MODULE_2__["queue"]).now(); + }; + ReplaySubject.prototype._trimBufferThenGetEvents = function () { + var now = this._getNow(); + var _bufferSize = this._bufferSize; + var _windowTime = this._windowTime; + var _events = this._events; + var eventsCount = _events.length; + var spliceCount = 0; + while (spliceCount < eventsCount) { + if ((now - _events[spliceCount].time) < _windowTime) { + break; } + spliceCount++; } - _super.prototype._complete.call(this); + if (eventsCount > _bufferSize) { + spliceCount = Math.max(spliceCount, eventsCount - _bufferSize); + } + if (spliceCount > 0) { + _events.splice(0, spliceCount); + } + return _events; }; - return BufferSkipCountSubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); -//# sourceMappingURL=bufferCount.js.map + return ReplaySubject; +}(_Subject__WEBPACK_IMPORTED_MODULE_1__["Subject"])); + +var ReplayEvent = /*@__PURE__*/ (function () { + function ReplayEvent(time, value) { + this.time = time; + this.value = value; + } + return ReplayEvent; +}()); +//# sourceMappingURL=ReplaySubject.js.map /***/ }), -/* 209 */ +/* 194 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "bufferTime", function() { return bufferTime; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(199); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(172); -/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(206); -/** PURE_IMPORTS_START tslib,_scheduler_async,_Subscriber,_util_isScheduler PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "queue", function() { return queue; }); +/* harmony import */ var _QueueAction__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(195); +/* harmony import */ var _QueueScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(198); +/** PURE_IMPORTS_START _QueueAction,_QueueScheduler PURE_IMPORTS_END */ +var queue = /*@__PURE__*/ new _QueueScheduler__WEBPACK_IMPORTED_MODULE_1__["QueueScheduler"](_QueueAction__WEBPACK_IMPORTED_MODULE_0__["QueueAction"]); +//# sourceMappingURL=queue.js.map -function bufferTime(bufferTimeSpan) { - var length = arguments.length; - var scheduler = _scheduler_async__WEBPACK_IMPORTED_MODULE_1__["async"]; - if (Object(_util_isScheduler__WEBPACK_IMPORTED_MODULE_3__["isScheduler"])(arguments[arguments.length - 1])) { - scheduler = arguments[arguments.length - 1]; - length--; - } - var bufferCreationInterval = null; - if (length >= 2) { - bufferCreationInterval = arguments[1]; +/***/ }), +/* 195 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "QueueAction", function() { return QueueAction; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _AsyncAction__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(196); +/** PURE_IMPORTS_START tslib,_AsyncAction PURE_IMPORTS_END */ + + +var QueueAction = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](QueueAction, _super); + function QueueAction(scheduler, work) { + var _this = _super.call(this, scheduler, work) || this; + _this.scheduler = scheduler; + _this.work = work; + return _this; } - var maxBufferSize = Number.POSITIVE_INFINITY; - if (length >= 3) { - maxBufferSize = arguments[2]; - } - return function bufferTimeOperatorFunction(source) { - return source.lift(new BufferTimeOperator(bufferTimeSpan, bufferCreationInterval, maxBufferSize, scheduler)); - }; -} -var BufferTimeOperator = /*@__PURE__*/ (function () { - function BufferTimeOperator(bufferTimeSpan, bufferCreationInterval, maxBufferSize, scheduler) { - this.bufferTimeSpan = bufferTimeSpan; - this.bufferCreationInterval = bufferCreationInterval; - this.maxBufferSize = maxBufferSize; - this.scheduler = scheduler; - } - BufferTimeOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new BufferTimeSubscriber(subscriber, this.bufferTimeSpan, this.bufferCreationInterval, this.maxBufferSize, this.scheduler)); - }; - return BufferTimeOperator; -}()); -var Context = /*@__PURE__*/ (function () { - function Context() { - this.buffer = []; - } - return Context; -}()); -var BufferTimeSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](BufferTimeSubscriber, _super); - function BufferTimeSubscriber(destination, bufferTimeSpan, bufferCreationInterval, maxBufferSize, scheduler) { - var _this = _super.call(this, destination) || this; - _this.bufferTimeSpan = bufferTimeSpan; - _this.bufferCreationInterval = bufferCreationInterval; - _this.maxBufferSize = maxBufferSize; - _this.scheduler = scheduler; - _this.contexts = []; - var context = _this.openContext(); - _this.timespanOnly = bufferCreationInterval == null || bufferCreationInterval < 0; - if (_this.timespanOnly) { - var timeSpanOnlyState = { subscriber: _this, context: context, bufferTimeSpan: bufferTimeSpan }; - _this.add(context.closeAction = scheduler.schedule(dispatchBufferTimeSpanOnly, bufferTimeSpan, timeSpanOnlyState)); - } - else { - var closeState = { subscriber: _this, context: context }; - var creationState = { bufferTimeSpan: bufferTimeSpan, bufferCreationInterval: bufferCreationInterval, subscriber: _this, scheduler: scheduler }; - _this.add(context.closeAction = scheduler.schedule(dispatchBufferClose, bufferTimeSpan, closeState)); - _this.add(scheduler.schedule(dispatchBufferCreation, bufferCreationInterval, creationState)); - } - return _this; - } - BufferTimeSubscriber.prototype._next = function (value) { - var contexts = this.contexts; - var len = contexts.length; - var filledBufferContext; - for (var i = 0; i < len; i++) { - var context_1 = contexts[i]; - var buffer = context_1.buffer; - buffer.push(value); - if (buffer.length == this.maxBufferSize) { - filledBufferContext = context_1; - } - } - if (filledBufferContext) { - this.onBufferFull(filledBufferContext); + QueueAction.prototype.schedule = function (state, delay) { + if (delay === void 0) { + delay = 0; } - }; - BufferTimeSubscriber.prototype._error = function (err) { - this.contexts.length = 0; - _super.prototype._error.call(this, err); - }; - BufferTimeSubscriber.prototype._complete = function () { - var _a = this, contexts = _a.contexts, destination = _a.destination; - while (contexts.length > 0) { - var context_2 = contexts.shift(); - destination.next(context_2.buffer); + if (delay > 0) { + return _super.prototype.schedule.call(this, state, delay); } - _super.prototype._complete.call(this); + this.delay = delay; + this.state = state; + this.scheduler.flush(this); + return this; }; - BufferTimeSubscriber.prototype._unsubscribe = function () { - this.contexts = null; + QueueAction.prototype.execute = function (state, delay) { + return (delay > 0 || this.closed) ? + _super.prototype.execute.call(this, state, delay) : + this._execute(state, delay); }; - BufferTimeSubscriber.prototype.onBufferFull = function (context) { - this.closeContext(context); - var closeAction = context.closeAction; - closeAction.unsubscribe(); - this.remove(closeAction); - if (!this.closed && this.timespanOnly) { - context = this.openContext(); - var bufferTimeSpan = this.bufferTimeSpan; - var timeSpanOnlyState = { subscriber: this, context: context, bufferTimeSpan: bufferTimeSpan }; - this.add(context.closeAction = this.scheduler.schedule(dispatchBufferTimeSpanOnly, bufferTimeSpan, timeSpanOnlyState)); + QueueAction.prototype.requestAsyncId = function (scheduler, id, delay) { + if (delay === void 0) { + delay = 0; } - }; - BufferTimeSubscriber.prototype.openContext = function () { - var context = new Context(); - this.contexts.push(context); - return context; - }; - BufferTimeSubscriber.prototype.closeContext = function (context) { - this.destination.next(context.buffer); - var contexts = this.contexts; - var spliceIndex = contexts ? contexts.indexOf(context) : -1; - if (spliceIndex >= 0) { - contexts.splice(contexts.indexOf(context), 1); + if ((delay !== null && delay > 0) || (delay === null && this.delay > 0)) { + return _super.prototype.requestAsyncId.call(this, scheduler, id, delay); } + return scheduler.flush(this); }; - return BufferTimeSubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_2__["Subscriber"])); -function dispatchBufferTimeSpanOnly(state) { - var subscriber = state.subscriber; - var prevContext = state.context; - if (prevContext) { - subscriber.closeContext(prevContext); - } - if (!subscriber.closed) { - state.context = subscriber.openContext(); - state.context.closeAction = this.schedule(state, state.bufferTimeSpan); - } -} -function dispatchBufferCreation(state) { - var bufferCreationInterval = state.bufferCreationInterval, bufferTimeSpan = state.bufferTimeSpan, subscriber = state.subscriber, scheduler = state.scheduler; - var context = subscriber.openContext(); - var action = this; - if (!subscriber.closed) { - subscriber.add(context.closeAction = scheduler.schedule(dispatchBufferClose, bufferTimeSpan, { subscriber: subscriber, context: context })); - action.schedule(state, bufferCreationInterval); - } -} -function dispatchBufferClose(arg) { - var subscriber = arg.subscriber, context = arg.context; - subscriber.closeContext(context); -} -//# sourceMappingURL=bufferTime.js.map + return QueueAction; +}(_AsyncAction__WEBPACK_IMPORTED_MODULE_1__["AsyncAction"])); + +//# sourceMappingURL=QueueAction.js.map /***/ }), -/* 210 */ +/* 196 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "bufferToggle", function() { return bufferToggle; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "AsyncAction", function() { return AsyncAction; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(177); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(182); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(171); -/** PURE_IMPORTS_START tslib,_Subscription,_util_subscribeToResult,_OuterSubscriber PURE_IMPORTS_END */ - - +/* harmony import */ var _Action__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(197); +/** PURE_IMPORTS_START tslib,_Action PURE_IMPORTS_END */ -function bufferToggle(openings, closingSelector) { - return function bufferToggleOperatorFunction(source) { - return source.lift(new BufferToggleOperator(openings, closingSelector)); - }; -} -var BufferToggleOperator = /*@__PURE__*/ (function () { - function BufferToggleOperator(openings, closingSelector) { - this.openings = openings; - this.closingSelector = closingSelector; - } - BufferToggleOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new BufferToggleSubscriber(subscriber, this.openings, this.closingSelector)); - }; - return BufferToggleOperator; -}()); -var BufferToggleSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](BufferToggleSubscriber, _super); - function BufferToggleSubscriber(destination, openings, closingSelector) { - var _this = _super.call(this, destination) || this; - _this.openings = openings; - _this.closingSelector = closingSelector; - _this.contexts = []; - _this.add(Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__["subscribeToResult"])(_this, openings)); +var AsyncAction = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](AsyncAction, _super); + function AsyncAction(scheduler, work) { + var _this = _super.call(this, scheduler, work) || this; + _this.scheduler = scheduler; + _this.work = work; + _this.pending = false; return _this; } - BufferToggleSubscriber.prototype._next = function (value) { - var contexts = this.contexts; - var len = contexts.length; - for (var i = 0; i < len; i++) { - contexts[i].buffer.push(value); + AsyncAction.prototype.schedule = function (state, delay) { + if (delay === void 0) { + delay = 0; } - }; - BufferToggleSubscriber.prototype._error = function (err) { - var contexts = this.contexts; - while (contexts.length > 0) { - var context_1 = contexts.shift(); - context_1.subscription.unsubscribe(); - context_1.buffer = null; - context_1.subscription = null; + if (this.closed) { + return this; } - this.contexts = null; - _super.prototype._error.call(this, err); + this.state = state; + var id = this.id; + var scheduler = this.scheduler; + if (id != null) { + this.id = this.recycleAsyncId(scheduler, id, delay); + } + this.pending = true; + this.delay = delay; + this.id = this.id || this.requestAsyncId(scheduler, this.id, delay); + return this; }; - BufferToggleSubscriber.prototype._complete = function () { - var contexts = this.contexts; - while (contexts.length > 0) { - var context_2 = contexts.shift(); - this.destination.next(context_2.buffer); - context_2.subscription.unsubscribe(); - context_2.buffer = null; - context_2.subscription = null; + AsyncAction.prototype.requestAsyncId = function (scheduler, id, delay) { + if (delay === void 0) { + delay = 0; } - this.contexts = null; - _super.prototype._complete.call(this); + return setInterval(scheduler.flush.bind(scheduler, this), delay); }; - BufferToggleSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { - outerValue ? this.closeBuffer(outerValue) : this.openBuffer(innerValue); + AsyncAction.prototype.recycleAsyncId = function (scheduler, id, delay) { + if (delay === void 0) { + delay = 0; + } + if (delay !== null && this.delay === delay && this.pending === false) { + return id; + } + clearInterval(id); + return undefined; }; - BufferToggleSubscriber.prototype.notifyComplete = function (innerSub) { - this.closeBuffer(innerSub.context); + AsyncAction.prototype.execute = function (state, delay) { + if (this.closed) { + return new Error('executing a cancelled action'); + } + this.pending = false; + var error = this._execute(state, delay); + if (error) { + return error; + } + else if (this.pending === false && this.id != null) { + this.id = this.recycleAsyncId(this.scheduler, this.id, null); + } }; - BufferToggleSubscriber.prototype.openBuffer = function (value) { + AsyncAction.prototype._execute = function (state, delay) { + var errored = false; + var errorValue = undefined; try { - var closingSelector = this.closingSelector; - var closingNotifier = closingSelector.call(this, value); - if (closingNotifier) { - this.trySubscribe(closingNotifier); - } + this.work(state); } - catch (err) { - this._error(err); + catch (e) { + errored = true; + errorValue = !!e && e || new Error(e); } - }; - BufferToggleSubscriber.prototype.closeBuffer = function (context) { - var contexts = this.contexts; - if (contexts && context) { - var buffer = context.buffer, subscription = context.subscription; - this.destination.next(buffer); - contexts.splice(contexts.indexOf(context), 1); - this.remove(subscription); - subscription.unsubscribe(); + if (errored) { + this.unsubscribe(); + return errorValue; } }; - BufferToggleSubscriber.prototype.trySubscribe = function (closingNotifier) { - var contexts = this.contexts; - var buffer = []; - var subscription = new _Subscription__WEBPACK_IMPORTED_MODULE_1__["Subscription"](); - var context = { buffer: buffer, subscription: subscription }; - contexts.push(context); - var innerSubscription = Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__["subscribeToResult"])(this, closingNotifier, context); - if (!innerSubscription || innerSubscription.closed) { - this.closeBuffer(context); + AsyncAction.prototype._unsubscribe = function () { + var id = this.id; + var scheduler = this.scheduler; + var actions = scheduler.actions; + var index = actions.indexOf(this); + this.work = null; + this.state = null; + this.pending = false; + this.scheduler = null; + if (index !== -1) { + actions.splice(index, 1); } - else { - innerSubscription.context = context; - this.add(innerSubscription); - subscription.add(innerSubscription); + if (id != null) { + this.id = this.recycleAsyncId(scheduler, id, null); } + this.delay = null; }; - return BufferToggleSubscriber; -}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__["OuterSubscriber"])); -//# sourceMappingURL=bufferToggle.js.map + return AsyncAction; +}(_Action__WEBPACK_IMPORTED_MODULE_1__["Action"])); + +//# sourceMappingURL=AsyncAction.js.map /***/ }), -/* 211 */ +/* 197 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "bufferWhen", function() { return bufferWhen; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Action", function() { return Action; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); /* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(177); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(171); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(182); -/** PURE_IMPORTS_START tslib,_Subscription,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ - - +/** PURE_IMPORTS_START tslib,_Subscription PURE_IMPORTS_END */ -function bufferWhen(closingSelector) { - return function (source) { - return source.lift(new BufferWhenOperator(closingSelector)); - }; -} -var BufferWhenOperator = /*@__PURE__*/ (function () { - function BufferWhenOperator(closingSelector) { - this.closingSelector = closingSelector; - } - BufferWhenOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new BufferWhenSubscriber(subscriber, this.closingSelector)); - }; - return BufferWhenOperator; -}()); -var BufferWhenSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](BufferWhenSubscriber, _super); - function BufferWhenSubscriber(destination, closingSelector) { - var _this = _super.call(this, destination) || this; - _this.closingSelector = closingSelector; - _this.subscribing = false; - _this.openBuffer(); - return _this; +var Action = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](Action, _super); + function Action(scheduler, work) { + return _super.call(this) || this; } - BufferWhenSubscriber.prototype._next = function (value) { - this.buffer.push(value); - }; - BufferWhenSubscriber.prototype._complete = function () { - var buffer = this.buffer; - if (buffer) { - this.destination.next(buffer); - } - _super.prototype._complete.call(this); - }; - BufferWhenSubscriber.prototype._unsubscribe = function () { - this.buffer = null; - this.subscribing = false; - }; - BufferWhenSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { - this.openBuffer(); - }; - BufferWhenSubscriber.prototype.notifyComplete = function () { - if (this.subscribing) { - this.complete(); - } - else { - this.openBuffer(); - } - }; - BufferWhenSubscriber.prototype.openBuffer = function () { - var closingSubscription = this.closingSubscription; - if (closingSubscription) { - this.remove(closingSubscription); - closingSubscription.unsubscribe(); - } - var buffer = this.buffer; - if (this.buffer) { - this.destination.next(buffer); - } - this.buffer = []; - var closingNotifier; - try { - var closingSelector = this.closingSelector; - closingNotifier = closingSelector(); - } - catch (err) { - return this.error(err); + Action.prototype.schedule = function (state, delay) { + if (delay === void 0) { + delay = 0; } - closingSubscription = new _Subscription__WEBPACK_IMPORTED_MODULE_1__["Subscription"](); - this.closingSubscription = closingSubscription; - this.add(closingSubscription); - this.subscribing = true; - closingSubscription.add(Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__["subscribeToResult"])(this, closingNotifier)); - this.subscribing = false; + return this; }; - return BufferWhenSubscriber; -}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_2__["OuterSubscriber"])); -//# sourceMappingURL=bufferWhen.js.map + return Action; +}(_Subscription__WEBPACK_IMPORTED_MODULE_1__["Subscription"])); + +//# sourceMappingURL=Action.js.map /***/ }), -/* 212 */ +/* 198 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "catchError", function() { return catchError; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "QueueScheduler", function() { return QueueScheduler; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(171); -/* harmony import */ var _InnerSubscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(183); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(182); -/** PURE_IMPORTS_START tslib,_OuterSubscriber,_InnerSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ - - +/* harmony import */ var _AsyncScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(199); +/** PURE_IMPORTS_START tslib,_AsyncScheduler PURE_IMPORTS_END */ -function catchError(selector) { - return function catchErrorOperatorFunction(source) { - var operator = new CatchOperator(selector); - var caught = source.lift(operator); - return (operator.caught = caught); - }; -} -var CatchOperator = /*@__PURE__*/ (function () { - function CatchOperator(selector) { - this.selector = selector; - } - CatchOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new CatchSubscriber(subscriber, this.selector, this.caught)); - }; - return CatchOperator; -}()); -var CatchSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](CatchSubscriber, _super); - function CatchSubscriber(destination, selector, caught) { - var _this = _super.call(this, destination) || this; - _this.selector = selector; - _this.caught = caught; - return _this; +var QueueScheduler = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](QueueScheduler, _super); + function QueueScheduler() { + return _super !== null && _super.apply(this, arguments) || this; } - CatchSubscriber.prototype.error = function (err) { - if (!this.isStopped) { - var result = void 0; - try { - result = this.selector(err, this.caught); - } - catch (err2) { - _super.prototype.error.call(this, err2); - return; - } - this._unsubscribeAndRecycle(); - var innerSubscriber = new _InnerSubscriber__WEBPACK_IMPORTED_MODULE_2__["InnerSubscriber"](this, undefined, undefined); - this.add(innerSubscriber); - Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__["subscribeToResult"])(this, result, undefined, undefined, innerSubscriber); - } - }; - return CatchSubscriber; -}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__["OuterSubscriber"])); -//# sourceMappingURL=catchError.js.map - - -/***/ }), -/* 213 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "combineAll", function() { return combineAll; }); -/* harmony import */ var _observable_combineLatest__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(214); -/** PURE_IMPORTS_START _observable_combineLatest PURE_IMPORTS_END */ + return QueueScheduler; +}(_AsyncScheduler__WEBPACK_IMPORTED_MODULE_1__["AsyncScheduler"])); -function combineAll(project) { - return function (source) { return source.lift(new _observable_combineLatest__WEBPACK_IMPORTED_MODULE_0__["CombineLatestOperator"](project)); }; -} -//# sourceMappingURL=combineAll.js.map +//# sourceMappingURL=QueueScheduler.js.map /***/ }), -/* 214 */ +/* 199 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "combineLatest", function() { return combineLatest; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "CombineLatestOperator", function() { return CombineLatestOperator; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "CombineLatestSubscriber", function() { return CombineLatestSubscriber; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "AsyncScheduler", function() { return AsyncScheduler; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(206); -/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(178); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(171); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(182); -/* harmony import */ var _fromArray__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(215); -/** PURE_IMPORTS_START tslib,_util_isScheduler,_util_isArray,_OuterSubscriber,_util_subscribeToResult,_fromArray PURE_IMPORTS_END */ - - - - +/* harmony import */ var _Scheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(200); +/** PURE_IMPORTS_START tslib,_Scheduler PURE_IMPORTS_END */ -var NONE = {}; -function combineLatest() { - var observables = []; - for (var _i = 0; _i < arguments.length; _i++) { - observables[_i] = arguments[_i]; - } - var resultSelector = null; - var scheduler = null; - if (Object(_util_isScheduler__WEBPACK_IMPORTED_MODULE_1__["isScheduler"])(observables[observables.length - 1])) { - scheduler = observables.pop(); - } - if (typeof observables[observables.length - 1] === 'function') { - resultSelector = observables.pop(); - } - if (observables.length === 1 && Object(_util_isArray__WEBPACK_IMPORTED_MODULE_2__["isArray"])(observables[0])) { - observables = observables[0]; - } - return Object(_fromArray__WEBPACK_IMPORTED_MODULE_5__["fromArray"])(observables, scheduler).lift(new CombineLatestOperator(resultSelector)); -} -var CombineLatestOperator = /*@__PURE__*/ (function () { - function CombineLatestOperator(resultSelector) { - this.resultSelector = resultSelector; - } - CombineLatestOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new CombineLatestSubscriber(subscriber, this.resultSelector)); - }; - return CombineLatestOperator; -}()); - -var CombineLatestSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](CombineLatestSubscriber, _super); - function CombineLatestSubscriber(destination, resultSelector) { - var _this = _super.call(this, destination) || this; - _this.resultSelector = resultSelector; - _this.active = 0; - _this.values = []; - _this.observables = []; +var AsyncScheduler = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](AsyncScheduler, _super); + function AsyncScheduler(SchedulerAction, now) { + if (now === void 0) { + now = _Scheduler__WEBPACK_IMPORTED_MODULE_1__["Scheduler"].now; + } + var _this = _super.call(this, SchedulerAction, function () { + if (AsyncScheduler.delegate && AsyncScheduler.delegate !== _this) { + return AsyncScheduler.delegate.now(); + } + else { + return now(); + } + }) || this; + _this.actions = []; + _this.active = false; + _this.scheduled = undefined; return _this; } - CombineLatestSubscriber.prototype._next = function (observable) { - this.values.push(NONE); - this.observables.push(observable); - }; - CombineLatestSubscriber.prototype._complete = function () { - var observables = this.observables; - var len = observables.length; - if (len === 0) { - this.destination.complete(); + AsyncScheduler.prototype.schedule = function (work, delay, state) { + if (delay === void 0) { + delay = 0; + } + if (AsyncScheduler.delegate && AsyncScheduler.delegate !== this) { + return AsyncScheduler.delegate.schedule(work, delay, state); } else { - this.active = len; - this.toRespond = len; - for (var i = 0; i < len; i++) { - var observable = observables[i]; - this.add(Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__["subscribeToResult"])(this, observable, observable, i)); - } + return _super.prototype.schedule.call(this, work, delay, state); } }; - CombineLatestSubscriber.prototype.notifyComplete = function (unused) { - if ((this.active -= 1) === 0) { - this.destination.complete(); + AsyncScheduler.prototype.flush = function (action) { + var actions = this.actions; + if (this.active) { + actions.push(action); + return; } - }; - CombineLatestSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { - var values = this.values; - var oldVal = values[outerIndex]; - var toRespond = !this.toRespond - ? 0 - : oldVal === NONE ? --this.toRespond : this.toRespond; - values[outerIndex] = innerValue; - if (toRespond === 0) { - if (this.resultSelector) { - this._tryResultSelector(values); + var error; + this.active = true; + do { + if (error = action.execute(action.state, action.delay)) { + break; } - else { - this.destination.next(values.slice()); + } while (action = actions.shift()); + this.active = false; + if (error) { + while (action = actions.shift()) { + action.unsubscribe(); } + throw error; } }; - CombineLatestSubscriber.prototype._tryResultSelector = function (values) { - var result; - try { - result = this.resultSelector.apply(this, values); + return AsyncScheduler; +}(_Scheduler__WEBPACK_IMPORTED_MODULE_1__["Scheduler"])); + +//# sourceMappingURL=AsyncScheduler.js.map + + +/***/ }), +/* 200 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Scheduler", function() { return Scheduler; }); +var Scheduler = /*@__PURE__*/ (function () { + function Scheduler(SchedulerAction, now) { + if (now === void 0) { + now = Scheduler.now; } - catch (err) { - this.destination.error(err); - return; + this.SchedulerAction = SchedulerAction; + this.now = now; + } + Scheduler.prototype.schedule = function (work, delay, state) { + if (delay === void 0) { + delay = 0; } - this.destination.next(result); + return new this.SchedulerAction(this, work).schedule(state, delay); }; - return CombineLatestSubscriber; -}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__["OuterSubscriber"])); + Scheduler.now = function () { return Date.now(); }; + return Scheduler; +}()); -//# sourceMappingURL=combineLatest.js.map +//# sourceMappingURL=Scheduler.js.map /***/ }), -/* 215 */ +/* 201 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "fromArray", function() { return fromArray; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(193); -/* harmony import */ var _util_subscribeToArray__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(185); -/* harmony import */ var _scheduled_scheduleArray__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(216); -/** PURE_IMPORTS_START _Observable,_util_subscribeToArray,_scheduled_scheduleArray PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "observeOn", function() { return observeOn; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ObserveOnOperator", function() { return ObserveOnOperator; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ObserveOnSubscriber", function() { return ObserveOnSubscriber; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ObserveOnMessage", function() { return ObserveOnMessage; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); +/* harmony import */ var _Notification__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(202); +/** PURE_IMPORTS_START tslib,_Subscriber,_Notification PURE_IMPORTS_END */ -function fromArray(input, scheduler) { - if (!scheduler) { - return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](Object(_util_subscribeToArray__WEBPACK_IMPORTED_MODULE_1__["subscribeToArray"])(input)); - } - else { - return Object(_scheduled_scheduleArray__WEBPACK_IMPORTED_MODULE_2__["scheduleArray"])(input, scheduler); +function observeOn(scheduler, delay) { + if (delay === void 0) { + delay = 0; } + return function observeOnOperatorFunction(source) { + return source.lift(new ObserveOnOperator(scheduler, delay)); + }; } -//# sourceMappingURL=fromArray.js.map +var ObserveOnOperator = /*@__PURE__*/ (function () { + function ObserveOnOperator(scheduler, delay) { + if (delay === void 0) { + delay = 0; + } + this.scheduler = scheduler; + this.delay = delay; + } + ObserveOnOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new ObserveOnSubscriber(subscriber, this.scheduler, this.delay)); + }; + return ObserveOnOperator; +}()); + +var ObserveOnSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](ObserveOnSubscriber, _super); + function ObserveOnSubscriber(destination, scheduler, delay) { + if (delay === void 0) { + delay = 0; + } + var _this = _super.call(this, destination) || this; + _this.scheduler = scheduler; + _this.delay = delay; + return _this; + } + ObserveOnSubscriber.dispatch = function (arg) { + var notification = arg.notification, destination = arg.destination; + notification.observe(destination); + this.unsubscribe(); + }; + ObserveOnSubscriber.prototype.scheduleMessage = function (notification) { + var destination = this.destination; + destination.add(this.scheduler.schedule(ObserveOnSubscriber.dispatch, this.delay, new ObserveOnMessage(notification, this.destination))); + }; + ObserveOnSubscriber.prototype._next = function (value) { + this.scheduleMessage(_Notification__WEBPACK_IMPORTED_MODULE_2__["Notification"].createNext(value)); + }; + ObserveOnSubscriber.prototype._error = function (err) { + this.scheduleMessage(_Notification__WEBPACK_IMPORTED_MODULE_2__["Notification"].createError(err)); + this.unsubscribe(); + }; + ObserveOnSubscriber.prototype._complete = function () { + this.scheduleMessage(_Notification__WEBPACK_IMPORTED_MODULE_2__["Notification"].createComplete()); + this.unsubscribe(); + }; + return ObserveOnSubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); + +var ObserveOnMessage = /*@__PURE__*/ (function () { + function ObserveOnMessage(notification, destination) { + this.notification = notification; + this.destination = destination; + } + return ObserveOnMessage; +}()); + +//# sourceMappingURL=observeOn.js.map /***/ }), -/* 216 */ +/* 202 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "scheduleArray", function() { return scheduleArray; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(193); -/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(177); -/** PURE_IMPORTS_START _Observable,_Subscription PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "NotificationKind", function() { return NotificationKind; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Notification", function() { return Notification; }); +/* harmony import */ var _observable_empty__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(203); +/* harmony import */ var _observable_of__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(204); +/* harmony import */ var _observable_throwError__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(209); +/** PURE_IMPORTS_START _observable_empty,_observable_of,_observable_throwError PURE_IMPORTS_END */ -function scheduleArray(input, scheduler) { - return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { - var sub = new _Subscription__WEBPACK_IMPORTED_MODULE_1__["Subscription"](); - var i = 0; - sub.add(scheduler.schedule(function () { - if (i === input.length) { - subscriber.complete(); - return; - } - subscriber.next(input[i++]); - if (!subscriber.closed) { - sub.add(this.schedule()); - } - })); - return sub; - }); -} -//# sourceMappingURL=scheduleArray.js.map + +var NotificationKind; +/*@__PURE__*/ (function (NotificationKind) { + NotificationKind["NEXT"] = "N"; + NotificationKind["ERROR"] = "E"; + NotificationKind["COMPLETE"] = "C"; +})(NotificationKind || (NotificationKind = {})); +var Notification = /*@__PURE__*/ (function () { + function Notification(kind, value, error) { + this.kind = kind; + this.value = value; + this.error = error; + this.hasValue = kind === 'N'; + } + Notification.prototype.observe = function (observer) { + switch (this.kind) { + case 'N': + return observer.next && observer.next(this.value); + case 'E': + return observer.error && observer.error(this.error); + case 'C': + return observer.complete && observer.complete(); + } + }; + Notification.prototype.do = function (next, error, complete) { + var kind = this.kind; + switch (kind) { + case 'N': + return next && next(this.value); + case 'E': + return error && error(this.error); + case 'C': + return complete && complete(); + } + }; + Notification.prototype.accept = function (nextOrObserver, error, complete) { + if (nextOrObserver && typeof nextOrObserver.next === 'function') { + return this.observe(nextOrObserver); + } + else { + return this.do(nextOrObserver, error, complete); + } + }; + Notification.prototype.toObservable = function () { + var kind = this.kind; + switch (kind) { + case 'N': + return Object(_observable_of__WEBPACK_IMPORTED_MODULE_1__["of"])(this.value); + case 'E': + return Object(_observable_throwError__WEBPACK_IMPORTED_MODULE_2__["throwError"])(this.error); + case 'C': + return Object(_observable_empty__WEBPACK_IMPORTED_MODULE_0__["empty"])(); + } + throw new Error('unexpected notification kind value'); + }; + Notification.createNext = function (value) { + if (typeof value !== 'undefined') { + return new Notification('N', value); + } + return Notification.undefinedValueNotification; + }; + Notification.createError = function (err) { + return new Notification('E', undefined, err); + }; + Notification.createComplete = function () { + return Notification.completeNotification; + }; + Notification.completeNotification = new Notification('C'); + Notification.undefinedValueNotification = new Notification('N', undefined); + return Notification; +}()); + +//# sourceMappingURL=Notification.js.map /***/ }), -/* 217 */ +/* 203 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "combineLatest", function() { return combineLatest; }); -/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(178); -/* harmony import */ var _observable_combineLatest__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(214); -/* harmony import */ var _observable_from__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(218); -/** PURE_IMPORTS_START _util_isArray,_observable_combineLatest,_observable_from PURE_IMPORTS_END */ - - +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "EMPTY", function() { return EMPTY; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "empty", function() { return empty; }); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(170); +/** PURE_IMPORTS_START _Observable PURE_IMPORTS_END */ -var none = {}; -function combineLatest() { - var observables = []; - for (var _i = 0; _i < arguments.length; _i++) { - observables[_i] = arguments[_i]; - } - var project = null; - if (typeof observables[observables.length - 1] === 'function') { - project = observables.pop(); - } - if (observables.length === 1 && Object(_util_isArray__WEBPACK_IMPORTED_MODULE_0__["isArray"])(observables[0])) { - observables = observables[0].slice(); - } - return function (source) { return source.lift.call(Object(_observable_from__WEBPACK_IMPORTED_MODULE_2__["from"])([source].concat(observables)), new _observable_combineLatest__WEBPACK_IMPORTED_MODULE_1__["CombineLatestOperator"](project)); }; +var EMPTY = /*@__PURE__*/ new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { return subscriber.complete(); }); +function empty(scheduler) { + return scheduler ? emptyScheduled(scheduler) : EMPTY; } -//# sourceMappingURL=combineLatest.js.map +function emptyScheduled(scheduler) { + return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { return scheduler.schedule(function () { return subscriber.complete(); }); }); +} +//# sourceMappingURL=empty.js.map /***/ }), -/* 218 */ +/* 204 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "from", function() { return from; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(193); -/* harmony import */ var _util_subscribeTo__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(184); -/* harmony import */ var _scheduled_scheduled__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(219); -/** PURE_IMPORTS_START _Observable,_util_subscribeTo,_scheduled_scheduled PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "of", function() { return of; }); +/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(205); +/* harmony import */ var _fromArray__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(206); +/* harmony import */ var _scheduled_scheduleArray__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(208); +/** PURE_IMPORTS_START _util_isScheduler,_fromArray,_scheduled_scheduleArray PURE_IMPORTS_END */ -function from(input, scheduler) { - if (!scheduler) { - if (input instanceof _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"]) { - return input; - } - return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](Object(_util_subscribeTo__WEBPACK_IMPORTED_MODULE_1__["subscribeTo"])(input)); +function of() { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + var scheduler = args[args.length - 1]; + if (Object(_util_isScheduler__WEBPACK_IMPORTED_MODULE_0__["isScheduler"])(scheduler)) { + args.pop(); + return Object(_scheduled_scheduleArray__WEBPACK_IMPORTED_MODULE_2__["scheduleArray"])(args, scheduler); } else { - return Object(_scheduled_scheduled__WEBPACK_IMPORTED_MODULE_2__["scheduled"])(input, scheduler); + return Object(_fromArray__WEBPACK_IMPORTED_MODULE_1__["fromArray"])(args); } } -//# sourceMappingURL=from.js.map +//# sourceMappingURL=of.js.map /***/ }), -/* 219 */ +/* 205 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "scheduled", function() { return scheduled; }); -/* harmony import */ var _scheduleObservable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(220); -/* harmony import */ var _schedulePromise__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(221); -/* harmony import */ var _scheduleArray__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(216); -/* harmony import */ var _scheduleIterable__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(222); -/* harmony import */ var _util_isInteropObservable__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(223); -/* harmony import */ var _util_isPromise__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(192); -/* harmony import */ var _util_isArrayLike__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(191); -/* harmony import */ var _util_isIterable__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(224); -/** PURE_IMPORTS_START _scheduleObservable,_schedulePromise,_scheduleArray,_scheduleIterable,_util_isInteropObservable,_util_isPromise,_util_isArrayLike,_util_isIterable PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isScheduler", function() { return isScheduler; }); +/** PURE_IMPORTS_START PURE_IMPORTS_END */ +function isScheduler(value) { + return value && typeof value.schedule === 'function'; +} +//# sourceMappingURL=isScheduler.js.map + +/***/ }), +/* 206 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "fromArray", function() { return fromArray; }); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(170); +/* harmony import */ var _util_subscribeToArray__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(207); +/* harmony import */ var _scheduled_scheduleArray__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(208); +/** PURE_IMPORTS_START _Observable,_util_subscribeToArray,_scheduled_scheduleArray PURE_IMPORTS_END */ +function fromArray(input, scheduler) { + if (!scheduler) { + return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](Object(_util_subscribeToArray__WEBPACK_IMPORTED_MODULE_1__["subscribeToArray"])(input)); + } + else { + return Object(_scheduled_scheduleArray__WEBPACK_IMPORTED_MODULE_2__["scheduleArray"])(input, scheduler); + } +} +//# sourceMappingURL=fromArray.js.map +/***/ }), +/* 207 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { -function scheduled(input, scheduler) { - if (input != null) { - if (Object(_util_isInteropObservable__WEBPACK_IMPORTED_MODULE_4__["isInteropObservable"])(input)) { - return Object(_scheduleObservable__WEBPACK_IMPORTED_MODULE_0__["scheduleObservable"])(input, scheduler); - } - else if (Object(_util_isPromise__WEBPACK_IMPORTED_MODULE_5__["isPromise"])(input)) { - return Object(_schedulePromise__WEBPACK_IMPORTED_MODULE_1__["schedulePromise"])(input, scheduler); - } - else if (Object(_util_isArrayLike__WEBPACK_IMPORTED_MODULE_6__["isArrayLike"])(input)) { - return Object(_scheduleArray__WEBPACK_IMPORTED_MODULE_2__["scheduleArray"])(input, scheduler); - } - else if (Object(_util_isIterable__WEBPACK_IMPORTED_MODULE_7__["isIterable"])(input) || typeof input === 'string') { - return Object(_scheduleIterable__WEBPACK_IMPORTED_MODULE_3__["scheduleIterable"])(input, scheduler); +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "subscribeToArray", function() { return subscribeToArray; }); +/** PURE_IMPORTS_START PURE_IMPORTS_END */ +var subscribeToArray = function (array) { + return function (subscriber) { + for (var i = 0, len = array.length; i < len && !subscriber.closed; i++) { + subscriber.next(array[i]); } - } - throw new TypeError((input !== null && typeof input || input) + ' is not observable'); -} -//# sourceMappingURL=scheduled.js.map + subscriber.complete(); + }; +}; +//# sourceMappingURL=subscribeToArray.js.map /***/ }), -/* 220 */ +/* 208 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "scheduleObservable", function() { return scheduleObservable; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(193); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "scheduleArray", function() { return scheduleArray; }); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(170); /* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(177); -/* harmony import */ var _symbol_observable__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(190); -/** PURE_IMPORTS_START _Observable,_Subscription,_symbol_observable PURE_IMPORTS_END */ - +/** PURE_IMPORTS_START _Observable,_Subscription PURE_IMPORTS_END */ -function scheduleObservable(input, scheduler) { +function scheduleArray(input, scheduler) { return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { var sub = new _Subscription__WEBPACK_IMPORTED_MODULE_1__["Subscription"](); + var i = 0; sub.add(scheduler.schedule(function () { - var observable = input[_symbol_observable__WEBPACK_IMPORTED_MODULE_2__["observable"]](); - sub.add(observable.subscribe({ - next: function (value) { sub.add(scheduler.schedule(function () { return subscriber.next(value); })); }, - error: function (err) { sub.add(scheduler.schedule(function () { return subscriber.error(err); })); }, - complete: function () { sub.add(scheduler.schedule(function () { return subscriber.complete(); })); }, - })); + if (i === input.length) { + subscriber.complete(); + return; + } + subscriber.next(input[i++]); + if (!subscriber.closed) { + sub.add(this.schedule()); + } })); return sub; }); } -//# sourceMappingURL=scheduleObservable.js.map +//# sourceMappingURL=scheduleArray.js.map /***/ }), -/* 221 */ +/* 209 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "schedulePromise", function() { return schedulePromise; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(193); -/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(177); -/** PURE_IMPORTS_START _Observable,_Subscription PURE_IMPORTS_END */ - +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "throwError", function() { return throwError; }); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(170); +/** PURE_IMPORTS_START _Observable PURE_IMPORTS_END */ -function schedulePromise(input, scheduler) { - return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { - var sub = new _Subscription__WEBPACK_IMPORTED_MODULE_1__["Subscription"](); - sub.add(scheduler.schedule(function () { - return input.then(function (value) { - sub.add(scheduler.schedule(function () { - subscriber.next(value); - sub.add(scheduler.schedule(function () { return subscriber.complete(); })); - })); - }, function (err) { - sub.add(scheduler.schedule(function () { return subscriber.error(err); })); - }); - })); - return sub; - }); +function throwError(error, scheduler) { + if (!scheduler) { + return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { return subscriber.error(error); }); + } + else { + return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { return scheduler.schedule(dispatch, 0, { error: error, subscriber: subscriber }); }); + } } -//# sourceMappingURL=schedulePromise.js.map +function dispatch(_a) { + var error = _a.error, subscriber = _a.subscriber; + subscriber.error(error); +} +//# sourceMappingURL=throwError.js.map /***/ }), -/* 222 */ +/* 210 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "scheduleIterable", function() { return scheduleIterable; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(193); -/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(177); -/* harmony import */ var _symbol_iterator__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(188); -/** PURE_IMPORTS_START _Observable,_Subscription,_symbol_iterator PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "AsyncSubject", function() { return AsyncSubject; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(187); +/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(177); +/** PURE_IMPORTS_START tslib,_Subject,_Subscription PURE_IMPORTS_END */ -function scheduleIterable(input, scheduler) { - if (!input) { - throw new Error('Iterable cannot be null'); +var AsyncSubject = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](AsyncSubject, _super); + function AsyncSubject() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.value = null; + _this.hasNext = false; + _this.hasCompleted = false; + return _this; } - return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { - var sub = new _Subscription__WEBPACK_IMPORTED_MODULE_1__["Subscription"](); - var iterator; - sub.add(function () { - if (iterator && typeof iterator.return === 'function') { - iterator.return(); - } - }); - sub.add(scheduler.schedule(function () { - iterator = input[_symbol_iterator__WEBPACK_IMPORTED_MODULE_2__["iterator"]](); - sub.add(scheduler.schedule(function () { - if (subscriber.closed) { - return; - } - var value; - var done; - try { - var result = iterator.next(); - value = result.value; - done = result.done; - } - catch (err) { - subscriber.error(err); - return; - } - if (done) { - subscriber.complete(); - } - else { - subscriber.next(value); - this.schedule(); - } - })); - })); - return sub; - }); -} -//# sourceMappingURL=scheduleIterable.js.map + AsyncSubject.prototype._subscribe = function (subscriber) { + if (this.hasError) { + subscriber.error(this.thrownError); + return _Subscription__WEBPACK_IMPORTED_MODULE_2__["Subscription"].EMPTY; + } + else if (this.hasCompleted && this.hasNext) { + subscriber.next(this.value); + subscriber.complete(); + return _Subscription__WEBPACK_IMPORTED_MODULE_2__["Subscription"].EMPTY; + } + return _super.prototype._subscribe.call(this, subscriber); + }; + AsyncSubject.prototype.next = function (value) { + if (!this.hasCompleted) { + this.value = value; + this.hasNext = true; + } + }; + AsyncSubject.prototype.error = function (error) { + if (!this.hasCompleted) { + _super.prototype.error.call(this, error); + } + }; + AsyncSubject.prototype.complete = function () { + this.hasCompleted = true; + if (this.hasNext) { + _super.prototype.next.call(this, this.value); + } + _super.prototype.complete.call(this); + }; + return AsyncSubject; +}(_Subject__WEBPACK_IMPORTED_MODULE_1__["Subject"])); + +//# sourceMappingURL=AsyncSubject.js.map /***/ }), -/* 223 */ +/* 211 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isInteropObservable", function() { return isInteropObservable; }); -/* harmony import */ var _symbol_observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(190); -/** PURE_IMPORTS_START _symbol_observable PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "asap", function() { return asap; }); +/* harmony import */ var _AsapAction__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(212); +/* harmony import */ var _AsapScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(214); +/** PURE_IMPORTS_START _AsapAction,_AsapScheduler PURE_IMPORTS_END */ -function isInteropObservable(input) { - return input && typeof input[_symbol_observable__WEBPACK_IMPORTED_MODULE_0__["observable"]] === 'function'; -} -//# sourceMappingURL=isInteropObservable.js.map + +var asap = /*@__PURE__*/ new _AsapScheduler__WEBPACK_IMPORTED_MODULE_1__["AsapScheduler"](_AsapAction__WEBPACK_IMPORTED_MODULE_0__["AsapAction"]); +//# sourceMappingURL=asap.js.map /***/ }), -/* 224 */ +/* 212 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isIterable", function() { return isIterable; }); -/* harmony import */ var _symbol_iterator__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(188); -/** PURE_IMPORTS_START _symbol_iterator PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "AsapAction", function() { return AsapAction; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _util_Immediate__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(213); +/* harmony import */ var _AsyncAction__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(196); +/** PURE_IMPORTS_START tslib,_util_Immediate,_AsyncAction PURE_IMPORTS_END */ -function isIterable(input) { - return input && typeof input[_symbol_iterator__WEBPACK_IMPORTED_MODULE_0__["iterator"]] === 'function'; -} -//# sourceMappingURL=isIterable.js.map + + +var AsapAction = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](AsapAction, _super); + function AsapAction(scheduler, work) { + var _this = _super.call(this, scheduler, work) || this; + _this.scheduler = scheduler; + _this.work = work; + return _this; + } + AsapAction.prototype.requestAsyncId = function (scheduler, id, delay) { + if (delay === void 0) { + delay = 0; + } + if (delay !== null && delay > 0) { + return _super.prototype.requestAsyncId.call(this, scheduler, id, delay); + } + scheduler.actions.push(this); + return scheduler.scheduled || (scheduler.scheduled = _util_Immediate__WEBPACK_IMPORTED_MODULE_1__["Immediate"].setImmediate(scheduler.flush.bind(scheduler, null))); + }; + AsapAction.prototype.recycleAsyncId = function (scheduler, id, delay) { + if (delay === void 0) { + delay = 0; + } + if ((delay !== null && delay > 0) || (delay === null && this.delay > 0)) { + return _super.prototype.recycleAsyncId.call(this, scheduler, id, delay); + } + if (scheduler.actions.length === 0) { + _util_Immediate__WEBPACK_IMPORTED_MODULE_1__["Immediate"].clearImmediate(id); + scheduler.scheduled = undefined; + } + return undefined; + }; + return AsapAction; +}(_AsyncAction__WEBPACK_IMPORTED_MODULE_2__["AsyncAction"])); + +//# sourceMappingURL=AsapAction.js.map /***/ }), -/* 225 */ +/* 213 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "concat", function() { return concat; }); -/* harmony import */ var _observable_concat__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(226); -/** PURE_IMPORTS_START _observable_concat PURE_IMPORTS_END */ - -function concat() { - var observables = []; - for (var _i = 0; _i < arguments.length; _i++) { - observables[_i] = arguments[_i]; +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Immediate", function() { return Immediate; }); +/** PURE_IMPORTS_START PURE_IMPORTS_END */ +var nextHandle = 1; +var tasksByHandle = {}; +function runIfPresent(handle) { + var cb = tasksByHandle[handle]; + if (cb) { + cb(); } - return function (source) { return source.lift.call(_observable_concat__WEBPACK_IMPORTED_MODULE_0__["concat"].apply(void 0, [source].concat(observables))); }; } -//# sourceMappingURL=concat.js.map +var Immediate = { + setImmediate: function (cb) { + var handle = nextHandle++; + tasksByHandle[handle] = cb; + Promise.resolve().then(function () { return runIfPresent(handle); }); + return handle; + }, + clearImmediate: function (handle) { + delete tasksByHandle[handle]; + }, +}; +//# sourceMappingURL=Immediate.js.map /***/ }), -/* 226 */ +/* 214 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "concat", function() { return concat; }); -/* harmony import */ var _of__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(227); -/* harmony import */ var _operators_concatAll__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(228); -/** PURE_IMPORTS_START _of,_operators_concatAll PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "AsapScheduler", function() { return AsapScheduler; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _AsyncScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(199); +/** PURE_IMPORTS_START tslib,_AsyncScheduler PURE_IMPORTS_END */ -function concat() { - var observables = []; - for (var _i = 0; _i < arguments.length; _i++) { - observables[_i] = arguments[_i]; +var AsapScheduler = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](AsapScheduler, _super); + function AsapScheduler() { + return _super !== null && _super.apply(this, arguments) || this; } - return Object(_operators_concatAll__WEBPACK_IMPORTED_MODULE_1__["concatAll"])()(_of__WEBPACK_IMPORTED_MODULE_0__["of"].apply(void 0, observables)); -} -//# sourceMappingURL=concat.js.map + AsapScheduler.prototype.flush = function (action) { + this.active = true; + this.scheduled = undefined; + var actions = this.actions; + var error; + var index = -1; + var count = actions.length; + action = action || actions.shift(); + do { + if (error = action.execute(action.state, action.delay)) { + break; + } + } while (++index < count && (action = actions.shift())); + this.active = false; + if (error) { + while (++index < count && (action = actions.shift())) { + action.unsubscribe(); + } + throw error; + } + }; + return AsapScheduler; +}(_AsyncScheduler__WEBPACK_IMPORTED_MODULE_1__["AsyncScheduler"])); + +//# sourceMappingURL=AsapScheduler.js.map /***/ }), -/* 227 */ +/* 215 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "of", function() { return of; }); -/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(206); -/* harmony import */ var _fromArray__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(215); -/* harmony import */ var _scheduled_scheduleArray__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(216); -/** PURE_IMPORTS_START _util_isScheduler,_fromArray,_scheduled_scheduleArray PURE_IMPORTS_END */ - +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "async", function() { return async; }); +/* harmony import */ var _AsyncAction__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(196); +/* harmony import */ var _AsyncScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(199); +/** PURE_IMPORTS_START _AsyncAction,_AsyncScheduler PURE_IMPORTS_END */ -function of() { - var args = []; - for (var _i = 0; _i < arguments.length; _i++) { - args[_i] = arguments[_i]; - } - var scheduler = args[args.length - 1]; - if (Object(_util_isScheduler__WEBPACK_IMPORTED_MODULE_0__["isScheduler"])(scheduler)) { - args.pop(); - return Object(_scheduled_scheduleArray__WEBPACK_IMPORTED_MODULE_2__["scheduleArray"])(args, scheduler); - } - else { - return Object(_fromArray__WEBPACK_IMPORTED_MODULE_1__["fromArray"])(args); - } -} -//# sourceMappingURL=of.js.map +var async = /*@__PURE__*/ new _AsyncScheduler__WEBPACK_IMPORTED_MODULE_1__["AsyncScheduler"](_AsyncAction__WEBPACK_IMPORTED_MODULE_0__["AsyncAction"]); +//# sourceMappingURL=async.js.map /***/ }), -/* 228 */ +/* 216 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "concatAll", function() { return concatAll; }); -/* harmony import */ var _mergeAll__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(229); -/** PURE_IMPORTS_START _mergeAll PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "animationFrame", function() { return animationFrame; }); +/* harmony import */ var _AnimationFrameAction__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(217); +/* harmony import */ var _AnimationFrameScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(218); +/** PURE_IMPORTS_START _AnimationFrameAction,_AnimationFrameScheduler PURE_IMPORTS_END */ -function concatAll() { - return Object(_mergeAll__WEBPACK_IMPORTED_MODULE_0__["mergeAll"])(1); -} -//# sourceMappingURL=concatAll.js.map + +var animationFrame = /*@__PURE__*/ new _AnimationFrameScheduler__WEBPACK_IMPORTED_MODULE_1__["AnimationFrameScheduler"](_AnimationFrameAction__WEBPACK_IMPORTED_MODULE_0__["AnimationFrameAction"]); +//# sourceMappingURL=animationFrame.js.map /***/ }), -/* 229 */ +/* 217 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "mergeAll", function() { return mergeAll; }); -/* harmony import */ var _mergeMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(230); -/* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(232); -/** PURE_IMPORTS_START _mergeMap,_util_identity PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "AnimationFrameAction", function() { return AnimationFrameAction; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _AsyncAction__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(196); +/** PURE_IMPORTS_START tslib,_AsyncAction PURE_IMPORTS_END */ -function mergeAll(concurrent) { - if (concurrent === void 0) { - concurrent = Number.POSITIVE_INFINITY; +var AnimationFrameAction = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](AnimationFrameAction, _super); + function AnimationFrameAction(scheduler, work) { + var _this = _super.call(this, scheduler, work) || this; + _this.scheduler = scheduler; + _this.work = work; + return _this; } - return Object(_mergeMap__WEBPACK_IMPORTED_MODULE_0__["mergeMap"])(_util_identity__WEBPACK_IMPORTED_MODULE_1__["identity"], concurrent); -} -//# sourceMappingURL=mergeAll.js.map + AnimationFrameAction.prototype.requestAsyncId = function (scheduler, id, delay) { + if (delay === void 0) { + delay = 0; + } + if (delay !== null && delay > 0) { + return _super.prototype.requestAsyncId.call(this, scheduler, id, delay); + } + scheduler.actions.push(this); + return scheduler.scheduled || (scheduler.scheduled = requestAnimationFrame(function () { return scheduler.flush(null); })); + }; + AnimationFrameAction.prototype.recycleAsyncId = function (scheduler, id, delay) { + if (delay === void 0) { + delay = 0; + } + if ((delay !== null && delay > 0) || (delay === null && this.delay > 0)) { + return _super.prototype.recycleAsyncId.call(this, scheduler, id, delay); + } + if (scheduler.actions.length === 0) { + cancelAnimationFrame(id); + scheduler.scheduled = undefined; + } + return undefined; + }; + return AnimationFrameAction; +}(_AsyncAction__WEBPACK_IMPORTED_MODULE_1__["AsyncAction"])); + +//# sourceMappingURL=AnimationFrameAction.js.map /***/ }), -/* 230 */ +/* 218 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "mergeMap", function() { return mergeMap; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MergeMapOperator", function() { return MergeMapOperator; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MergeMapSubscriber", function() { return MergeMapSubscriber; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "AnimationFrameScheduler", function() { return AnimationFrameScheduler; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(182); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(171); -/* harmony import */ var _InnerSubscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(183); -/* harmony import */ var _map__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(231); -/* harmony import */ var _observable_from__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(218); -/** PURE_IMPORTS_START tslib,_util_subscribeToResult,_OuterSubscriber,_InnerSubscriber,_map,_observable_from PURE_IMPORTS_END */ +/* harmony import */ var _AsyncScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(199); +/** PURE_IMPORTS_START tslib,_AsyncScheduler PURE_IMPORTS_END */ + + +var AnimationFrameScheduler = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](AnimationFrameScheduler, _super); + function AnimationFrameScheduler() { + return _super !== null && _super.apply(this, arguments) || this; + } + AnimationFrameScheduler.prototype.flush = function (action) { + this.active = true; + this.scheduled = undefined; + var actions = this.actions; + var error; + var index = -1; + var count = actions.length; + action = action || actions.shift(); + do { + if (error = action.execute(action.state, action.delay)) { + break; + } + } while (++index < count && (action = actions.shift())); + this.active = false; + if (error) { + while (++index < count && (action = actions.shift())) { + action.unsubscribe(); + } + throw error; + } + }; + return AnimationFrameScheduler; +}(_AsyncScheduler__WEBPACK_IMPORTED_MODULE_1__["AsyncScheduler"])); +//# sourceMappingURL=AnimationFrameScheduler.js.map +/***/ }), +/* 219 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "VirtualTimeScheduler", function() { return VirtualTimeScheduler; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "VirtualAction", function() { return VirtualAction; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _AsyncAction__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(196); +/* harmony import */ var _AsyncScheduler__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(199); +/** PURE_IMPORTS_START tslib,_AsyncAction,_AsyncScheduler PURE_IMPORTS_END */ -function mergeMap(project, resultSelector, concurrent) { - if (concurrent === void 0) { - concurrent = Number.POSITIVE_INFINITY; - } - if (typeof resultSelector === 'function') { - return function (source) { return source.pipe(mergeMap(function (a, i) { return Object(_observable_from__WEBPACK_IMPORTED_MODULE_5__["from"])(project(a, i)).pipe(Object(_map__WEBPACK_IMPORTED_MODULE_4__["map"])(function (b, ii) { return resultSelector(a, b, i, ii); })); }, concurrent)); }; - } - else if (typeof resultSelector === 'number') { - concurrent = resultSelector; - } - return function (source) { return source.lift(new MergeMapOperator(project, concurrent)); }; -} -var MergeMapOperator = /*@__PURE__*/ (function () { - function MergeMapOperator(project, concurrent) { - if (concurrent === void 0) { - concurrent = Number.POSITIVE_INFINITY; + +var VirtualTimeScheduler = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](VirtualTimeScheduler, _super); + function VirtualTimeScheduler(SchedulerAction, maxFrames) { + if (SchedulerAction === void 0) { + SchedulerAction = VirtualAction; } - this.project = project; - this.concurrent = concurrent; + if (maxFrames === void 0) { + maxFrames = Number.POSITIVE_INFINITY; + } + var _this = _super.call(this, SchedulerAction, function () { return _this.frame; }) || this; + _this.maxFrames = maxFrames; + _this.frame = 0; + _this.index = -1; + return _this; } - MergeMapOperator.prototype.call = function (observer, source) { - return source.subscribe(new MergeMapSubscriber(observer, this.project, this.concurrent)); + VirtualTimeScheduler.prototype.flush = function () { + var _a = this, actions = _a.actions, maxFrames = _a.maxFrames; + var error, action; + while ((action = actions[0]) && action.delay <= maxFrames) { + actions.shift(); + this.frame = action.delay; + if (error = action.execute(action.state, action.delay)) { + break; + } + } + if (error) { + while (action = actions.shift()) { + action.unsubscribe(); + } + throw error; + } }; - return MergeMapOperator; -}()); + VirtualTimeScheduler.frameTimeFactor = 10; + return VirtualTimeScheduler; +}(_AsyncScheduler__WEBPACK_IMPORTED_MODULE_2__["AsyncScheduler"])); -var MergeMapSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](MergeMapSubscriber, _super); - function MergeMapSubscriber(destination, project, concurrent) { - if (concurrent === void 0) { - concurrent = Number.POSITIVE_INFINITY; +var VirtualAction = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](VirtualAction, _super); + function VirtualAction(scheduler, work, index) { + if (index === void 0) { + index = scheduler.index += 1; } - var _this = _super.call(this, destination) || this; - _this.project = project; - _this.concurrent = concurrent; - _this.hasCompleted = false; - _this.buffer = []; - _this.active = 0; - _this.index = 0; + var _this = _super.call(this, scheduler, work) || this; + _this.scheduler = scheduler; + _this.work = work; + _this.index = index; + _this.active = true; + _this.index = scheduler.index = index; return _this; } - MergeMapSubscriber.prototype._next = function (value) { - if (this.active < this.concurrent) { - this._tryNext(value); + VirtualAction.prototype.schedule = function (state, delay) { + if (delay === void 0) { + delay = 0; } - else { - this.buffer.push(value); + if (!this.id) { + return _super.prototype.schedule.call(this, state, delay); } + this.active = false; + var action = new VirtualAction(this.scheduler, this.work); + this.add(action); + return action.schedule(state, delay); }; - MergeMapSubscriber.prototype._tryNext = function (value) { - var result; - var index = this.index++; - try { - result = this.project(value, index); - } - catch (err) { - this.destination.error(err); - return; + VirtualAction.prototype.requestAsyncId = function (scheduler, id, delay) { + if (delay === void 0) { + delay = 0; } - this.active++; - this._innerSub(result, value, index); - }; - MergeMapSubscriber.prototype._innerSub = function (ish, value, index) { - var innerSubscriber = new _InnerSubscriber__WEBPACK_IMPORTED_MODULE_3__["InnerSubscriber"](this, undefined, undefined); - var destination = this.destination; - destination.add(innerSubscriber); - Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_1__["subscribeToResult"])(this, ish, value, index, innerSubscriber); + this.delay = scheduler.frame + delay; + var actions = scheduler.actions; + actions.push(this); + actions.sort(VirtualAction.sortActions); + return true; }; - MergeMapSubscriber.prototype._complete = function () { - this.hasCompleted = true; - if (this.active === 0 && this.buffer.length === 0) { - this.destination.complete(); + VirtualAction.prototype.recycleAsyncId = function (scheduler, id, delay) { + if (delay === void 0) { + delay = 0; } - this.unsubscribe(); + return undefined; }; - MergeMapSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { - this.destination.next(innerValue); + VirtualAction.prototype._execute = function (state, delay) { + if (this.active === true) { + return _super.prototype._execute.call(this, state, delay); + } }; - MergeMapSubscriber.prototype.notifyComplete = function (innerSub) { - var buffer = this.buffer; - this.remove(innerSub); - this.active--; - if (buffer.length > 0) { - this._next(buffer.shift()); + VirtualAction.sortActions = function (a, b) { + if (a.delay === b.delay) { + if (a.index === b.index) { + return 0; + } + else if (a.index > b.index) { + return 1; + } + else { + return -1; + } } - else if (this.active === 0 && this.hasCompleted) { - this.destination.complete(); + else if (a.delay > b.delay) { + return 1; + } + else { + return -1; } }; - return MergeMapSubscriber; -}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_2__["OuterSubscriber"])); + return VirtualAction; +}(_AsyncAction__WEBPACK_IMPORTED_MODULE_1__["AsyncAction"])); -//# sourceMappingURL=mergeMap.js.map +//# sourceMappingURL=VirtualTimeScheduler.js.map /***/ }), -/* 231 */ +/* 220 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "map", function() { return map; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MapOperator", function() { return MapOperator; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); -/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "identity", function() { return identity; }); +/** PURE_IMPORTS_START PURE_IMPORTS_END */ +function identity(x) { + return x; +} +//# sourceMappingURL=identity.js.map -function map(project, thisArg) { - return function mapOperation(source) { +/***/ }), +/* 221 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isObservable", function() { return isObservable; }); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(170); +/** PURE_IMPORTS_START _Observable PURE_IMPORTS_END */ + +function isObservable(obj) { + return !!obj && (obj instanceof _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"] || (typeof obj.lift === 'function' && typeof obj.subscribe === 'function')); +} +//# sourceMappingURL=isObservable.js.map + + +/***/ }), +/* 222 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ArgumentOutOfRangeError", function() { return ArgumentOutOfRangeError; }); +/** PURE_IMPORTS_START PURE_IMPORTS_END */ +var ArgumentOutOfRangeErrorImpl = /*@__PURE__*/ (function () { + function ArgumentOutOfRangeErrorImpl() { + Error.call(this); + this.message = 'argument out of range'; + this.name = 'ArgumentOutOfRangeError'; + return this; + } + ArgumentOutOfRangeErrorImpl.prototype = /*@__PURE__*/ Object.create(Error.prototype); + return ArgumentOutOfRangeErrorImpl; +})(); +var ArgumentOutOfRangeError = ArgumentOutOfRangeErrorImpl; +//# sourceMappingURL=ArgumentOutOfRangeError.js.map + + +/***/ }), +/* 223 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "EmptyError", function() { return EmptyError; }); +/** PURE_IMPORTS_START PURE_IMPORTS_END */ +var EmptyErrorImpl = /*@__PURE__*/ (function () { + function EmptyErrorImpl() { + Error.call(this); + this.message = 'no elements in sequence'; + this.name = 'EmptyError'; + return this; + } + EmptyErrorImpl.prototype = /*@__PURE__*/ Object.create(Error.prototype); + return EmptyErrorImpl; +})(); +var EmptyError = EmptyErrorImpl; +//# sourceMappingURL=EmptyError.js.map + + +/***/ }), +/* 224 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TimeoutError", function() { return TimeoutError; }); +/** PURE_IMPORTS_START PURE_IMPORTS_END */ +var TimeoutErrorImpl = /*@__PURE__*/ (function () { + function TimeoutErrorImpl() { + Error.call(this); + this.message = 'Timeout has occurred'; + this.name = 'TimeoutError'; + return this; + } + TimeoutErrorImpl.prototype = /*@__PURE__*/ Object.create(Error.prototype); + return TimeoutErrorImpl; +})(); +var TimeoutError = TimeoutErrorImpl; +//# sourceMappingURL=TimeoutError.js.map + + +/***/ }), +/* 225 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "bindCallback", function() { return bindCallback; }); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(170); +/* harmony import */ var _AsyncSubject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(210); +/* harmony import */ var _operators_map__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(226); +/* harmony import */ var _util_canReportError__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(171); +/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(178); +/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(205); +/** PURE_IMPORTS_START _Observable,_AsyncSubject,_operators_map,_util_canReportError,_util_isArray,_util_isScheduler PURE_IMPORTS_END */ + + + + + + +function bindCallback(callbackFunc, resultSelector, scheduler) { + if (resultSelector) { + if (Object(_util_isScheduler__WEBPACK_IMPORTED_MODULE_5__["isScheduler"])(resultSelector)) { + scheduler = resultSelector; + } + else { + return function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + return bindCallback(callbackFunc, scheduler).apply(void 0, args).pipe(Object(_operators_map__WEBPACK_IMPORTED_MODULE_2__["map"])(function (args) { return Object(_util_isArray__WEBPACK_IMPORTED_MODULE_4__["isArray"])(args) ? resultSelector.apply(void 0, args) : resultSelector(args); })); + }; + } + } + return function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + var context = this; + var subject; + var params = { + context: context, + subject: subject, + callbackFunc: callbackFunc, + scheduler: scheduler, + }; + return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { + if (!scheduler) { + if (!subject) { + subject = new _AsyncSubject__WEBPACK_IMPORTED_MODULE_1__["AsyncSubject"](); + var handler = function () { + var innerArgs = []; + for (var _i = 0; _i < arguments.length; _i++) { + innerArgs[_i] = arguments[_i]; + } + subject.next(innerArgs.length <= 1 ? innerArgs[0] : innerArgs); + subject.complete(); + }; + try { + callbackFunc.apply(context, args.concat([handler])); + } + catch (err) { + if (Object(_util_canReportError__WEBPACK_IMPORTED_MODULE_3__["canReportError"])(subject)) { + subject.error(err); + } + else { + console.warn(err); + } + } + } + return subject.subscribe(subscriber); + } + else { + var state = { + args: args, subscriber: subscriber, params: params, + }; + return scheduler.schedule(dispatch, 0, state); + } + }); + }; +} +function dispatch(state) { + var _this = this; + var self = this; + var args = state.args, subscriber = state.subscriber, params = state.params; + var callbackFunc = params.callbackFunc, context = params.context, scheduler = params.scheduler; + var subject = params.subject; + if (!subject) { + subject = params.subject = new _AsyncSubject__WEBPACK_IMPORTED_MODULE_1__["AsyncSubject"](); + var handler = function () { + var innerArgs = []; + for (var _i = 0; _i < arguments.length; _i++) { + innerArgs[_i] = arguments[_i]; + } + var value = innerArgs.length <= 1 ? innerArgs[0] : innerArgs; + _this.add(scheduler.schedule(dispatchNext, 0, { value: value, subject: subject })); + }; + try { + callbackFunc.apply(context, args.concat([handler])); + } + catch (err) { + subject.error(err); + } + } + this.add(subject.subscribe(subscriber)); +} +function dispatchNext(state) { + var value = state.value, subject = state.subject; + subject.next(value); + subject.complete(); +} +function dispatchError(state) { + var err = state.err, subject = state.subject; + subject.error(err); +} +//# sourceMappingURL=bindCallback.js.map + + +/***/ }), +/* 226 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "map", function() { return map; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MapOperator", function() { return MapOperator; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); +/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ + + +function map(project, thisArg) { + return function mapOperation(source) { if (typeof project !== 'function') { throw new TypeError('argument is not a function. Are you looking for `mapTo()`?'); } @@ -25230,1041 +25225,1045 @@ var MapSubscriber = /*@__PURE__*/ (function (_super) { /***/ }), -/* 232 */ +/* 227 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "identity", function() { return identity; }); -/** PURE_IMPORTS_START PURE_IMPORTS_END */ -function identity(x) { - return x; -} -//# sourceMappingURL=identity.js.map +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "bindNodeCallback", function() { return bindNodeCallback; }); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(170); +/* harmony import */ var _AsyncSubject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(210); +/* harmony import */ var _operators_map__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(226); +/* harmony import */ var _util_canReportError__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(171); +/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(205); +/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(178); +/** PURE_IMPORTS_START _Observable,_AsyncSubject,_operators_map,_util_canReportError,_util_isScheduler,_util_isArray PURE_IMPORTS_END */ -/***/ }), -/* 233 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "concatMap", function() { return concatMap; }); -/* harmony import */ var _mergeMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(230); -/** PURE_IMPORTS_START _mergeMap PURE_IMPORTS_END */ -function concatMap(project, resultSelector) { - return Object(_mergeMap__WEBPACK_IMPORTED_MODULE_0__["mergeMap"])(project, resultSelector, 1); + + +function bindNodeCallback(callbackFunc, resultSelector, scheduler) { + if (resultSelector) { + if (Object(_util_isScheduler__WEBPACK_IMPORTED_MODULE_4__["isScheduler"])(resultSelector)) { + scheduler = resultSelector; + } + else { + return function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + return bindNodeCallback(callbackFunc, scheduler).apply(void 0, args).pipe(Object(_operators_map__WEBPACK_IMPORTED_MODULE_2__["map"])(function (args) { return Object(_util_isArray__WEBPACK_IMPORTED_MODULE_5__["isArray"])(args) ? resultSelector.apply(void 0, args) : resultSelector(args); })); + }; + } + } + return function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + var params = { + subject: undefined, + args: args, + callbackFunc: callbackFunc, + scheduler: scheduler, + context: this, + }; + return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { + var context = params.context; + var subject = params.subject; + if (!scheduler) { + if (!subject) { + subject = params.subject = new _AsyncSubject__WEBPACK_IMPORTED_MODULE_1__["AsyncSubject"](); + var handler = function () { + var innerArgs = []; + for (var _i = 0; _i < arguments.length; _i++) { + innerArgs[_i] = arguments[_i]; + } + var err = innerArgs.shift(); + if (err) { + subject.error(err); + return; + } + subject.next(innerArgs.length <= 1 ? innerArgs[0] : innerArgs); + subject.complete(); + }; + try { + callbackFunc.apply(context, args.concat([handler])); + } + catch (err) { + if (Object(_util_canReportError__WEBPACK_IMPORTED_MODULE_3__["canReportError"])(subject)) { + subject.error(err); + } + else { + console.warn(err); + } + } + } + return subject.subscribe(subscriber); + } + else { + return scheduler.schedule(dispatch, 0, { params: params, subscriber: subscriber, context: context }); + } + }); + }; } -//# sourceMappingURL=concatMap.js.map +function dispatch(state) { + var _this = this; + var params = state.params, subscriber = state.subscriber, context = state.context; + var callbackFunc = params.callbackFunc, args = params.args, scheduler = params.scheduler; + var subject = params.subject; + if (!subject) { + subject = params.subject = new _AsyncSubject__WEBPACK_IMPORTED_MODULE_1__["AsyncSubject"](); + var handler = function () { + var innerArgs = []; + for (var _i = 0; _i < arguments.length; _i++) { + innerArgs[_i] = arguments[_i]; + } + var err = innerArgs.shift(); + if (err) { + _this.add(scheduler.schedule(dispatchError, 0, { err: err, subject: subject })); + } + else { + var value = innerArgs.length <= 1 ? innerArgs[0] : innerArgs; + _this.add(scheduler.schedule(dispatchNext, 0, { value: value, subject: subject })); + } + }; + try { + callbackFunc.apply(context, args.concat([handler])); + } + catch (err) { + this.add(scheduler.schedule(dispatchError, 0, { err: err, subject: subject })); + } + } + this.add(subject.subscribe(subscriber)); +} +function dispatchNext(arg) { + var value = arg.value, subject = arg.subject; + subject.next(value); + subject.complete(); +} +function dispatchError(arg) { + var err = arg.err, subject = arg.subject; + subject.error(err); +} +//# sourceMappingURL=bindNodeCallback.js.map /***/ }), -/* 234 */ +/* 228 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "concatMapTo", function() { return concatMapTo; }); -/* harmony import */ var _concatMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(233); -/** PURE_IMPORTS_START _concatMap PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "combineLatest", function() { return combineLatest; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "CombineLatestOperator", function() { return CombineLatestOperator; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "CombineLatestSubscriber", function() { return CombineLatestSubscriber; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(205); +/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(178); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(229); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(230); +/* harmony import */ var _fromArray__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(206); +/** PURE_IMPORTS_START tslib,_util_isScheduler,_util_isArray,_OuterSubscriber,_util_subscribeToResult,_fromArray PURE_IMPORTS_END */ -function concatMapTo(innerObservable, resultSelector) { - return Object(_concatMap__WEBPACK_IMPORTED_MODULE_0__["concatMap"])(function () { return innerObservable; }, resultSelector); -} -//# sourceMappingURL=concatMapTo.js.map -/***/ }), -/* 235 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "count", function() { return count; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); -/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ -function count(predicate) { - return function (source) { return source.lift(new CountOperator(predicate, source)); }; +var NONE = {}; +function combineLatest() { + var observables = []; + for (var _i = 0; _i < arguments.length; _i++) { + observables[_i] = arguments[_i]; + } + var resultSelector = null; + var scheduler = null; + if (Object(_util_isScheduler__WEBPACK_IMPORTED_MODULE_1__["isScheduler"])(observables[observables.length - 1])) { + scheduler = observables.pop(); + } + if (typeof observables[observables.length - 1] === 'function') { + resultSelector = observables.pop(); + } + if (observables.length === 1 && Object(_util_isArray__WEBPACK_IMPORTED_MODULE_2__["isArray"])(observables[0])) { + observables = observables[0]; + } + return Object(_fromArray__WEBPACK_IMPORTED_MODULE_5__["fromArray"])(observables, scheduler).lift(new CombineLatestOperator(resultSelector)); } -var CountOperator = /*@__PURE__*/ (function () { - function CountOperator(predicate, source) { - this.predicate = predicate; - this.source = source; +var CombineLatestOperator = /*@__PURE__*/ (function () { + function CombineLatestOperator(resultSelector) { + this.resultSelector = resultSelector; } - CountOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new CountSubscriber(subscriber, this.predicate, this.source)); + CombineLatestOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new CombineLatestSubscriber(subscriber, this.resultSelector)); }; - return CountOperator; + return CombineLatestOperator; }()); -var CountSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](CountSubscriber, _super); - function CountSubscriber(destination, predicate, source) { + +var CombineLatestSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](CombineLatestSubscriber, _super); + function CombineLatestSubscriber(destination, resultSelector) { var _this = _super.call(this, destination) || this; - _this.predicate = predicate; - _this.source = source; - _this.count = 0; - _this.index = 0; + _this.resultSelector = resultSelector; + _this.active = 0; + _this.values = []; + _this.observables = []; return _this; } - CountSubscriber.prototype._next = function (value) { - if (this.predicate) { - this._tryPredicate(value); + CombineLatestSubscriber.prototype._next = function (observable) { + this.values.push(NONE); + this.observables.push(observable); + }; + CombineLatestSubscriber.prototype._complete = function () { + var observables = this.observables; + var len = observables.length; + if (len === 0) { + this.destination.complete(); } else { - this.count++; + this.active = len; + this.toRespond = len; + for (var i = 0; i < len; i++) { + var observable = observables[i]; + this.add(Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__["subscribeToResult"])(this, observable, observable, i)); + } } }; - CountSubscriber.prototype._tryPredicate = function (value) { + CombineLatestSubscriber.prototype.notifyComplete = function (unused) { + if ((this.active -= 1) === 0) { + this.destination.complete(); + } + }; + CombineLatestSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { + var values = this.values; + var oldVal = values[outerIndex]; + var toRespond = !this.toRespond + ? 0 + : oldVal === NONE ? --this.toRespond : this.toRespond; + values[outerIndex] = innerValue; + if (toRespond === 0) { + if (this.resultSelector) { + this._tryResultSelector(values); + } + else { + this.destination.next(values.slice()); + } + } + }; + CombineLatestSubscriber.prototype._tryResultSelector = function (values) { var result; try { - result = this.predicate(value, this.index++, this.source); + result = this.resultSelector.apply(this, values); } catch (err) { this.destination.error(err); return; } - if (result) { - this.count++; - } + this.destination.next(result); }; - CountSubscriber.prototype._complete = function () { - this.destination.next(this.count); + return CombineLatestSubscriber; +}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__["OuterSubscriber"])); + +//# sourceMappingURL=combineLatest.js.map + + +/***/ }), +/* 229 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "OuterSubscriber", function() { return OuterSubscriber; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); +/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ + + +var OuterSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](OuterSubscriber, _super); + function OuterSubscriber() { + return _super !== null && _super.apply(this, arguments) || this; + } + OuterSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { + this.destination.next(innerValue); + }; + OuterSubscriber.prototype.notifyError = function (error, innerSub) { + this.destination.error(error); + }; + OuterSubscriber.prototype.notifyComplete = function (innerSub) { this.destination.complete(); }; - return CountSubscriber; + return OuterSubscriber; }(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); -//# sourceMappingURL=count.js.map + +//# sourceMappingURL=OuterSubscriber.js.map /***/ }), -/* 236 */ +/* 230 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "debounce", function() { return debounce; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(171); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(182); -/** PURE_IMPORTS_START tslib,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "subscribeToResult", function() { return subscribeToResult; }); +/* harmony import */ var _InnerSubscriber__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(231); +/* harmony import */ var _subscribeTo__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(232); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(170); +/** PURE_IMPORTS_START _InnerSubscriber,_subscribeTo,_Observable PURE_IMPORTS_END */ -function debounce(durationSelector) { - return function (source) { return source.lift(new DebounceOperator(durationSelector)); }; -} -var DebounceOperator = /*@__PURE__*/ (function () { - function DebounceOperator(durationSelector) { - this.durationSelector = durationSelector; +function subscribeToResult(outerSubscriber, result, outerValue, outerIndex, destination) { + if (destination === void 0) { + destination = new _InnerSubscriber__WEBPACK_IMPORTED_MODULE_0__["InnerSubscriber"](outerSubscriber, outerValue, outerIndex); } - DebounceOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new DebounceSubscriber(subscriber, this.durationSelector)); - }; - return DebounceOperator; -}()); -var DebounceSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](DebounceSubscriber, _super); - function DebounceSubscriber(destination, durationSelector) { - var _this = _super.call(this, destination) || this; - _this.durationSelector = durationSelector; - _this.hasValue = false; - _this.durationSubscription = null; - return _this; + if (destination.closed) { + return undefined; } - DebounceSubscriber.prototype._next = function (value) { - try { - var result = this.durationSelector.call(this, value); - if (result) { - this._tryNext(value, result); - } - } - catch (err) { - this.destination.error(err); - } - }; - DebounceSubscriber.prototype._complete = function () { - this.emitValue(); - this.destination.complete(); - }; - DebounceSubscriber.prototype._tryNext = function (value, duration) { - var subscription = this.durationSubscription; - this.value = value; - this.hasValue = true; - if (subscription) { - subscription.unsubscribe(); - this.remove(subscription); - } - subscription = Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__["subscribeToResult"])(this, duration); - if (subscription && !subscription.closed) { - this.add(this.durationSubscription = subscription); - } - }; - DebounceSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { - this.emitValue(); - }; - DebounceSubscriber.prototype.notifyComplete = function () { - this.emitValue(); - }; - DebounceSubscriber.prototype.emitValue = function () { - if (this.hasValue) { - var value = this.value; - var subscription = this.durationSubscription; - if (subscription) { - this.durationSubscription = null; - subscription.unsubscribe(); - this.remove(subscription); - } - this.value = null; - this.hasValue = false; - _super.prototype._next.call(this, value); - } - }; - return DebounceSubscriber; -}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__["OuterSubscriber"])); -//# sourceMappingURL=debounce.js.map + if (result instanceof _Observable__WEBPACK_IMPORTED_MODULE_2__["Observable"]) { + return result.subscribe(destination); + } + return Object(_subscribeTo__WEBPACK_IMPORTED_MODULE_1__["subscribeTo"])(result)(destination); +} +//# sourceMappingURL=subscribeToResult.js.map /***/ }), -/* 237 */ +/* 231 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "debounceTime", function() { return debounceTime; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "InnerSubscriber", function() { return InnerSubscriber; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); /* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); -/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(199); -/** PURE_IMPORTS_START tslib,_Subscriber,_scheduler_async PURE_IMPORTS_END */ - +/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ -function debounceTime(dueTime, scheduler) { - if (scheduler === void 0) { - scheduler = _scheduler_async__WEBPACK_IMPORTED_MODULE_2__["async"]; - } - return function (source) { return source.lift(new DebounceTimeOperator(dueTime, scheduler)); }; -} -var DebounceTimeOperator = /*@__PURE__*/ (function () { - function DebounceTimeOperator(dueTime, scheduler) { - this.dueTime = dueTime; - this.scheduler = scheduler; - } - DebounceTimeOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new DebounceTimeSubscriber(subscriber, this.dueTime, this.scheduler)); - }; - return DebounceTimeOperator; -}()); -var DebounceTimeSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](DebounceTimeSubscriber, _super); - function DebounceTimeSubscriber(destination, dueTime, scheduler) { - var _this = _super.call(this, destination) || this; - _this.dueTime = dueTime; - _this.scheduler = scheduler; - _this.debouncedSubscription = null; - _this.lastValue = null; - _this.hasValue = false; +var InnerSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](InnerSubscriber, _super); + function InnerSubscriber(parent, outerValue, outerIndex) { + var _this = _super.call(this) || this; + _this.parent = parent; + _this.outerValue = outerValue; + _this.outerIndex = outerIndex; + _this.index = 0; return _this; } - DebounceTimeSubscriber.prototype._next = function (value) { - this.clearDebounce(); - this.lastValue = value; - this.hasValue = true; - this.add(this.debouncedSubscription = this.scheduler.schedule(dispatchNext, this.dueTime, this)); - }; - DebounceTimeSubscriber.prototype._complete = function () { - this.debouncedNext(); - this.destination.complete(); + InnerSubscriber.prototype._next = function (value) { + this.parent.notifyNext(this.outerValue, value, this.outerIndex, this.index++, this); }; - DebounceTimeSubscriber.prototype.debouncedNext = function () { - this.clearDebounce(); - if (this.hasValue) { - var lastValue = this.lastValue; - this.lastValue = null; - this.hasValue = false; - this.destination.next(lastValue); - } + InnerSubscriber.prototype._error = function (error) { + this.parent.notifyError(error, this); + this.unsubscribe(); }; - DebounceTimeSubscriber.prototype.clearDebounce = function () { - var debouncedSubscription = this.debouncedSubscription; - if (debouncedSubscription !== null) { - this.remove(debouncedSubscription); - debouncedSubscription.unsubscribe(); - this.debouncedSubscription = null; - } + InnerSubscriber.prototype._complete = function () { + this.parent.notifyComplete(this); + this.unsubscribe(); }; - return DebounceTimeSubscriber; + return InnerSubscriber; }(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); -function dispatchNext(subscriber) { - subscriber.debouncedNext(); -} -//# sourceMappingURL=debounceTime.js.map + +//# sourceMappingURL=InnerSubscriber.js.map /***/ }), -/* 238 */ +/* 232 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "defaultIfEmpty", function() { return defaultIfEmpty; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); -/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "subscribeTo", function() { return subscribeTo; }); +/* harmony import */ var _subscribeToArray__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(207); +/* harmony import */ var _subscribeToPromise__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(233); +/* harmony import */ var _subscribeToIterable__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(234); +/* harmony import */ var _subscribeToObservable__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(236); +/* harmony import */ var _isArrayLike__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(237); +/* harmony import */ var _isPromise__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(238); +/* harmony import */ var _isObject__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(179); +/* harmony import */ var _symbol_iterator__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(235); +/* harmony import */ var _symbol_observable__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(183); +/** PURE_IMPORTS_START _subscribeToArray,_subscribeToPromise,_subscribeToIterable,_subscribeToObservable,_isArrayLike,_isPromise,_isObject,_symbol_iterator,_symbol_observable PURE_IMPORTS_END */ -function defaultIfEmpty(defaultValue) { - if (defaultValue === void 0) { - defaultValue = null; + + + + + + + +var subscribeTo = function (result) { + if (!!result && typeof result[_symbol_observable__WEBPACK_IMPORTED_MODULE_8__["observable"]] === 'function') { + return Object(_subscribeToObservable__WEBPACK_IMPORTED_MODULE_3__["subscribeToObservable"])(result); } - return function (source) { return source.lift(new DefaultIfEmptyOperator(defaultValue)); }; -} -var DefaultIfEmptyOperator = /*@__PURE__*/ (function () { - function DefaultIfEmptyOperator(defaultValue) { - this.defaultValue = defaultValue; + else if (Object(_isArrayLike__WEBPACK_IMPORTED_MODULE_4__["isArrayLike"])(result)) { + return Object(_subscribeToArray__WEBPACK_IMPORTED_MODULE_0__["subscribeToArray"])(result); } - DefaultIfEmptyOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new DefaultIfEmptySubscriber(subscriber, this.defaultValue)); - }; - return DefaultIfEmptyOperator; -}()); -var DefaultIfEmptySubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](DefaultIfEmptySubscriber, _super); - function DefaultIfEmptySubscriber(destination, defaultValue) { - var _this = _super.call(this, destination) || this; - _this.defaultValue = defaultValue; - _this.isEmpty = true; - return _this; + else if (Object(_isPromise__WEBPACK_IMPORTED_MODULE_5__["isPromise"])(result)) { + return Object(_subscribeToPromise__WEBPACK_IMPORTED_MODULE_1__["subscribeToPromise"])(result); } - DefaultIfEmptySubscriber.prototype._next = function (value) { - this.isEmpty = false; - this.destination.next(value); - }; - DefaultIfEmptySubscriber.prototype._complete = function () { - if (this.isEmpty) { - this.destination.next(this.defaultValue); - } - this.destination.complete(); - }; - return DefaultIfEmptySubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); -//# sourceMappingURL=defaultIfEmpty.js.map + else if (!!result && typeof result[_symbol_iterator__WEBPACK_IMPORTED_MODULE_7__["iterator"]] === 'function') { + return Object(_subscribeToIterable__WEBPACK_IMPORTED_MODULE_2__["subscribeToIterable"])(result); + } + else { + var value = Object(_isObject__WEBPACK_IMPORTED_MODULE_6__["isObject"])(result) ? 'an invalid object' : "'" + result + "'"; + var msg = "You provided " + value + " where a stream was expected." + + ' You can provide an Observable, Promise, Array, or Iterable.'; + throw new TypeError(msg); + } +}; +//# sourceMappingURL=subscribeTo.js.map /***/ }), -/* 239 */ +/* 233 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "delay", function() { return delay; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(199); -/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(240); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(172); -/* harmony import */ var _Notification__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(241); -/** PURE_IMPORTS_START tslib,_scheduler_async,_util_isDate,_Subscriber,_Notification PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "subscribeToPromise", function() { return subscribeToPromise; }); +/* harmony import */ var _hostReportError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(176); +/** PURE_IMPORTS_START _hostReportError PURE_IMPORTS_END */ +var subscribeToPromise = function (promise) { + return function (subscriber) { + promise.then(function (value) { + if (!subscriber.closed) { + subscriber.next(value); + subscriber.complete(); + } + }, function (err) { return subscriber.error(err); }) + .then(null, _hostReportError__WEBPACK_IMPORTED_MODULE_0__["hostReportError"]); + return subscriber; + }; +}; +//# sourceMappingURL=subscribeToPromise.js.map +/***/ }), +/* 234 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "subscribeToIterable", function() { return subscribeToIterable; }); +/* harmony import */ var _symbol_iterator__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(235); +/** PURE_IMPORTS_START _symbol_iterator PURE_IMPORTS_END */ -function delay(delay, scheduler) { - if (scheduler === void 0) { - scheduler = _scheduler_async__WEBPACK_IMPORTED_MODULE_1__["async"]; - } - var absoluteDelay = Object(_util_isDate__WEBPACK_IMPORTED_MODULE_2__["isDate"])(delay); - var delayFor = absoluteDelay ? (+delay - scheduler.now()) : Math.abs(delay); - return function (source) { return source.lift(new DelayOperator(delayFor, scheduler)); }; -} -var DelayOperator = /*@__PURE__*/ (function () { - function DelayOperator(delay, scheduler) { - this.delay = delay; - this.scheduler = scheduler; - } - DelayOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new DelaySubscriber(subscriber, this.delay, this.scheduler)); - }; - return DelayOperator; -}()); -var DelaySubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](DelaySubscriber, _super); - function DelaySubscriber(destination, delay, scheduler) { - var _this = _super.call(this, destination) || this; - _this.delay = delay; - _this.scheduler = scheduler; - _this.queue = []; - _this.active = false; - _this.errored = false; - return _this; - } - DelaySubscriber.dispatch = function (state) { - var source = state.source; - var queue = source.queue; - var scheduler = state.scheduler; - var destination = state.destination; - while (queue.length > 0 && (queue[0].time - scheduler.now()) <= 0) { - queue.shift().notification.observe(destination); - } - if (queue.length > 0) { - var delay_1 = Math.max(0, queue[0].time - scheduler.now()); - this.schedule(state, delay_1); - } - else { - this.unsubscribe(); - source.active = false; - } - }; - DelaySubscriber.prototype._schedule = function (scheduler) { - this.active = true; - var destination = this.destination; - destination.add(scheduler.schedule(DelaySubscriber.dispatch, this.delay, { - source: this, destination: this.destination, scheduler: scheduler - })); - }; - DelaySubscriber.prototype.scheduleNotification = function (notification) { - if (this.errored === true) { - return; - } - var scheduler = this.scheduler; - var message = new DelayMessage(scheduler.now() + this.delay, notification); - this.queue.push(message); - if (this.active === false) { - this._schedule(scheduler); +var subscribeToIterable = function (iterable) { + return function (subscriber) { + var iterator = iterable[_symbol_iterator__WEBPACK_IMPORTED_MODULE_0__["iterator"]](); + do { + var item = iterator.next(); + if (item.done) { + subscriber.complete(); + break; + } + subscriber.next(item.value); + if (subscriber.closed) { + break; + } + } while (true); + if (typeof iterator.return === 'function') { + subscriber.add(function () { + if (iterator.return) { + iterator.return(); + } + }); } + return subscriber; }; - DelaySubscriber.prototype._next = function (value) { - this.scheduleNotification(_Notification__WEBPACK_IMPORTED_MODULE_4__["Notification"].createNext(value)); - }; - DelaySubscriber.prototype._error = function (err) { - this.errored = true; - this.queue = []; - this.destination.error(err); - this.unsubscribe(); - }; - DelaySubscriber.prototype._complete = function () { - this.scheduleNotification(_Notification__WEBPACK_IMPORTED_MODULE_4__["Notification"].createComplete()); - this.unsubscribe(); - }; - return DelaySubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_3__["Subscriber"])); -var DelayMessage = /*@__PURE__*/ (function () { - function DelayMessage(time, notification) { - this.time = time; - this.notification = notification; - } - return DelayMessage; -}()); -//# sourceMappingURL=delay.js.map +}; +//# sourceMappingURL=subscribeToIterable.js.map /***/ }), -/* 240 */ +/* 235 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isDate", function() { return isDate; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "getSymbolIterator", function() { return getSymbolIterator; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "iterator", function() { return iterator; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "$$iterator", function() { return $$iterator; }); /** PURE_IMPORTS_START PURE_IMPORTS_END */ -function isDate(value) { - return value instanceof Date && !isNaN(+value); +function getSymbolIterator() { + if (typeof Symbol !== 'function' || !Symbol.iterator) { + return '@@iterator'; + } + return Symbol.iterator; } -//# sourceMappingURL=isDate.js.map +var iterator = /*@__PURE__*/ getSymbolIterator(); +var $$iterator = iterator; +//# sourceMappingURL=iterator.js.map /***/ }), -/* 241 */ +/* 236 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "NotificationKind", function() { return NotificationKind; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Notification", function() { return Notification; }); -/* harmony import */ var _observable_empty__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(242); -/* harmony import */ var _observable_of__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(227); -/* harmony import */ var _observable_throwError__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(243); -/** PURE_IMPORTS_START _observable_empty,_observable_of,_observable_throwError PURE_IMPORTS_END */ - - +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "subscribeToObservable", function() { return subscribeToObservable; }); +/* harmony import */ var _symbol_observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(183); +/** PURE_IMPORTS_START _symbol_observable PURE_IMPORTS_END */ -var NotificationKind; -/*@__PURE__*/ (function (NotificationKind) { - NotificationKind["NEXT"] = "N"; - NotificationKind["ERROR"] = "E"; - NotificationKind["COMPLETE"] = "C"; -})(NotificationKind || (NotificationKind = {})); -var Notification = /*@__PURE__*/ (function () { - function Notification(kind, value, error) { - this.kind = kind; - this.value = value; - this.error = error; - this.hasValue = kind === 'N'; - } - Notification.prototype.observe = function (observer) { - switch (this.kind) { - case 'N': - return observer.next && observer.next(this.value); - case 'E': - return observer.error && observer.error(this.error); - case 'C': - return observer.complete && observer.complete(); - } - }; - Notification.prototype.do = function (next, error, complete) { - var kind = this.kind; - switch (kind) { - case 'N': - return next && next(this.value); - case 'E': - return error && error(this.error); - case 'C': - return complete && complete(); - } - }; - Notification.prototype.accept = function (nextOrObserver, error, complete) { - if (nextOrObserver && typeof nextOrObserver.next === 'function') { - return this.observe(nextOrObserver); +var subscribeToObservable = function (obj) { + return function (subscriber) { + var obs = obj[_symbol_observable__WEBPACK_IMPORTED_MODULE_0__["observable"]](); + if (typeof obs.subscribe !== 'function') { + throw new TypeError('Provided object does not correctly implement Symbol.observable'); } else { - return this.do(nextOrObserver, error, complete); - } - }; - Notification.prototype.toObservable = function () { - var kind = this.kind; - switch (kind) { - case 'N': - return Object(_observable_of__WEBPACK_IMPORTED_MODULE_1__["of"])(this.value); - case 'E': - return Object(_observable_throwError__WEBPACK_IMPORTED_MODULE_2__["throwError"])(this.error); - case 'C': - return Object(_observable_empty__WEBPACK_IMPORTED_MODULE_0__["empty"])(); - } - throw new Error('unexpected notification kind value'); - }; - Notification.createNext = function (value) { - if (typeof value !== 'undefined') { - return new Notification('N', value); + return obs.subscribe(subscriber); } - return Notification.undefinedValueNotification; - }; - Notification.createError = function (err) { - return new Notification('E', undefined, err); - }; - Notification.createComplete = function () { - return Notification.completeNotification; }; - Notification.completeNotification = new Notification('C'); - Notification.undefinedValueNotification = new Notification('N', undefined); - return Notification; -}()); - -//# sourceMappingURL=Notification.js.map +}; +//# sourceMappingURL=subscribeToObservable.js.map /***/ }), -/* 242 */ +/* 237 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "EMPTY", function() { return EMPTY; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "empty", function() { return empty; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(193); -/** PURE_IMPORTS_START _Observable PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isArrayLike", function() { return isArrayLike; }); +/** PURE_IMPORTS_START PURE_IMPORTS_END */ +var isArrayLike = (function (x) { return x && typeof x.length === 'number' && typeof x !== 'function'; }); +//# sourceMappingURL=isArrayLike.js.map -var EMPTY = /*@__PURE__*/ new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { return subscriber.complete(); }); -function empty(scheduler) { - return scheduler ? emptyScheduled(scheduler) : EMPTY; -} -function emptyScheduled(scheduler) { - return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { return scheduler.schedule(function () { return subscriber.complete(); }); }); + +/***/ }), +/* 238 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isPromise", function() { return isPromise; }); +/** PURE_IMPORTS_START PURE_IMPORTS_END */ +function isPromise(value) { + return !!value && typeof value.subscribe !== 'function' && typeof value.then === 'function'; } -//# sourceMappingURL=empty.js.map +//# sourceMappingURL=isPromise.js.map /***/ }), -/* 243 */ +/* 239 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "throwError", function() { return throwError; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(193); -/** PURE_IMPORTS_START _Observable PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "concat", function() { return concat; }); +/* harmony import */ var _of__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(204); +/* harmony import */ var _operators_concatAll__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(240); +/** PURE_IMPORTS_START _of,_operators_concatAll PURE_IMPORTS_END */ -function throwError(error, scheduler) { - if (!scheduler) { - return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { return subscriber.error(error); }); - } - else { - return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { return scheduler.schedule(dispatch, 0, { error: error, subscriber: subscriber }); }); + +function concat() { + var observables = []; + for (var _i = 0; _i < arguments.length; _i++) { + observables[_i] = arguments[_i]; } + return Object(_operators_concatAll__WEBPACK_IMPORTED_MODULE_1__["concatAll"])()(_of__WEBPACK_IMPORTED_MODULE_0__["of"].apply(void 0, observables)); } -function dispatch(_a) { - var error = _a.error, subscriber = _a.subscriber; - subscriber.error(error); -} -//# sourceMappingURL=throwError.js.map +//# sourceMappingURL=concat.js.map /***/ }), -/* 244 */ +/* 240 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "delayWhen", function() { return delayWhen; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "concatAll", function() { return concatAll; }); +/* harmony import */ var _mergeAll__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(241); +/** PURE_IMPORTS_START _mergeAll PURE_IMPORTS_END */ + +function concatAll() { + return Object(_mergeAll__WEBPACK_IMPORTED_MODULE_0__["mergeAll"])(1); +} +//# sourceMappingURL=concatAll.js.map + + +/***/ }), +/* 241 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "mergeAll", function() { return mergeAll; }); +/* harmony import */ var _mergeMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(242); +/* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(220); +/** PURE_IMPORTS_START _mergeMap,_util_identity PURE_IMPORTS_END */ + + +function mergeAll(concurrent) { + if (concurrent === void 0) { + concurrent = Number.POSITIVE_INFINITY; + } + return Object(_mergeMap__WEBPACK_IMPORTED_MODULE_0__["mergeMap"])(_util_identity__WEBPACK_IMPORTED_MODULE_1__["identity"], concurrent); +} +//# sourceMappingURL=mergeAll.js.map + + +/***/ }), +/* 242 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "mergeMap", function() { return mergeMap; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MergeMapOperator", function() { return MergeMapOperator; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MergeMapSubscriber", function() { return MergeMapSubscriber; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(193); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(171); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(182); -/** PURE_IMPORTS_START tslib,_Subscriber,_Observable,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(230); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(229); +/* harmony import */ var _InnerSubscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(231); +/* harmony import */ var _map__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(226); +/* harmony import */ var _observable_from__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(243); +/** PURE_IMPORTS_START tslib,_util_subscribeToResult,_OuterSubscriber,_InnerSubscriber,_map,_observable_from PURE_IMPORTS_END */ -function delayWhen(delayDurationSelector, subscriptionDelay) { - if (subscriptionDelay) { - return function (source) { - return new SubscriptionDelayObservable(source, subscriptionDelay) - .lift(new DelayWhenOperator(delayDurationSelector)); - }; + +function mergeMap(project, resultSelector, concurrent) { + if (concurrent === void 0) { + concurrent = Number.POSITIVE_INFINITY; } - return function (source) { return source.lift(new DelayWhenOperator(delayDurationSelector)); }; + if (typeof resultSelector === 'function') { + return function (source) { return source.pipe(mergeMap(function (a, i) { return Object(_observable_from__WEBPACK_IMPORTED_MODULE_5__["from"])(project(a, i)).pipe(Object(_map__WEBPACK_IMPORTED_MODULE_4__["map"])(function (b, ii) { return resultSelector(a, b, i, ii); })); }, concurrent)); }; + } + else if (typeof resultSelector === 'number') { + concurrent = resultSelector; + } + return function (source) { return source.lift(new MergeMapOperator(project, concurrent)); }; } -var DelayWhenOperator = /*@__PURE__*/ (function () { - function DelayWhenOperator(delayDurationSelector) { - this.delayDurationSelector = delayDurationSelector; +var MergeMapOperator = /*@__PURE__*/ (function () { + function MergeMapOperator(project, concurrent) { + if (concurrent === void 0) { + concurrent = Number.POSITIVE_INFINITY; + } + this.project = project; + this.concurrent = concurrent; } - DelayWhenOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new DelayWhenSubscriber(subscriber, this.delayDurationSelector)); + MergeMapOperator.prototype.call = function (observer, source) { + return source.subscribe(new MergeMapSubscriber(observer, this.project, this.concurrent)); }; - return DelayWhenOperator; + return MergeMapOperator; }()); -var DelayWhenSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](DelayWhenSubscriber, _super); - function DelayWhenSubscriber(destination, delayDurationSelector) { + +var MergeMapSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](MergeMapSubscriber, _super); + function MergeMapSubscriber(destination, project, concurrent) { + if (concurrent === void 0) { + concurrent = Number.POSITIVE_INFINITY; + } var _this = _super.call(this, destination) || this; - _this.delayDurationSelector = delayDurationSelector; - _this.completed = false; - _this.delayNotifierSubscriptions = []; + _this.project = project; + _this.concurrent = concurrent; + _this.hasCompleted = false; + _this.buffer = []; + _this.active = 0; _this.index = 0; return _this; } - DelayWhenSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { - this.destination.next(outerValue); - this.removeSubscription(innerSub); - this.tryComplete(); - }; - DelayWhenSubscriber.prototype.notifyError = function (error, innerSub) { - this._error(error); - }; - DelayWhenSubscriber.prototype.notifyComplete = function (innerSub) { - var value = this.removeSubscription(innerSub); - if (value) { - this.destination.next(value); + MergeMapSubscriber.prototype._next = function (value) { + if (this.active < this.concurrent) { + this._tryNext(value); + } + else { + this.buffer.push(value); } - this.tryComplete(); }; - DelayWhenSubscriber.prototype._next = function (value) { + MergeMapSubscriber.prototype._tryNext = function (value) { + var result; var index = this.index++; try { - var delayNotifier = this.delayDurationSelector(value, index); - if (delayNotifier) { - this.tryDelay(delayNotifier, value); - } + result = this.project(value, index); } catch (err) { this.destination.error(err); + return; } + this.active++; + this._innerSub(result, value, index); }; - DelayWhenSubscriber.prototype._complete = function () { - this.completed = true; - this.tryComplete(); - this.unsubscribe(); - }; - DelayWhenSubscriber.prototype.removeSubscription = function (subscription) { - subscription.unsubscribe(); - var subscriptionIdx = this.delayNotifierSubscriptions.indexOf(subscription); - if (subscriptionIdx !== -1) { - this.delayNotifierSubscriptions.splice(subscriptionIdx, 1); - } - return subscription.outerValue; - }; - DelayWhenSubscriber.prototype.tryDelay = function (delayNotifier, value) { - var notifierSubscription = Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__["subscribeToResult"])(this, delayNotifier, value); - if (notifierSubscription && !notifierSubscription.closed) { - var destination = this.destination; - destination.add(notifierSubscription); - this.delayNotifierSubscriptions.push(notifierSubscription); - } + MergeMapSubscriber.prototype._innerSub = function (ish, value, index) { + var innerSubscriber = new _InnerSubscriber__WEBPACK_IMPORTED_MODULE_3__["InnerSubscriber"](this, undefined, undefined); + var destination = this.destination; + destination.add(innerSubscriber); + Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_1__["subscribeToResult"])(this, ish, value, index, innerSubscriber); }; - DelayWhenSubscriber.prototype.tryComplete = function () { - if (this.completed && this.delayNotifierSubscriptions.length === 0) { + MergeMapSubscriber.prototype._complete = function () { + this.hasCompleted = true; + if (this.active === 0 && this.buffer.length === 0) { this.destination.complete(); } - }; - return DelayWhenSubscriber; -}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__["OuterSubscriber"])); -var SubscriptionDelayObservable = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](SubscriptionDelayObservable, _super); - function SubscriptionDelayObservable(source, subscriptionDelay) { - var _this = _super.call(this) || this; - _this.source = source; - _this.subscriptionDelay = subscriptionDelay; - return _this; - } - SubscriptionDelayObservable.prototype._subscribe = function (subscriber) { - this.subscriptionDelay.subscribe(new SubscriptionDelaySubscriber(subscriber, this.source)); - }; - return SubscriptionDelayObservable; -}(_Observable__WEBPACK_IMPORTED_MODULE_2__["Observable"])); -var SubscriptionDelaySubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](SubscriptionDelaySubscriber, _super); - function SubscriptionDelaySubscriber(parent, source) { - var _this = _super.call(this) || this; - _this.parent = parent; - _this.source = source; - _this.sourceSubscribed = false; - return _this; - } - SubscriptionDelaySubscriber.prototype._next = function (unused) { - this.subscribeToSource(); - }; - SubscriptionDelaySubscriber.prototype._error = function (err) { this.unsubscribe(); - this.parent.error(err); }; - SubscriptionDelaySubscriber.prototype._complete = function () { - this.unsubscribe(); - this.subscribeToSource(); + MergeMapSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { + this.destination.next(innerValue); }; - SubscriptionDelaySubscriber.prototype.subscribeToSource = function () { - if (!this.sourceSubscribed) { - this.sourceSubscribed = true; - this.unsubscribe(); - this.source.subscribe(this.parent); + MergeMapSubscriber.prototype.notifyComplete = function (innerSub) { + var buffer = this.buffer; + this.remove(innerSub); + this.active--; + if (buffer.length > 0) { + this._next(buffer.shift()); + } + else if (this.active === 0 && this.hasCompleted) { + this.destination.complete(); } }; - return SubscriptionDelaySubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); -//# sourceMappingURL=delayWhen.js.map + return MergeMapSubscriber; +}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_2__["OuterSubscriber"])); + +//# sourceMappingURL=mergeMap.js.map /***/ }), -/* 245 */ +/* 243 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "dematerialize", function() { return dematerialize; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); -/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "from", function() { return from; }); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(170); +/* harmony import */ var _util_subscribeTo__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(232); +/* harmony import */ var _scheduled_scheduled__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(244); +/** PURE_IMPORTS_START _Observable,_util_subscribeTo,_scheduled_scheduled PURE_IMPORTS_END */ -function dematerialize() { - return function dematerializeOperatorFunction(source) { - return source.lift(new DeMaterializeOperator()); - }; -} -var DeMaterializeOperator = /*@__PURE__*/ (function () { - function DeMaterializeOperator() { + +function from(input, scheduler) { + if (!scheduler) { + if (input instanceof _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"]) { + return input; + } + return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](Object(_util_subscribeTo__WEBPACK_IMPORTED_MODULE_1__["subscribeTo"])(input)); } - DeMaterializeOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new DeMaterializeSubscriber(subscriber)); - }; - return DeMaterializeOperator; -}()); -var DeMaterializeSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](DeMaterializeSubscriber, _super); - function DeMaterializeSubscriber(destination) { - return _super.call(this, destination) || this; + else { + return Object(_scheduled_scheduled__WEBPACK_IMPORTED_MODULE_2__["scheduled"])(input, scheduler); } - DeMaterializeSubscriber.prototype._next = function (value) { - value.observe(this.destination); - }; - return DeMaterializeSubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); -//# sourceMappingURL=dematerialize.js.map +} +//# sourceMappingURL=from.js.map /***/ }), -/* 246 */ +/* 244 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "distinct", function() { return distinct; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "DistinctSubscriber", function() { return DistinctSubscriber; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(171); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(182); -/** PURE_IMPORTS_START tslib,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "scheduled", function() { return scheduled; }); +/* harmony import */ var _scheduleObservable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(245); +/* harmony import */ var _schedulePromise__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(246); +/* harmony import */ var _scheduleArray__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(208); +/* harmony import */ var _scheduleIterable__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(247); +/* harmony import */ var _util_isInteropObservable__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(248); +/* harmony import */ var _util_isPromise__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(238); +/* harmony import */ var _util_isArrayLike__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(237); +/* harmony import */ var _util_isIterable__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(249); +/** PURE_IMPORTS_START _scheduleObservable,_schedulePromise,_scheduleArray,_scheduleIterable,_util_isInteropObservable,_util_isPromise,_util_isArrayLike,_util_isIterable PURE_IMPORTS_END */ -function distinct(keySelector, flushes) { - return function (source) { return source.lift(new DistinctOperator(keySelector, flushes)); }; -} -var DistinctOperator = /*@__PURE__*/ (function () { - function DistinctOperator(keySelector, flushes) { - this.keySelector = keySelector; - this.flushes = flushes; - } - DistinctOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new DistinctSubscriber(subscriber, this.keySelector, this.flushes)); - }; - return DistinctOperator; -}()); -var DistinctSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](DistinctSubscriber, _super); - function DistinctSubscriber(destination, keySelector, flushes) { - var _this = _super.call(this, destination) || this; - _this.keySelector = keySelector; - _this.values = new Set(); - if (flushes) { - _this.add(Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__["subscribeToResult"])(_this, flushes)); - } - return _this; - } - DistinctSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { - this.values.clear(); - }; - DistinctSubscriber.prototype.notifyError = function (error, innerSub) { - this._error(error); - }; - DistinctSubscriber.prototype._next = function (value) { - if (this.keySelector) { - this._useKeySelector(value); - } - else { - this._finalizeNext(value, value); + + + + + +function scheduled(input, scheduler) { + if (input != null) { + if (Object(_util_isInteropObservable__WEBPACK_IMPORTED_MODULE_4__["isInteropObservable"])(input)) { + return Object(_scheduleObservable__WEBPACK_IMPORTED_MODULE_0__["scheduleObservable"])(input, scheduler); } - }; - DistinctSubscriber.prototype._useKeySelector = function (value) { - var key; - var destination = this.destination; - try { - key = this.keySelector(value); + else if (Object(_util_isPromise__WEBPACK_IMPORTED_MODULE_5__["isPromise"])(input)) { + return Object(_schedulePromise__WEBPACK_IMPORTED_MODULE_1__["schedulePromise"])(input, scheduler); } - catch (err) { - destination.error(err); - return; + else if (Object(_util_isArrayLike__WEBPACK_IMPORTED_MODULE_6__["isArrayLike"])(input)) { + return Object(_scheduleArray__WEBPACK_IMPORTED_MODULE_2__["scheduleArray"])(input, scheduler); } - this._finalizeNext(key, value); - }; - DistinctSubscriber.prototype._finalizeNext = function (key, value) { - var values = this.values; - if (!values.has(key)) { - values.add(key); - this.destination.next(value); + else if (Object(_util_isIterable__WEBPACK_IMPORTED_MODULE_7__["isIterable"])(input) || typeof input === 'string') { + return Object(_scheduleIterable__WEBPACK_IMPORTED_MODULE_3__["scheduleIterable"])(input, scheduler); } - }; - return DistinctSubscriber; -}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__["OuterSubscriber"])); - -//# sourceMappingURL=distinct.js.map + } + throw new TypeError((input !== null && typeof input || input) + ' is not observable'); +} +//# sourceMappingURL=scheduled.js.map /***/ }), -/* 247 */ +/* 245 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "distinctUntilChanged", function() { return distinctUntilChanged; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); -/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "scheduleObservable", function() { return scheduleObservable; }); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(170); +/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(177); +/* harmony import */ var _symbol_observable__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(183); +/** PURE_IMPORTS_START _Observable,_Subscription,_symbol_observable PURE_IMPORTS_END */ -function distinctUntilChanged(compare, keySelector) { - return function (source) { return source.lift(new DistinctUntilChangedOperator(compare, keySelector)); }; + +function scheduleObservable(input, scheduler) { + return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { + var sub = new _Subscription__WEBPACK_IMPORTED_MODULE_1__["Subscription"](); + sub.add(scheduler.schedule(function () { + var observable = input[_symbol_observable__WEBPACK_IMPORTED_MODULE_2__["observable"]](); + sub.add(observable.subscribe({ + next: function (value) { sub.add(scheduler.schedule(function () { return subscriber.next(value); })); }, + error: function (err) { sub.add(scheduler.schedule(function () { return subscriber.error(err); })); }, + complete: function () { sub.add(scheduler.schedule(function () { return subscriber.complete(); })); }, + })); + })); + return sub; + }); } -var DistinctUntilChangedOperator = /*@__PURE__*/ (function () { - function DistinctUntilChangedOperator(compare, keySelector) { - this.compare = compare; - this.keySelector = keySelector; - } - DistinctUntilChangedOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new DistinctUntilChangedSubscriber(subscriber, this.compare, this.keySelector)); - }; - return DistinctUntilChangedOperator; -}()); -var DistinctUntilChangedSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](DistinctUntilChangedSubscriber, _super); - function DistinctUntilChangedSubscriber(destination, compare, keySelector) { - var _this = _super.call(this, destination) || this; - _this.keySelector = keySelector; - _this.hasKey = false; - if (typeof compare === 'function') { - _this.compare = compare; - } - return _this; - } - DistinctUntilChangedSubscriber.prototype.compare = function (x, y) { - return x === y; - }; - DistinctUntilChangedSubscriber.prototype._next = function (value) { - var key; - try { - var keySelector = this.keySelector; - key = keySelector ? keySelector(value) : value; - } - catch (err) { - return this.destination.error(err); - } - var result = false; - if (this.hasKey) { - try { - var compare = this.compare; - result = compare(this.key, key); - } - catch (err) { - return this.destination.error(err); - } - } - else { - this.hasKey = true; - } - if (!result) { - this.key = key; - this.destination.next(value); - } - }; - return DistinctUntilChangedSubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); -//# sourceMappingURL=distinctUntilChanged.js.map +//# sourceMappingURL=scheduleObservable.js.map /***/ }), -/* 248 */ +/* 246 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "distinctUntilKeyChanged", function() { return distinctUntilKeyChanged; }); -/* harmony import */ var _distinctUntilChanged__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(247); -/** PURE_IMPORTS_START _distinctUntilChanged PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "schedulePromise", function() { return schedulePromise; }); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(170); +/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(177); +/** PURE_IMPORTS_START _Observable,_Subscription PURE_IMPORTS_END */ -function distinctUntilKeyChanged(key, compare) { - return Object(_distinctUntilChanged__WEBPACK_IMPORTED_MODULE_0__["distinctUntilChanged"])(function (x, y) { return compare ? compare(x[key], y[key]) : x[key] === y[key]; }); + +function schedulePromise(input, scheduler) { + return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { + var sub = new _Subscription__WEBPACK_IMPORTED_MODULE_1__["Subscription"](); + sub.add(scheduler.schedule(function () { + return input.then(function (value) { + sub.add(scheduler.schedule(function () { + subscriber.next(value); + sub.add(scheduler.schedule(function () { return subscriber.complete(); })); + })); + }, function (err) { + sub.add(scheduler.schedule(function () { return subscriber.error(err); })); + }); + })); + return sub; + }); } -//# sourceMappingURL=distinctUntilKeyChanged.js.map +//# sourceMappingURL=schedulePromise.js.map /***/ }), -/* 249 */ +/* 247 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "elementAt", function() { return elementAt; }); -/* harmony import */ var _util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(250); -/* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(251); -/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(252); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(238); -/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(254); -/** PURE_IMPORTS_START _util_ArgumentOutOfRangeError,_filter,_throwIfEmpty,_defaultIfEmpty,_take PURE_IMPORTS_END */ - - +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "scheduleIterable", function() { return scheduleIterable; }); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(170); +/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(177); +/* harmony import */ var _symbol_iterator__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(235); +/** PURE_IMPORTS_START _Observable,_Subscription,_symbol_iterator PURE_IMPORTS_END */ -function elementAt(index, defaultValue) { - if (index < 0) { - throw new _util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_0__["ArgumentOutOfRangeError"](); +function scheduleIterable(input, scheduler) { + if (!input) { + throw new Error('Iterable cannot be null'); } - var hasDefaultValue = arguments.length >= 2; - return function (source) { - return source.pipe(Object(_filter__WEBPACK_IMPORTED_MODULE_1__["filter"])(function (v, i) { return i === index; }), Object(_take__WEBPACK_IMPORTED_MODULE_4__["take"])(1), hasDefaultValue - ? Object(_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__["defaultIfEmpty"])(defaultValue) - : Object(_throwIfEmpty__WEBPACK_IMPORTED_MODULE_2__["throwIfEmpty"])(function () { return new _util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_0__["ArgumentOutOfRangeError"](); })); - }; + return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { + var sub = new _Subscription__WEBPACK_IMPORTED_MODULE_1__["Subscription"](); + var iterator; + sub.add(function () { + if (iterator && typeof iterator.return === 'function') { + iterator.return(); + } + }); + sub.add(scheduler.schedule(function () { + iterator = input[_symbol_iterator__WEBPACK_IMPORTED_MODULE_2__["iterator"]](); + sub.add(scheduler.schedule(function () { + if (subscriber.closed) { + return; + } + var value; + var done; + try { + var result = iterator.next(); + value = result.value; + done = result.done; + } + catch (err) { + subscriber.error(err); + return; + } + if (done) { + subscriber.complete(); + } + else { + subscriber.next(value); + this.schedule(); + } + })); + })); + return sub; + }); } -//# sourceMappingURL=elementAt.js.map +//# sourceMappingURL=scheduleIterable.js.map /***/ }), -/* 250 */ +/* 248 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ArgumentOutOfRangeError", function() { return ArgumentOutOfRangeError; }); -/** PURE_IMPORTS_START PURE_IMPORTS_END */ -var ArgumentOutOfRangeErrorImpl = /*@__PURE__*/ (function () { - function ArgumentOutOfRangeErrorImpl() { - Error.call(this); - this.message = 'argument out of range'; - this.name = 'ArgumentOutOfRangeError'; - return this; - } - ArgumentOutOfRangeErrorImpl.prototype = /*@__PURE__*/ Object.create(Error.prototype); - return ArgumentOutOfRangeErrorImpl; -})(); -var ArgumentOutOfRangeError = ArgumentOutOfRangeErrorImpl; -//# sourceMappingURL=ArgumentOutOfRangeError.js.map +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isInteropObservable", function() { return isInteropObservable; }); +/* harmony import */ var _symbol_observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(183); +/** PURE_IMPORTS_START _symbol_observable PURE_IMPORTS_END */ + +function isInteropObservable(input) { + return input && typeof input[_symbol_observable__WEBPACK_IMPORTED_MODULE_0__["observable"]] === 'function'; +} +//# sourceMappingURL=isInteropObservable.js.map /***/ }), -/* 251 */ +/* 249 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "filter", function() { return filter; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); -/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ - +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isIterable", function() { return isIterable; }); +/* harmony import */ var _symbol_iterator__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(235); +/** PURE_IMPORTS_START _symbol_iterator PURE_IMPORTS_END */ -function filter(predicate, thisArg) { - return function filterOperatorFunction(source) { - return source.lift(new FilterOperator(predicate, thisArg)); - }; +function isIterable(input) { + return input && typeof input[_symbol_iterator__WEBPACK_IMPORTED_MODULE_0__["iterator"]] === 'function'; } -var FilterOperator = /*@__PURE__*/ (function () { - function FilterOperator(predicate, thisArg) { - this.predicate = predicate; - this.thisArg = thisArg; - } - FilterOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new FilterSubscriber(subscriber, this.predicate, this.thisArg)); - }; - return FilterOperator; -}()); -var FilterSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](FilterSubscriber, _super); - function FilterSubscriber(destination, predicate, thisArg) { - var _this = _super.call(this, destination) || this; - _this.predicate = predicate; - _this.thisArg = thisArg; - _this.count = 0; - return _this; - } - FilterSubscriber.prototype._next = function (value) { - var result; +//# sourceMappingURL=isIterable.js.map + + +/***/ }), +/* 250 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "defer", function() { return defer; }); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(170); +/* harmony import */ var _from__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(243); +/* harmony import */ var _empty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(203); +/** PURE_IMPORTS_START _Observable,_from,_empty PURE_IMPORTS_END */ + + + +function defer(observableFactory) { + return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { + var input; try { - result = this.predicate.call(this.thisArg, value, this.count++); + input = observableFactory(); } catch (err) { - this.destination.error(err); + subscriber.error(err); + return undefined; + } + var source = input ? Object(_from__WEBPACK_IMPORTED_MODULE_1__["from"])(input) : Object(_empty__WEBPACK_IMPORTED_MODULE_2__["empty"])(); + return source.subscribe(subscriber); + }); +} +//# sourceMappingURL=defer.js.map + + +/***/ }), +/* 251 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "forkJoin", function() { return forkJoin; }); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(170); +/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(178); +/* harmony import */ var _operators_map__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(226); +/* harmony import */ var _util_isObject__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(179); +/* harmony import */ var _from__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(243); +/** PURE_IMPORTS_START _Observable,_util_isArray,_operators_map,_util_isObject,_from PURE_IMPORTS_END */ + + + + + +function forkJoin() { + var sources = []; + for (var _i = 0; _i < arguments.length; _i++) { + sources[_i] = arguments[_i]; + } + if (sources.length === 1) { + var first_1 = sources[0]; + if (Object(_util_isArray__WEBPACK_IMPORTED_MODULE_1__["isArray"])(first_1)) { + return forkJoinInternal(first_1, null); + } + if (Object(_util_isObject__WEBPACK_IMPORTED_MODULE_3__["isObject"])(first_1) && Object.getPrototypeOf(first_1) === Object.prototype) { + var keys = Object.keys(first_1); + return forkJoinInternal(keys.map(function (key) { return first_1[key]; }), keys); + } + } + if (typeof sources[sources.length - 1] === 'function') { + var resultSelector_1 = sources.pop(); + sources = (sources.length === 1 && Object(_util_isArray__WEBPACK_IMPORTED_MODULE_1__["isArray"])(sources[0])) ? sources[0] : sources; + return forkJoinInternal(sources, null).pipe(Object(_operators_map__WEBPACK_IMPORTED_MODULE_2__["map"])(function (args) { return resultSelector_1.apply(void 0, args); })); + } + return forkJoinInternal(sources, null); +} +function forkJoinInternal(sources, keys) { + return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { + var len = sources.length; + if (len === 0) { + subscriber.complete(); return; } - if (result) { - this.destination.next(value); + var values = new Array(len); + var completed = 0; + var emitted = 0; + var _loop_1 = function (i) { + var source = Object(_from__WEBPACK_IMPORTED_MODULE_4__["from"])(sources[i]); + var hasValue = false; + subscriber.add(source.subscribe({ + next: function (value) { + if (!hasValue) { + hasValue = true; + emitted++; + } + values[i] = value; + }, + error: function (err) { return subscriber.error(err); }, + complete: function () { + completed++; + if (completed === len || !hasValue) { + if (emitted === len) { + subscriber.next(keys ? + keys.reduce(function (result, key, i) { return (result[key] = values[i], result); }, {}) : + values); + } + subscriber.complete(); + } + } + })); + }; + for (var i = 0; i < len; i++) { + _loop_1(i); } - }; - return FilterSubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); -//# sourceMappingURL=filter.js.map + }); +} +//# sourceMappingURL=forkJoin.js.map /***/ }), @@ -26273,64 +26272,74 @@ var FilterSubscriber = /*@__PURE__*/ (function (_super) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "throwIfEmpty", function() { return throwIfEmpty; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _util_EmptyError__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(253); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(172); -/** PURE_IMPORTS_START tslib,_util_EmptyError,_Subscriber PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "fromEvent", function() { return fromEvent; }); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(170); +/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(178); +/* harmony import */ var _util_isFunction__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(173); +/* harmony import */ var _operators_map__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(226); +/** PURE_IMPORTS_START _Observable,_util_isArray,_util_isFunction,_operators_map PURE_IMPORTS_END */ -function throwIfEmpty(errorFactory) { - if (errorFactory === void 0) { - errorFactory = defaultErrorFactory; - } - return function (source) { - return source.lift(new ThrowIfEmptyOperator(errorFactory)); - }; -} -var ThrowIfEmptyOperator = /*@__PURE__*/ (function () { - function ThrowIfEmptyOperator(errorFactory) { - this.errorFactory = errorFactory; + +var toString = /*@__PURE__*/ (function () { return Object.prototype.toString; })(); +function fromEvent(target, eventName, options, resultSelector) { + if (Object(_util_isFunction__WEBPACK_IMPORTED_MODULE_2__["isFunction"])(options)) { + resultSelector = options; + options = undefined; } - ThrowIfEmptyOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new ThrowIfEmptySubscriber(subscriber, this.errorFactory)); - }; - return ThrowIfEmptyOperator; -}()); -var ThrowIfEmptySubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](ThrowIfEmptySubscriber, _super); - function ThrowIfEmptySubscriber(destination, errorFactory) { - var _this = _super.call(this, destination) || this; - _this.errorFactory = errorFactory; - _this.hasValue = false; - return _this; + if (resultSelector) { + return fromEvent(target, eventName, options).pipe(Object(_operators_map__WEBPACK_IMPORTED_MODULE_3__["map"])(function (args) { return Object(_util_isArray__WEBPACK_IMPORTED_MODULE_1__["isArray"])(args) ? resultSelector.apply(void 0, args) : resultSelector(args); })); } - ThrowIfEmptySubscriber.prototype._next = function (value) { - this.hasValue = true; - this.destination.next(value); - }; - ThrowIfEmptySubscriber.prototype._complete = function () { - if (!this.hasValue) { - var err = void 0; - try { - err = this.errorFactory(); + return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { + function handler(e) { + if (arguments.length > 1) { + subscriber.next(Array.prototype.slice.call(arguments)); } - catch (e) { - err = e; + else { + subscriber.next(e); } - this.destination.error(err); } - else { - return this.destination.complete(); + setupSubscription(target, eventName, handler, subscriber, options); + }); +} +function setupSubscription(sourceObj, eventName, handler, subscriber, options) { + var unsubscribe; + if (isEventTarget(sourceObj)) { + var source_1 = sourceObj; + sourceObj.addEventListener(eventName, handler, options); + unsubscribe = function () { return source_1.removeEventListener(eventName, handler, options); }; + } + else if (isJQueryStyleEventEmitter(sourceObj)) { + var source_2 = sourceObj; + sourceObj.on(eventName, handler); + unsubscribe = function () { return source_2.off(eventName, handler); }; + } + else if (isNodeStyleEventEmitter(sourceObj)) { + var source_3 = sourceObj; + sourceObj.addListener(eventName, handler); + unsubscribe = function () { return source_3.removeListener(eventName, handler); }; + } + else if (sourceObj && sourceObj.length) { + for (var i = 0, len = sourceObj.length; i < len; i++) { + setupSubscription(sourceObj[i], eventName, handler, subscriber, options); } - }; - return ThrowIfEmptySubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_2__["Subscriber"])); -function defaultErrorFactory() { - return new _util_EmptyError__WEBPACK_IMPORTED_MODULE_1__["EmptyError"](); + } + else { + throw new TypeError('Invalid event target'); + } + subscriber.add(unsubscribe); } -//# sourceMappingURL=throwIfEmpty.js.map +function isNodeStyleEventEmitter(sourceObj) { + return sourceObj && typeof sourceObj.addListener === 'function' && typeof sourceObj.removeListener === 'function'; +} +function isJQueryStyleEventEmitter(sourceObj) { + return sourceObj && typeof sourceObj.on === 'function' && typeof sourceObj.off === 'function'; +} +function isEventTarget(sourceObj) { + return sourceObj && typeof sourceObj.addEventListener === 'function' && typeof sourceObj.removeEventListener === 'function'; +} +//# sourceMappingURL=fromEvent.js.map /***/ }), @@ -26339,20 +26348,43 @@ function defaultErrorFactory() { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "EmptyError", function() { return EmptyError; }); -/** PURE_IMPORTS_START PURE_IMPORTS_END */ -var EmptyErrorImpl = /*@__PURE__*/ (function () { - function EmptyErrorImpl() { - Error.call(this); - this.message = 'no elements in sequence'; - this.name = 'EmptyError'; - return this; +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "fromEventPattern", function() { return fromEventPattern; }); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(170); +/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(178); +/* harmony import */ var _util_isFunction__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(173); +/* harmony import */ var _operators_map__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(226); +/** PURE_IMPORTS_START _Observable,_util_isArray,_util_isFunction,_operators_map PURE_IMPORTS_END */ + + + + +function fromEventPattern(addHandler, removeHandler, resultSelector) { + if (resultSelector) { + return fromEventPattern(addHandler, removeHandler).pipe(Object(_operators_map__WEBPACK_IMPORTED_MODULE_3__["map"])(function (args) { return Object(_util_isArray__WEBPACK_IMPORTED_MODULE_1__["isArray"])(args) ? resultSelector.apply(void 0, args) : resultSelector(args); })); } - EmptyErrorImpl.prototype = /*@__PURE__*/ Object.create(Error.prototype); - return EmptyErrorImpl; -})(); -var EmptyError = EmptyErrorImpl; -//# sourceMappingURL=EmptyError.js.map + return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { + var handler = function () { + var e = []; + for (var _i = 0; _i < arguments.length; _i++) { + e[_i] = arguments[_i]; + } + return subscriber.next(e.length === 1 ? e[0] : e); + }; + var retValue; + try { + retValue = addHandler(handler); + } + catch (err) { + subscriber.error(err); + return undefined; + } + if (!Object(_util_isFunction__WEBPACK_IMPORTED_MODULE_2__["isFunction"])(removeHandler)) { + return undefined; + } + return function () { return removeHandler(handler, retValue); }; + }); +} +//# sourceMappingURL=fromEventPattern.js.map /***/ }), @@ -26361,60 +26393,135 @@ var EmptyError = EmptyErrorImpl; "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "take", function() { return take; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); -/* harmony import */ var _util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(250); -/* harmony import */ var _observable_empty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(242); -/** PURE_IMPORTS_START tslib,_Subscriber,_util_ArgumentOutOfRangeError,_observable_empty PURE_IMPORTS_END */ - +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "generate", function() { return generate; }); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(170); +/* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(220); +/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(205); +/** PURE_IMPORTS_START _Observable,_util_identity,_util_isScheduler PURE_IMPORTS_END */ -function take(count) { - return function (source) { - if (count === 0) { - return Object(_observable_empty__WEBPACK_IMPORTED_MODULE_3__["empty"])(); - } - else { - return source.lift(new TakeOperator(count)); +function generate(initialStateOrOptions, condition, iterate, resultSelectorOrObservable, scheduler) { + var resultSelector; + var initialState; + if (arguments.length == 1) { + var options = initialStateOrOptions; + initialState = options.initialState; + condition = options.condition; + iterate = options.iterate; + resultSelector = options.resultSelector || _util_identity__WEBPACK_IMPORTED_MODULE_1__["identity"]; + scheduler = options.scheduler; + } + else if (resultSelectorOrObservable === undefined || Object(_util_isScheduler__WEBPACK_IMPORTED_MODULE_2__["isScheduler"])(resultSelectorOrObservable)) { + initialState = initialStateOrOptions; + resultSelector = _util_identity__WEBPACK_IMPORTED_MODULE_1__["identity"]; + scheduler = resultSelectorOrObservable; + } + else { + initialState = initialStateOrOptions; + resultSelector = resultSelectorOrObservable; + } + return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { + var state = initialState; + if (scheduler) { + return scheduler.schedule(dispatch, 0, { + subscriber: subscriber, + iterate: iterate, + condition: condition, + resultSelector: resultSelector, + state: state + }); } - }; + do { + if (condition) { + var conditionResult = void 0; + try { + conditionResult = condition(state); + } + catch (err) { + subscriber.error(err); + return undefined; + } + if (!conditionResult) { + subscriber.complete(); + break; + } + } + var value = void 0; + try { + value = resultSelector(state); + } + catch (err) { + subscriber.error(err); + return undefined; + } + subscriber.next(value); + if (subscriber.closed) { + break; + } + try { + state = iterate(state); + } + catch (err) { + subscriber.error(err); + return undefined; + } + } while (true); + return undefined; + }); } -var TakeOperator = /*@__PURE__*/ (function () { - function TakeOperator(total) { - this.total = total; - if (this.total < 0) { - throw new _util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_2__["ArgumentOutOfRangeError"]; +function dispatch(state) { + var subscriber = state.subscriber, condition = state.condition; + if (subscriber.closed) { + return undefined; + } + if (state.needIterate) { + try { + state.state = state.iterate(state.state); + } + catch (err) { + subscriber.error(err); + return undefined; } } - TakeOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new TakeSubscriber(subscriber, this.total)); - }; - return TakeOperator; -}()); -var TakeSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](TakeSubscriber, _super); - function TakeSubscriber(destination, total) { - var _this = _super.call(this, destination) || this; - _this.total = total; - _this.count = 0; - return _this; + else { + state.needIterate = true; } - TakeSubscriber.prototype._next = function (value) { - var total = this.total; - var count = ++this.count; - if (count <= total) { - this.destination.next(value); - if (count === total) { - this.destination.complete(); - this.unsubscribe(); - } + if (condition) { + var conditionResult = void 0; + try { + conditionResult = condition(state.state); } - }; - return TakeSubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); -//# sourceMappingURL=take.js.map + catch (err) { + subscriber.error(err); + return undefined; + } + if (!conditionResult) { + subscriber.complete(); + return undefined; + } + if (subscriber.closed) { + return undefined; + } + } + var value; + try { + value = state.resultSelector(state.state); + } + catch (err) { + subscriber.error(err); + return undefined; + } + if (subscriber.closed) { + return undefined; + } + subscriber.next(value); + if (subscriber.closed) { + return undefined; + } + return this.schedule(state); +} +//# sourceMappingURL=generate.js.map /***/ }), @@ -26423,20 +26530,22 @@ var TakeSubscriber = /*@__PURE__*/ (function (_super) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "endWith", function() { return endWith; }); -/* harmony import */ var _observable_concat__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(226); -/* harmony import */ var _observable_of__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(227); -/** PURE_IMPORTS_START _observable_concat,_observable_of PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "iif", function() { return iif; }); +/* harmony import */ var _defer__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(250); +/* harmony import */ var _empty__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(203); +/** PURE_IMPORTS_START _defer,_empty PURE_IMPORTS_END */ -function endWith() { - var array = []; - for (var _i = 0; _i < arguments.length; _i++) { - array[_i] = arguments[_i]; +function iif(condition, trueResult, falseResult) { + if (trueResult === void 0) { + trueResult = _empty__WEBPACK_IMPORTED_MODULE_1__["EMPTY"]; } - return function (source) { return Object(_observable_concat__WEBPACK_IMPORTED_MODULE_0__["concat"])(source, _observable_of__WEBPACK_IMPORTED_MODULE_1__["of"].apply(void 0, array)); }; + if (falseResult === void 0) { + falseResult = _empty__WEBPACK_IMPORTED_MODULE_1__["EMPTY"]; + } + return Object(_defer__WEBPACK_IMPORTED_MODULE_0__["defer"])(function () { return condition() ? trueResult : falseResult; }); } -//# sourceMappingURL=endWith.js.map +//# sourceMappingURL=iif.js.map /***/ }), @@ -26445,60 +26554,38 @@ function endWith() { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "every", function() { return every; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); -/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "interval", function() { return interval; }); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(170); +/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(215); +/* harmony import */ var _util_isNumeric__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(257); +/** PURE_IMPORTS_START _Observable,_scheduler_async,_util_isNumeric PURE_IMPORTS_END */ -function every(predicate, thisArg) { - return function (source) { return source.lift(new EveryOperator(predicate, thisArg, source)); }; -} -var EveryOperator = /*@__PURE__*/ (function () { - function EveryOperator(predicate, thisArg, source) { - this.predicate = predicate; - this.thisArg = thisArg; - this.source = source; + +function interval(period, scheduler) { + if (period === void 0) { + period = 0; } - EveryOperator.prototype.call = function (observer, source) { - return source.subscribe(new EverySubscriber(observer, this.predicate, this.thisArg, this.source)); - }; - return EveryOperator; -}()); -var EverySubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](EverySubscriber, _super); - function EverySubscriber(destination, predicate, thisArg, source) { - var _this = _super.call(this, destination) || this; - _this.predicate = predicate; - _this.thisArg = thisArg; - _this.source = source; - _this.index = 0; - _this.thisArg = thisArg || _this; - return _this; + if (scheduler === void 0) { + scheduler = _scheduler_async__WEBPACK_IMPORTED_MODULE_1__["async"]; } - EverySubscriber.prototype.notifyComplete = function (everyValueMatch) { - this.destination.next(everyValueMatch); - this.destination.complete(); - }; - EverySubscriber.prototype._next = function (value) { - var result = false; - try { - result = this.predicate.call(this.thisArg, value, this.index++, this.source); - } - catch (err) { - this.destination.error(err); - return; - } - if (!result) { - this.notifyComplete(false); - } - }; - EverySubscriber.prototype._complete = function () { - this.notifyComplete(true); - }; - return EverySubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); -//# sourceMappingURL=every.js.map + if (!Object(_util_isNumeric__WEBPACK_IMPORTED_MODULE_2__["isNumeric"])(period) || period < 0) { + period = 0; + } + if (!scheduler || typeof scheduler.schedule !== 'function') { + scheduler = _scheduler_async__WEBPACK_IMPORTED_MODULE_1__["async"]; + } + return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { + subscriber.add(scheduler.schedule(dispatch, period, { subscriber: subscriber, counter: 0, period: period })); + return subscriber; + }); +} +function dispatch(state) { + var subscriber = state.subscriber, counter = state.counter, period = state.period; + subscriber.next(counter); + this.schedule({ subscriber: subscriber, counter: counter + 1, period: period }, period); +} +//# sourceMappingURL=interval.js.map /***/ }), @@ -26507,55 +26594,14 @@ var EverySubscriber = /*@__PURE__*/ (function (_super) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "exhaust", function() { return exhaust; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(171); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(182); -/** PURE_IMPORTS_START tslib,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ - - +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isNumeric", function() { return isNumeric; }); +/* harmony import */ var _isArray__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(178); +/** PURE_IMPORTS_START _isArray PURE_IMPORTS_END */ -function exhaust() { - return function (source) { return source.lift(new SwitchFirstOperator()); }; +function isNumeric(val) { + return !Object(_isArray__WEBPACK_IMPORTED_MODULE_0__["isArray"])(val) && (val - parseFloat(val) + 1) >= 0; } -var SwitchFirstOperator = /*@__PURE__*/ (function () { - function SwitchFirstOperator() { - } - SwitchFirstOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new SwitchFirstSubscriber(subscriber)); - }; - return SwitchFirstOperator; -}()); -var SwitchFirstSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](SwitchFirstSubscriber, _super); - function SwitchFirstSubscriber(destination) { - var _this = _super.call(this, destination) || this; - _this.hasCompleted = false; - _this.hasSubscription = false; - return _this; - } - SwitchFirstSubscriber.prototype._next = function (value) { - if (!this.hasSubscription) { - this.hasSubscription = true; - this.add(Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__["subscribeToResult"])(this, value)); - } - }; - SwitchFirstSubscriber.prototype._complete = function () { - this.hasCompleted = true; - if (!this.hasSubscription) { - this.destination.complete(); - } - }; - SwitchFirstSubscriber.prototype.notifyComplete = function (innerSub) { - this.remove(innerSub); - this.hasSubscription = false; - if (this.hasCompleted) { - this.destination.complete(); - } - }; - return SwitchFirstSubscriber; -}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__["OuterSubscriber"])); -//# sourceMappingURL=exhaust.js.map +//# sourceMappingURL=isNumeric.js.map /***/ }), @@ -26564,95 +26610,39 @@ var SwitchFirstSubscriber = /*@__PURE__*/ (function (_super) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "exhaustMap", function() { return exhaustMap; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(171); -/* harmony import */ var _InnerSubscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(183); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(182); -/* harmony import */ var _map__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(231); -/* harmony import */ var _observable_from__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(218); -/** PURE_IMPORTS_START tslib,_OuterSubscriber,_InnerSubscriber,_util_subscribeToResult,_map,_observable_from PURE_IMPORTS_END */ - - +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "merge", function() { return merge; }); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(170); +/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(205); +/* harmony import */ var _operators_mergeAll__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(241); +/* harmony import */ var _fromArray__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(206); +/** PURE_IMPORTS_START _Observable,_util_isScheduler,_operators_mergeAll,_fromArray PURE_IMPORTS_END */ -function exhaustMap(project, resultSelector) { - if (resultSelector) { - return function (source) { return source.pipe(exhaustMap(function (a, i) { return Object(_observable_from__WEBPACK_IMPORTED_MODULE_5__["from"])(project(a, i)).pipe(Object(_map__WEBPACK_IMPORTED_MODULE_4__["map"])(function (b, ii) { return resultSelector(a, b, i, ii); })); })); }; +function merge() { + var observables = []; + for (var _i = 0; _i < arguments.length; _i++) { + observables[_i] = arguments[_i]; } - return function (source) { - return source.lift(new ExhaustMapOperator(project)); - }; -} -var ExhaustMapOperator = /*@__PURE__*/ (function () { - function ExhaustMapOperator(project) { - this.project = project; + var concurrent = Number.POSITIVE_INFINITY; + var scheduler = null; + var last = observables[observables.length - 1]; + if (Object(_util_isScheduler__WEBPACK_IMPORTED_MODULE_1__["isScheduler"])(last)) { + scheduler = observables.pop(); + if (observables.length > 1 && typeof observables[observables.length - 1] === 'number') { + concurrent = observables.pop(); + } } - ExhaustMapOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new ExhaustMapSubscriber(subscriber, this.project)); - }; - return ExhaustMapOperator; -}()); -var ExhaustMapSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](ExhaustMapSubscriber, _super); - function ExhaustMapSubscriber(destination, project) { - var _this = _super.call(this, destination) || this; - _this.project = project; - _this.hasSubscription = false; - _this.hasCompleted = false; - _this.index = 0; - return _this; + else if (typeof last === 'number') { + concurrent = observables.pop(); } - ExhaustMapSubscriber.prototype._next = function (value) { - if (!this.hasSubscription) { - this.tryNext(value); - } - }; - ExhaustMapSubscriber.prototype.tryNext = function (value) { - var result; - var index = this.index++; - try { - result = this.project(value, index); - } - catch (err) { - this.destination.error(err); - return; - } - this.hasSubscription = true; - this._innerSub(result, value, index); - }; - ExhaustMapSubscriber.prototype._innerSub = function (result, value, index) { - var innerSubscriber = new _InnerSubscriber__WEBPACK_IMPORTED_MODULE_2__["InnerSubscriber"](this, undefined, undefined); - var destination = this.destination; - destination.add(innerSubscriber); - Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__["subscribeToResult"])(this, result, value, index, innerSubscriber); - }; - ExhaustMapSubscriber.prototype._complete = function () { - this.hasCompleted = true; - if (!this.hasSubscription) { - this.destination.complete(); - } - this.unsubscribe(); - }; - ExhaustMapSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { - this.destination.next(innerValue); - }; - ExhaustMapSubscriber.prototype.notifyError = function (err) { - this.destination.error(err); - }; - ExhaustMapSubscriber.prototype.notifyComplete = function (innerSub) { - var destination = this.destination; - destination.remove(innerSub); - this.hasSubscription = false; - if (this.hasCompleted) { - this.destination.complete(); - } - }; - return ExhaustMapSubscriber; -}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__["OuterSubscriber"])); -//# sourceMappingURL=exhaustMap.js.map + if (scheduler === null && observables.length === 1 && observables[0] instanceof _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"]) { + return observables[0]; + } + return Object(_operators_mergeAll__WEBPACK_IMPORTED_MODULE_2__["mergeAll"])(concurrent)(Object(_fromArray__WEBPACK_IMPORTED_MODULE_3__["fromArray"])(observables, scheduler)); +} +//# sourceMappingURL=merge.js.map /***/ }), @@ -26661,117 +26651,18 @@ var ExhaustMapSubscriber = /*@__PURE__*/ (function (_super) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "expand", function() { return expand; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ExpandOperator", function() { return ExpandOperator; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ExpandSubscriber", function() { return ExpandSubscriber; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(171); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(182); -/** PURE_IMPORTS_START tslib,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ - +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "NEVER", function() { return NEVER; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "never", function() { return never; }); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(170); +/* harmony import */ var _util_noop__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(185); +/** PURE_IMPORTS_START _Observable,_util_noop PURE_IMPORTS_END */ -function expand(project, concurrent, scheduler) { - if (concurrent === void 0) { - concurrent = Number.POSITIVE_INFINITY; - } - if (scheduler === void 0) { - scheduler = undefined; - } - concurrent = (concurrent || 0) < 1 ? Number.POSITIVE_INFINITY : concurrent; - return function (source) { return source.lift(new ExpandOperator(project, concurrent, scheduler)); }; +var NEVER = /*@__PURE__*/ new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](_util_noop__WEBPACK_IMPORTED_MODULE_1__["noop"]); +function never() { + return NEVER; } -var ExpandOperator = /*@__PURE__*/ (function () { - function ExpandOperator(project, concurrent, scheduler) { - this.project = project; - this.concurrent = concurrent; - this.scheduler = scheduler; - } - ExpandOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new ExpandSubscriber(subscriber, this.project, this.concurrent, this.scheduler)); - }; - return ExpandOperator; -}()); - -var ExpandSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](ExpandSubscriber, _super); - function ExpandSubscriber(destination, project, concurrent, scheduler) { - var _this = _super.call(this, destination) || this; - _this.project = project; - _this.concurrent = concurrent; - _this.scheduler = scheduler; - _this.index = 0; - _this.active = 0; - _this.hasCompleted = false; - if (concurrent < Number.POSITIVE_INFINITY) { - _this.buffer = []; - } - return _this; - } - ExpandSubscriber.dispatch = function (arg) { - var subscriber = arg.subscriber, result = arg.result, value = arg.value, index = arg.index; - subscriber.subscribeToProjection(result, value, index); - }; - ExpandSubscriber.prototype._next = function (value) { - var destination = this.destination; - if (destination.closed) { - this._complete(); - return; - } - var index = this.index++; - if (this.active < this.concurrent) { - destination.next(value); - try { - var project = this.project; - var result = project(value, index); - if (!this.scheduler) { - this.subscribeToProjection(result, value, index); - } - else { - var state = { subscriber: this, result: result, value: value, index: index }; - var destination_1 = this.destination; - destination_1.add(this.scheduler.schedule(ExpandSubscriber.dispatch, 0, state)); - } - } - catch (e) { - destination.error(e); - } - } - else { - this.buffer.push(value); - } - }; - ExpandSubscriber.prototype.subscribeToProjection = function (result, value, index) { - this.active++; - var destination = this.destination; - destination.add(Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__["subscribeToResult"])(this, result, value, index)); - }; - ExpandSubscriber.prototype._complete = function () { - this.hasCompleted = true; - if (this.hasCompleted && this.active === 0) { - this.destination.complete(); - } - this.unsubscribe(); - }; - ExpandSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { - this._next(innerValue); - }; - ExpandSubscriber.prototype.notifyComplete = function (innerSub) { - var buffer = this.buffer; - var destination = this.destination; - destination.remove(innerSub); - this.active--; - if (buffer && buffer.length > 0) { - this._next(buffer.shift()); - } - if (this.hasCompleted && this.active === 0) { - this.destination.complete(); - } - }; - return ExpandSubscriber; -}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__["OuterSubscriber"])); - -//# sourceMappingURL=expand.js.map +//# sourceMappingURL=never.js.map /***/ }), @@ -26780,36 +26671,38 @@ var ExpandSubscriber = /*@__PURE__*/ (function (_super) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "finalize", function() { return finalize; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); -/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(177); -/** PURE_IMPORTS_START tslib,_Subscriber,_Subscription PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "onErrorResumeNext", function() { return onErrorResumeNext; }); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(170); +/* harmony import */ var _from__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(243); +/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(178); +/* harmony import */ var _empty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(203); +/** PURE_IMPORTS_START _Observable,_from,_util_isArray,_empty PURE_IMPORTS_END */ -function finalize(callback) { - return function (source) { return source.lift(new FinallyOperator(callback)); }; -} -var FinallyOperator = /*@__PURE__*/ (function () { - function FinallyOperator(callback) { - this.callback = callback; + +function onErrorResumeNext() { + var sources = []; + for (var _i = 0; _i < arguments.length; _i++) { + sources[_i] = arguments[_i]; } - FinallyOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new FinallySubscriber(subscriber, this.callback)); - }; - return FinallyOperator; -}()); -var FinallySubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](FinallySubscriber, _super); - function FinallySubscriber(destination, callback) { - var _this = _super.call(this, destination) || this; - _this.add(new _Subscription__WEBPACK_IMPORTED_MODULE_2__["Subscription"](callback)); - return _this; + if (sources.length === 0) { + return _empty__WEBPACK_IMPORTED_MODULE_3__["EMPTY"]; } - return FinallySubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); -//# sourceMappingURL=finalize.js.map + var first = sources[0], remainder = sources.slice(1); + if (sources.length === 1 && Object(_util_isArray__WEBPACK_IMPORTED_MODULE_2__["isArray"])(first)) { + return onErrorResumeNext.apply(void 0, first); + } + return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { + var subNext = function () { return subscriber.add(onErrorResumeNext.apply(void 0, remainder).subscribe(subscriber)); }; + return Object(_from__WEBPACK_IMPORTED_MODULE_1__["from"])(first).subscribe({ + next: function (value) { subscriber.next(value); }, + error: subNext, + complete: subNext, + }); + }); +} +//# sourceMappingURL=onErrorResumeNext.js.map /***/ }), @@ -26818,70 +26711,49 @@ var FinallySubscriber = /*@__PURE__*/ (function (_super) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "find", function() { return find; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "FindValueOperator", function() { return FindValueOperator; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "FindValueSubscriber", function() { return FindValueSubscriber; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); -/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "pairs", function() { return pairs; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "dispatch", function() { return dispatch; }); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(170); +/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(177); +/** PURE_IMPORTS_START _Observable,_Subscription PURE_IMPORTS_END */ -function find(predicate, thisArg) { - if (typeof predicate !== 'function') { - throw new TypeError('predicate is not a function'); - } - return function (source) { return source.lift(new FindValueOperator(predicate, source, false, thisArg)); }; -} -var FindValueOperator = /*@__PURE__*/ (function () { - function FindValueOperator(predicate, source, yieldIndex, thisArg) { - this.predicate = predicate; - this.source = source; - this.yieldIndex = yieldIndex; - this.thisArg = thisArg; +function pairs(obj, scheduler) { + if (!scheduler) { + return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { + var keys = Object.keys(obj); + for (var i = 0; i < keys.length && !subscriber.closed; i++) { + var key = keys[i]; + if (obj.hasOwnProperty(key)) { + subscriber.next([key, obj[key]]); + } + } + subscriber.complete(); + }); } - FindValueOperator.prototype.call = function (observer, source) { - return source.subscribe(new FindValueSubscriber(observer, this.predicate, this.source, this.yieldIndex, this.thisArg)); - }; - return FindValueOperator; -}()); - -var FindValueSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](FindValueSubscriber, _super); - function FindValueSubscriber(destination, predicate, source, yieldIndex, thisArg) { - var _this = _super.call(this, destination) || this; - _this.predicate = predicate; - _this.source = source; - _this.yieldIndex = yieldIndex; - _this.thisArg = thisArg; - _this.index = 0; - return _this; + else { + return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { + var keys = Object.keys(obj); + var subscription = new _Subscription__WEBPACK_IMPORTED_MODULE_1__["Subscription"](); + subscription.add(scheduler.schedule(dispatch, 0, { keys: keys, index: 0, subscriber: subscriber, subscription: subscription, obj: obj })); + return subscription; + }); } - FindValueSubscriber.prototype.notifyComplete = function (value) { - var destination = this.destination; - destination.next(value); - destination.complete(); - this.unsubscribe(); - }; - FindValueSubscriber.prototype._next = function (value) { - var _a = this, predicate = _a.predicate, thisArg = _a.thisArg; - var index = this.index++; - try { - var result = predicate.call(thisArg || this, value, index, this.source); - if (result) { - this.notifyComplete(this.yieldIndex ? index : value); - } +} +function dispatch(state) { + var keys = state.keys, index = state.index, subscriber = state.subscriber, subscription = state.subscription, obj = state.obj; + if (!subscriber.closed) { + if (index < keys.length) { + var key = keys[index]; + subscriber.next([key, obj[key]]); + subscription.add(this.schedule({ keys: keys, index: index + 1, subscriber: subscriber, subscription: subscription, obj: obj })); } - catch (err) { - this.destination.error(err); + else { + subscriber.complete(); } - }; - FindValueSubscriber.prototype._complete = function () { - this.notifyComplete(this.yieldIndex ? -1 : undefined); - }; - return FindValueSubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); - -//# sourceMappingURL=find.js.map + } +} +//# sourceMappingURL=pairs.js.map /***/ }), @@ -26890,14 +26762,23 @@ var FindValueSubscriber = /*@__PURE__*/ (function (_super) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "findIndex", function() { return findIndex; }); -/* harmony import */ var _operators_find__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(261); -/** PURE_IMPORTS_START _operators_find PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "partition", function() { return partition; }); +/* harmony import */ var _util_not__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(263); +/* harmony import */ var _util_subscribeTo__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(232); +/* harmony import */ var _operators_filter__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(264); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(170); +/** PURE_IMPORTS_START _util_not,_util_subscribeTo,_operators_filter,_Observable PURE_IMPORTS_END */ -function findIndex(predicate, thisArg) { - return function (source) { return source.lift(new _operators_find__WEBPACK_IMPORTED_MODULE_0__["FindValueOperator"](predicate, source, true, thisArg)); }; + + + +function partition(source, predicate, thisArg) { + return [ + Object(_operators_filter__WEBPACK_IMPORTED_MODULE_2__["filter"])(predicate, thisArg)(new _Observable__WEBPACK_IMPORTED_MODULE_3__["Observable"](Object(_util_subscribeTo__WEBPACK_IMPORTED_MODULE_1__["subscribeTo"])(source))), + Object(_operators_filter__WEBPACK_IMPORTED_MODULE_2__["filter"])(Object(_util_not__WEBPACK_IMPORTED_MODULE_0__["not"])(predicate, thisArg))(new _Observable__WEBPACK_IMPORTED_MODULE_3__["Observable"](Object(_util_subscribeTo__WEBPACK_IMPORTED_MODULE_1__["subscribeTo"])(source))) + ]; } -//# sourceMappingURL=findIndex.js.map +//# sourceMappingURL=partition.js.map /***/ }), @@ -26906,25 +26787,17 @@ function findIndex(predicate, thisArg) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "first", function() { return first; }); -/* harmony import */ var _util_EmptyError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(253); -/* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(251); -/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(254); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(238); -/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(252); -/* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(232); -/** PURE_IMPORTS_START _util_EmptyError,_filter,_take,_defaultIfEmpty,_throwIfEmpty,_util_identity PURE_IMPORTS_END */ - - - - - - -function first(predicate, defaultValue) { - var hasDefaultValue = arguments.length >= 2; - return function (source) { return source.pipe(predicate ? Object(_filter__WEBPACK_IMPORTED_MODULE_1__["filter"])(function (v, i) { return predicate(v, i, source); }) : _util_identity__WEBPACK_IMPORTED_MODULE_5__["identity"], Object(_take__WEBPACK_IMPORTED_MODULE_2__["take"])(1), hasDefaultValue ? Object(_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__["defaultIfEmpty"])(defaultValue) : Object(_throwIfEmpty__WEBPACK_IMPORTED_MODULE_4__["throwIfEmpty"])(function () { return new _util_EmptyError__WEBPACK_IMPORTED_MODULE_0__["EmptyError"](); })); }; +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "not", function() { return not; }); +/** PURE_IMPORTS_START PURE_IMPORTS_END */ +function not(pred, thisArg) { + function notPred() { + return !(notPred.pred.apply(notPred.thisArg, arguments)); + } + notPred.pred = pred; + notPred.thisArg = thisArg; + return notPred; } -//# sourceMappingURL=first.js.map +//# sourceMappingURL=not.js.map /***/ }), @@ -26933,195 +26806,52 @@ function first(predicate, defaultValue) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "groupBy", function() { return groupBy; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "GroupedObservable", function() { return GroupedObservable; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "filter", function() { return filter; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); /* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); -/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(177); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(193); -/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(265); -/** PURE_IMPORTS_START tslib,_Subscriber,_Subscription,_Observable,_Subject PURE_IMPORTS_END */ - - - +/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ -function groupBy(keySelector, elementSelector, durationSelector, subjectSelector) { - return function (source) { - return source.lift(new GroupByOperator(keySelector, elementSelector, durationSelector, subjectSelector)); +function filter(predicate, thisArg) { + return function filterOperatorFunction(source) { + return source.lift(new FilterOperator(predicate, thisArg)); }; } -var GroupByOperator = /*@__PURE__*/ (function () { - function GroupByOperator(keySelector, elementSelector, durationSelector, subjectSelector) { - this.keySelector = keySelector; - this.elementSelector = elementSelector; - this.durationSelector = durationSelector; - this.subjectSelector = subjectSelector; +var FilterOperator = /*@__PURE__*/ (function () { + function FilterOperator(predicate, thisArg) { + this.predicate = predicate; + this.thisArg = thisArg; } - GroupByOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new GroupBySubscriber(subscriber, this.keySelector, this.elementSelector, this.durationSelector, this.subjectSelector)); + FilterOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new FilterSubscriber(subscriber, this.predicate, this.thisArg)); }; - return GroupByOperator; + return FilterOperator; }()); -var GroupBySubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](GroupBySubscriber, _super); - function GroupBySubscriber(destination, keySelector, elementSelector, durationSelector, subjectSelector) { +var FilterSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](FilterSubscriber, _super); + function FilterSubscriber(destination, predicate, thisArg) { var _this = _super.call(this, destination) || this; - _this.keySelector = keySelector; - _this.elementSelector = elementSelector; - _this.durationSelector = durationSelector; - _this.subjectSelector = subjectSelector; - _this.groups = null; - _this.attemptedToUnsubscribe = false; + _this.predicate = predicate; + _this.thisArg = thisArg; _this.count = 0; return _this; } - GroupBySubscriber.prototype._next = function (value) { - var key; + FilterSubscriber.prototype._next = function (value) { + var result; try { - key = this.keySelector(value); + result = this.predicate.call(this.thisArg, value, this.count++); } catch (err) { - this.error(err); + this.destination.error(err); return; } - this._group(value, key); - }; - GroupBySubscriber.prototype._group = function (value, key) { - var groups = this.groups; - if (!groups) { - groups = this.groups = new Map(); - } - var group = groups.get(key); - var element; - if (this.elementSelector) { - try { - element = this.elementSelector(value); - } - catch (err) { - this.error(err); - } - } - else { - element = value; - } - if (!group) { - group = (this.subjectSelector ? this.subjectSelector() : new _Subject__WEBPACK_IMPORTED_MODULE_4__["Subject"]()); - groups.set(key, group); - var groupedObservable = new GroupedObservable(key, group, this); - this.destination.next(groupedObservable); - if (this.durationSelector) { - var duration = void 0; - try { - duration = this.durationSelector(new GroupedObservable(key, group)); - } - catch (err) { - this.error(err); - return; - } - this.add(duration.subscribe(new GroupDurationSubscriber(key, group, this))); - } - } - if (!group.closed) { - group.next(element); - } - }; - GroupBySubscriber.prototype._error = function (err) { - var groups = this.groups; - if (groups) { - groups.forEach(function (group, key) { - group.error(err); - }); - groups.clear(); - } - this.destination.error(err); - }; - GroupBySubscriber.prototype._complete = function () { - var groups = this.groups; - if (groups) { - groups.forEach(function (group, key) { - group.complete(); - }); - groups.clear(); - } - this.destination.complete(); - }; - GroupBySubscriber.prototype.removeGroup = function (key) { - this.groups.delete(key); - }; - GroupBySubscriber.prototype.unsubscribe = function () { - if (!this.closed) { - this.attemptedToUnsubscribe = true; - if (this.count === 0) { - _super.prototype.unsubscribe.call(this); - } - } - }; - return GroupBySubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); -var GroupDurationSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](GroupDurationSubscriber, _super); - function GroupDurationSubscriber(key, group, parent) { - var _this = _super.call(this, group) || this; - _this.key = key; - _this.group = group; - _this.parent = parent; - return _this; - } - GroupDurationSubscriber.prototype._next = function (value) { - this.complete(); - }; - GroupDurationSubscriber.prototype._unsubscribe = function () { - var _a = this, parent = _a.parent, key = _a.key; - this.key = this.parent = null; - if (parent) { - parent.removeGroup(key); + if (result) { + this.destination.next(value); } }; - return GroupDurationSubscriber; + return FilterSubscriber; }(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); -var GroupedObservable = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](GroupedObservable, _super); - function GroupedObservable(key, groupSubject, refCountSubscription) { - var _this = _super.call(this) || this; - _this.key = key; - _this.groupSubject = groupSubject; - _this.refCountSubscription = refCountSubscription; - return _this; - } - GroupedObservable.prototype._subscribe = function (subscriber) { - var subscription = new _Subscription__WEBPACK_IMPORTED_MODULE_2__["Subscription"](); - var _a = this, refCountSubscription = _a.refCountSubscription, groupSubject = _a.groupSubject; - if (refCountSubscription && !refCountSubscription.closed) { - subscription.add(new InnerRefCountSubscription(refCountSubscription)); - } - subscription.add(groupSubject.subscribe(subscriber)); - return subscription; - }; - return GroupedObservable; -}(_Observable__WEBPACK_IMPORTED_MODULE_3__["Observable"])); - -var InnerRefCountSubscription = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](InnerRefCountSubscription, _super); - function InnerRefCountSubscription(parent) { - var _this = _super.call(this) || this; - _this.parent = parent; - parent.count++; - return _this; - } - InnerRefCountSubscription.prototype.unsubscribe = function () { - var parent = this.parent; - if (!parent.closed && !this.closed) { - _super.prototype.unsubscribe.call(this); - parent.count -= 1; - if (parent.count === 0 && parent.attemptedToUnsubscribe) { - parent.unsubscribe(); - } - } - }; - return InnerRefCountSubscription; -}(_Subscription__WEBPACK_IMPORTED_MODULE_2__["Subscription"])); -//# sourceMappingURL=groupBy.js.map +//# sourceMappingURL=filter.js.map /***/ }), @@ -27130,174 +26860,92 @@ var InnerRefCountSubscription = /*@__PURE__*/ (function (_super) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SubjectSubscriber", function() { return SubjectSubscriber; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Subject", function() { return Subject; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "AnonymousSubject", function() { return AnonymousSubject; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "race", function() { return race; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "RaceOperator", function() { return RaceOperator; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "RaceSubscriber", function() { return RaceSubscriber; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(193); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(172); -/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(177); -/* harmony import */ var _util_ObjectUnsubscribedError__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(266); -/* harmony import */ var _SubjectSubscription__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(267); -/* harmony import */ var _internal_symbol_rxSubscriber__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(181); -/** PURE_IMPORTS_START tslib,_Observable,_Subscriber,_Subscription,_util_ObjectUnsubscribedError,_SubjectSubscription,_internal_symbol_rxSubscriber PURE_IMPORTS_END */ - - - +/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(178); +/* harmony import */ var _fromArray__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(206); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(229); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(230); +/** PURE_IMPORTS_START tslib,_util_isArray,_fromArray,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ -var SubjectSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](SubjectSubscriber, _super); - function SubjectSubscriber(destination) { - var _this = _super.call(this, destination) || this; - _this.destination = destination; - return _this; - } - return SubjectSubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_2__["Subscriber"])); -var Subject = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](Subject, _super); - function Subject() { - var _this = _super.call(this) || this; - _this.observers = []; - _this.closed = false; - _this.isStopped = false; - _this.hasError = false; - _this.thrownError = null; - return _this; +function race() { + var observables = []; + for (var _i = 0; _i < arguments.length; _i++) { + observables[_i] = arguments[_i]; } - Subject.prototype[_internal_symbol_rxSubscriber__WEBPACK_IMPORTED_MODULE_6__["rxSubscriber"]] = function () { - return new SubjectSubscriber(this); - }; - Subject.prototype.lift = function (operator) { - var subject = new AnonymousSubject(this, this); - subject.operator = operator; - return subject; - }; - Subject.prototype.next = function (value) { - if (this.closed) { - throw new _util_ObjectUnsubscribedError__WEBPACK_IMPORTED_MODULE_4__["ObjectUnsubscribedError"](); - } - if (!this.isStopped) { - var observers = this.observers; - var len = observers.length; - var copy = observers.slice(); - for (var i = 0; i < len; i++) { - copy[i].next(value); - } - } - }; - Subject.prototype.error = function (err) { - if (this.closed) { - throw new _util_ObjectUnsubscribedError__WEBPACK_IMPORTED_MODULE_4__["ObjectUnsubscribedError"](); - } - this.hasError = true; - this.thrownError = err; - this.isStopped = true; - var observers = this.observers; - var len = observers.length; - var copy = observers.slice(); - for (var i = 0; i < len; i++) { - copy[i].error(err); - } - this.observers.length = 0; - }; - Subject.prototype.complete = function () { - if (this.closed) { - throw new _util_ObjectUnsubscribedError__WEBPACK_IMPORTED_MODULE_4__["ObjectUnsubscribedError"](); - } - this.isStopped = true; - var observers = this.observers; - var len = observers.length; - var copy = observers.slice(); - for (var i = 0; i < len; i++) { - copy[i].complete(); - } - this.observers.length = 0; - }; - Subject.prototype.unsubscribe = function () { - this.isStopped = true; - this.closed = true; - this.observers = null; - }; - Subject.prototype._trySubscribe = function (subscriber) { - if (this.closed) { - throw new _util_ObjectUnsubscribedError__WEBPACK_IMPORTED_MODULE_4__["ObjectUnsubscribedError"](); - } - else { - return _super.prototype._trySubscribe.call(this, subscriber); - } - }; - Subject.prototype._subscribe = function (subscriber) { - if (this.closed) { - throw new _util_ObjectUnsubscribedError__WEBPACK_IMPORTED_MODULE_4__["ObjectUnsubscribedError"](); - } - else if (this.hasError) { - subscriber.error(this.thrownError); - return _Subscription__WEBPACK_IMPORTED_MODULE_3__["Subscription"].EMPTY; - } - else if (this.isStopped) { - subscriber.complete(); - return _Subscription__WEBPACK_IMPORTED_MODULE_3__["Subscription"].EMPTY; + if (observables.length === 1) { + if (Object(_util_isArray__WEBPACK_IMPORTED_MODULE_1__["isArray"])(observables[0])) { + observables = observables[0]; } else { - this.observers.push(subscriber); - return new _SubjectSubscription__WEBPACK_IMPORTED_MODULE_5__["SubjectSubscription"](this, subscriber); + return observables[0]; } + } + return Object(_fromArray__WEBPACK_IMPORTED_MODULE_2__["fromArray"])(observables, undefined).lift(new RaceOperator()); +} +var RaceOperator = /*@__PURE__*/ (function () { + function RaceOperator() { + } + RaceOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new RaceSubscriber(subscriber)); }; - Subject.prototype.asObservable = function () { - var observable = new _Observable__WEBPACK_IMPORTED_MODULE_1__["Observable"](); - observable.source = this; - return observable; - }; - Subject.create = function (destination, source) { - return new AnonymousSubject(destination, source); - }; - return Subject; -}(_Observable__WEBPACK_IMPORTED_MODULE_1__["Observable"])); + return RaceOperator; +}()); -var AnonymousSubject = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](AnonymousSubject, _super); - function AnonymousSubject(destination, source) { - var _this = _super.call(this) || this; - _this.destination = destination; - _this.source = source; +var RaceSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](RaceSubscriber, _super); + function RaceSubscriber(destination) { + var _this = _super.call(this, destination) || this; + _this.hasFirst = false; + _this.observables = []; + _this.subscriptions = []; return _this; } - AnonymousSubject.prototype.next = function (value) { - var destination = this.destination; - if (destination && destination.next) { - destination.next(value); - } - }; - AnonymousSubject.prototype.error = function (err) { - var destination = this.destination; - if (destination && destination.error) { - this.destination.error(err); - } + RaceSubscriber.prototype._next = function (observable) { + this.observables.push(observable); }; - AnonymousSubject.prototype.complete = function () { - var destination = this.destination; - if (destination && destination.complete) { + RaceSubscriber.prototype._complete = function () { + var observables = this.observables; + var len = observables.length; + if (len === 0) { this.destination.complete(); } - }; - AnonymousSubject.prototype._subscribe = function (subscriber) { - var source = this.source; - if (source) { - return this.source.subscribe(subscriber); - } else { - return _Subscription__WEBPACK_IMPORTED_MODULE_3__["Subscription"].EMPTY; + for (var i = 0; i < len && !this.hasFirst; i++) { + var observable = observables[i]; + var subscription = Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__["subscribeToResult"])(this, observable, observable, i); + if (this.subscriptions) { + this.subscriptions.push(subscription); + } + this.add(subscription); + } + this.observables = null; } }; - return AnonymousSubject; -}(Subject)); + RaceSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { + if (!this.hasFirst) { + this.hasFirst = true; + for (var i = 0; i < this.subscriptions.length; i++) { + if (i !== outerIndex) { + var subscription = this.subscriptions[i]; + subscription.unsubscribe(); + this.remove(subscription); + } + } + this.subscriptions = null; + } + this.destination.next(innerValue); + }; + return RaceSubscriber; +}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__["OuterSubscriber"])); -//# sourceMappingURL=Subject.js.map +//# sourceMappingURL=race.js.map /***/ }), @@ -27306,20 +26954,57 @@ var AnonymousSubject = /*@__PURE__*/ (function (_super) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ObjectUnsubscribedError", function() { return ObjectUnsubscribedError; }); -/** PURE_IMPORTS_START PURE_IMPORTS_END */ -var ObjectUnsubscribedErrorImpl = /*@__PURE__*/ (function () { - function ObjectUnsubscribedErrorImpl() { - Error.call(this); - this.message = 'object unsubscribed'; - this.name = 'ObjectUnsubscribedError'; - return this; +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "range", function() { return range; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "dispatch", function() { return dispatch; }); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(170); +/** PURE_IMPORTS_START _Observable PURE_IMPORTS_END */ + +function range(start, count, scheduler) { + if (start === void 0) { + start = 0; } - ObjectUnsubscribedErrorImpl.prototype = /*@__PURE__*/ Object.create(Error.prototype); - return ObjectUnsubscribedErrorImpl; -})(); -var ObjectUnsubscribedError = ObjectUnsubscribedErrorImpl; -//# sourceMappingURL=ObjectUnsubscribedError.js.map + return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { + if (count === undefined) { + count = start; + start = 0; + } + var index = 0; + var current = start; + if (scheduler) { + return scheduler.schedule(dispatch, 0, { + index: index, count: count, start: start, subscriber: subscriber + }); + } + else { + do { + if (index++ >= count) { + subscriber.complete(); + break; + } + subscriber.next(current++); + if (subscriber.closed) { + break; + } + } while (true); + } + return undefined; + }); +} +function dispatch(state) { + var start = state.start, index = state.index, count = state.count, subscriber = state.subscriber; + if (index >= count) { + subscriber.complete(); + return; + } + subscriber.next(start); + if (subscriber.closed) { + return; + } + state.index = index + 1; + state.start = start + 1; + this.schedule(state); +} +//# sourceMappingURL=range.js.map /***/ }), @@ -27328,41 +27013,52 @@ var ObjectUnsubscribedError = ObjectUnsubscribedErrorImpl; "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SubjectSubscription", function() { return SubjectSubscription; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(177); -/** PURE_IMPORTS_START tslib,_Subscription PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timer", function() { return timer; }); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(170); +/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(215); +/* harmony import */ var _util_isNumeric__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(257); +/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(205); +/** PURE_IMPORTS_START _Observable,_scheduler_async,_util_isNumeric,_util_isScheduler PURE_IMPORTS_END */ -var SubjectSubscription = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](SubjectSubscription, _super); - function SubjectSubscription(subject, subscriber) { - var _this = _super.call(this) || this; - _this.subject = subject; - _this.subscriber = subscriber; - _this.closed = false; - return _this; - } - SubjectSubscription.prototype.unsubscribe = function () { - if (this.closed) { - return; - } - this.closed = true; - var subject = this.subject; - var observers = subject.observers; - this.subject = null; - if (!observers || observers.length === 0 || subject.isStopped || subject.closed) { - return; - } - var subscriberIndex = observers.indexOf(this.subscriber); - if (subscriberIndex !== -1) { - observers.splice(subscriberIndex, 1); - } - }; - return SubjectSubscription; -}(_Subscription__WEBPACK_IMPORTED_MODULE_1__["Subscription"])); -//# sourceMappingURL=SubjectSubscription.js.map + +function timer(dueTime, periodOrScheduler, scheduler) { + if (dueTime === void 0) { + dueTime = 0; + } + var period = -1; + if (Object(_util_isNumeric__WEBPACK_IMPORTED_MODULE_2__["isNumeric"])(periodOrScheduler)) { + period = Number(periodOrScheduler) < 1 && 1 || Number(periodOrScheduler); + } + else if (Object(_util_isScheduler__WEBPACK_IMPORTED_MODULE_3__["isScheduler"])(periodOrScheduler)) { + scheduler = periodOrScheduler; + } + if (!Object(_util_isScheduler__WEBPACK_IMPORTED_MODULE_3__["isScheduler"])(scheduler)) { + scheduler = _scheduler_async__WEBPACK_IMPORTED_MODULE_1__["async"]; + } + return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { + var due = Object(_util_isNumeric__WEBPACK_IMPORTED_MODULE_2__["isNumeric"])(dueTime) + ? dueTime + : (+dueTime - scheduler.now()); + return scheduler.schedule(dispatch, due, { + index: 0, period: period, subscriber: subscriber + }); + }); +} +function dispatch(state) { + var index = state.index, period = state.period, subscriber = state.subscriber; + subscriber.next(index); + if (subscriber.closed) { + return; + } + else if (period === -1) { + return subscriber.complete(); + } + state.index = index + 1; + this.schedule(state, period); +} +//# sourceMappingURL=timer.js.map /***/ }), @@ -27371,35 +27067,43 @@ var SubjectSubscription = /*@__PURE__*/ (function (_super) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ignoreElements", function() { return ignoreElements; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); -/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "using", function() { return using; }); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(170); +/* harmony import */ var _from__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(243); +/* harmony import */ var _empty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(203); +/** PURE_IMPORTS_START _Observable,_from,_empty PURE_IMPORTS_END */ -function ignoreElements() { - return function ignoreElementsOperatorFunction(source) { - return source.lift(new IgnoreElementsOperator()); - }; + +function using(resourceFactory, observableFactory) { + return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { + var resource; + try { + resource = resourceFactory(); + } + catch (err) { + subscriber.error(err); + return undefined; + } + var result; + try { + result = observableFactory(resource); + } + catch (err) { + subscriber.error(err); + return undefined; + } + var source = result ? Object(_from__WEBPACK_IMPORTED_MODULE_1__["from"])(result) : _empty__WEBPACK_IMPORTED_MODULE_2__["EMPTY"]; + var subscription = source.subscribe(subscriber); + return function () { + subscription.unsubscribe(); + if (resource) { + resource.unsubscribe(); + } + }; + }); } -var IgnoreElementsOperator = /*@__PURE__*/ (function () { - function IgnoreElementsOperator() { - } - IgnoreElementsOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new IgnoreElementsSubscriber(subscriber)); - }; - return IgnoreElementsOperator; -}()); -var IgnoreElementsSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](IgnoreElementsSubscriber, _super); - function IgnoreElementsSubscriber() { - return _super !== null && _super.apply(this, arguments) || this; - } - IgnoreElementsSubscriber.prototype._next = function (unused) { - }; - return IgnoreElementsSubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); -//# sourceMappingURL=ignoreElements.js.map +//# sourceMappingURL=using.js.map /***/ }), @@ -27408,8946 +27112,9248 @@ var IgnoreElementsSubscriber = /*@__PURE__*/ (function (_super) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isEmpty", function() { return isEmpty; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "zip", function() { return zip; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ZipOperator", function() { return ZipOperator; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ZipSubscriber", function() { return ZipSubscriber; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); -/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ +/* harmony import */ var _fromArray__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(206); +/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(178); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(172); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(229); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(230); +/* harmony import */ var _internal_symbol_iterator__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(235); +/** PURE_IMPORTS_START tslib,_fromArray,_util_isArray,_Subscriber,_OuterSubscriber,_util_subscribeToResult,_.._internal_symbol_iterator PURE_IMPORTS_END */ -function isEmpty() { - return function (source) { return source.lift(new IsEmptyOperator()); }; + + + + + +function zip() { + var observables = []; + for (var _i = 0; _i < arguments.length; _i++) { + observables[_i] = arguments[_i]; + } + var resultSelector = observables[observables.length - 1]; + if (typeof resultSelector === 'function') { + observables.pop(); + } + return Object(_fromArray__WEBPACK_IMPORTED_MODULE_1__["fromArray"])(observables, undefined).lift(new ZipOperator(resultSelector)); } -var IsEmptyOperator = /*@__PURE__*/ (function () { - function IsEmptyOperator() { +var ZipOperator = /*@__PURE__*/ (function () { + function ZipOperator(resultSelector) { + this.resultSelector = resultSelector; } - IsEmptyOperator.prototype.call = function (observer, source) { - return source.subscribe(new IsEmptySubscriber(observer)); + ZipOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new ZipSubscriber(subscriber, this.resultSelector)); }; - return IsEmptyOperator; + return ZipOperator; }()); -var IsEmptySubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](IsEmptySubscriber, _super); - function IsEmptySubscriber(destination) { - return _super.call(this, destination) || this; + +var ZipSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](ZipSubscriber, _super); + function ZipSubscriber(destination, resultSelector, values) { + if (values === void 0) { + values = Object.create(null); + } + var _this = _super.call(this, destination) || this; + _this.iterators = []; + _this.active = 0; + _this.resultSelector = (typeof resultSelector === 'function') ? resultSelector : null; + _this.values = values; + return _this; } - IsEmptySubscriber.prototype.notifyComplete = function (isEmpty) { - var destination = this.destination; - destination.next(isEmpty); - destination.complete(); + ZipSubscriber.prototype._next = function (value) { + var iterators = this.iterators; + if (Object(_util_isArray__WEBPACK_IMPORTED_MODULE_2__["isArray"])(value)) { + iterators.push(new StaticArrayIterator(value)); + } + else if (typeof value[_internal_symbol_iterator__WEBPACK_IMPORTED_MODULE_6__["iterator"]] === 'function') { + iterators.push(new StaticIterator(value[_internal_symbol_iterator__WEBPACK_IMPORTED_MODULE_6__["iterator"]]())); + } + else { + iterators.push(new ZipBufferIterator(this.destination, this, value)); + } }; - IsEmptySubscriber.prototype._next = function (value) { - this.notifyComplete(false); + ZipSubscriber.prototype._complete = function () { + var iterators = this.iterators; + var len = iterators.length; + this.unsubscribe(); + if (len === 0) { + this.destination.complete(); + return; + } + this.active = len; + for (var i = 0; i < len; i++) { + var iterator = iterators[i]; + if (iterator.stillUnsubscribed) { + var destination = this.destination; + destination.add(iterator.subscribe(iterator, i)); + } + else { + this.active--; + } + } }; - IsEmptySubscriber.prototype._complete = function () { - this.notifyComplete(true); + ZipSubscriber.prototype.notifyInactive = function () { + this.active--; + if (this.active === 0) { + this.destination.complete(); + } }; - return IsEmptySubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); -//# sourceMappingURL=isEmpty.js.map - - -/***/ }), -/* 270 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "last", function() { return last; }); -/* harmony import */ var _util_EmptyError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(253); -/* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(251); -/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(271); -/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(252); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(238); -/* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(232); -/** PURE_IMPORTS_START _util_EmptyError,_filter,_takeLast,_throwIfEmpty,_defaultIfEmpty,_util_identity PURE_IMPORTS_END */ - - - - - - -function last(predicate, defaultValue) { - var hasDefaultValue = arguments.length >= 2; - return function (source) { return source.pipe(predicate ? Object(_filter__WEBPACK_IMPORTED_MODULE_1__["filter"])(function (v, i) { return predicate(v, i, source); }) : _util_identity__WEBPACK_IMPORTED_MODULE_5__["identity"], Object(_takeLast__WEBPACK_IMPORTED_MODULE_2__["takeLast"])(1), hasDefaultValue ? Object(_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_4__["defaultIfEmpty"])(defaultValue) : Object(_throwIfEmpty__WEBPACK_IMPORTED_MODULE_3__["throwIfEmpty"])(function () { return new _util_EmptyError__WEBPACK_IMPORTED_MODULE_0__["EmptyError"](); })); }; -} -//# sourceMappingURL=last.js.map - - -/***/ }), -/* 271 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "takeLast", function() { return takeLast; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); -/* harmony import */ var _util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(250); -/* harmony import */ var _observable_empty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(242); -/** PURE_IMPORTS_START tslib,_Subscriber,_util_ArgumentOutOfRangeError,_observable_empty PURE_IMPORTS_END */ - - - - -function takeLast(count) { - return function takeLastOperatorFunction(source) { - if (count === 0) { - return Object(_observable_empty__WEBPACK_IMPORTED_MODULE_3__["empty"])(); + ZipSubscriber.prototype.checkIterators = function () { + var iterators = this.iterators; + var len = iterators.length; + var destination = this.destination; + for (var i = 0; i < len; i++) { + var iterator = iterators[i]; + if (typeof iterator.hasValue === 'function' && !iterator.hasValue()) { + return; + } + } + var shouldComplete = false; + var args = []; + for (var i = 0; i < len; i++) { + var iterator = iterators[i]; + var result = iterator.next(); + if (iterator.hasCompleted()) { + shouldComplete = true; + } + if (result.done) { + destination.complete(); + return; + } + args.push(result.value); + } + if (this.resultSelector) { + this._tryresultSelector(args); } else { - return source.lift(new TakeLastOperator(count)); + destination.next(args); + } + if (shouldComplete) { + destination.complete(); } }; -} -var TakeLastOperator = /*@__PURE__*/ (function () { - function TakeLastOperator(total) { - this.total = total; - if (this.total < 0) { - throw new _util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_2__["ArgumentOutOfRangeError"]; + ZipSubscriber.prototype._tryresultSelector = function (args) { + var result; + try { + result = this.resultSelector.apply(this, args); + } + catch (err) { + this.destination.error(err); + return; } + this.destination.next(result); + }; + return ZipSubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_3__["Subscriber"])); + +var StaticIterator = /*@__PURE__*/ (function () { + function StaticIterator(iterator) { + this.iterator = iterator; + this.nextResult = iterator.next(); } - TakeLastOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new TakeLastSubscriber(subscriber, this.total)); + StaticIterator.prototype.hasValue = function () { + return true; }; - return TakeLastOperator; + StaticIterator.prototype.next = function () { + var result = this.nextResult; + this.nextResult = this.iterator.next(); + return result; + }; + StaticIterator.prototype.hasCompleted = function () { + var nextResult = this.nextResult; + return nextResult && nextResult.done; + }; + return StaticIterator; }()); -var TakeLastSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](TakeLastSubscriber, _super); - function TakeLastSubscriber(destination, total) { +var StaticArrayIterator = /*@__PURE__*/ (function () { + function StaticArrayIterator(array) { + this.array = array; + this.index = 0; + this.length = 0; + this.length = array.length; + } + StaticArrayIterator.prototype[_internal_symbol_iterator__WEBPACK_IMPORTED_MODULE_6__["iterator"]] = function () { + return this; + }; + StaticArrayIterator.prototype.next = function (value) { + var i = this.index++; + var array = this.array; + return i < this.length ? { value: array[i], done: false } : { value: null, done: true }; + }; + StaticArrayIterator.prototype.hasValue = function () { + return this.array.length > this.index; + }; + StaticArrayIterator.prototype.hasCompleted = function () { + return this.array.length === this.index; + }; + return StaticArrayIterator; +}()); +var ZipBufferIterator = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](ZipBufferIterator, _super); + function ZipBufferIterator(destination, parent, observable) { var _this = _super.call(this, destination) || this; - _this.total = total; - _this.ring = new Array(); - _this.count = 0; + _this.parent = parent; + _this.observable = observable; + _this.stillUnsubscribed = true; + _this.buffer = []; + _this.isComplete = false; return _this; } - TakeLastSubscriber.prototype._next = function (value) { - var ring = this.ring; - var total = this.total; - var count = this.count++; - if (ring.length < total) { - ring.push(value); + ZipBufferIterator.prototype[_internal_symbol_iterator__WEBPACK_IMPORTED_MODULE_6__["iterator"]] = function () { + return this; + }; + ZipBufferIterator.prototype.next = function () { + var buffer = this.buffer; + if (buffer.length === 0 && this.isComplete) { + return { value: null, done: true }; } else { - var index = count % total; - ring[index] = value; + return { value: buffer.shift(), done: false }; } }; - TakeLastSubscriber.prototype._complete = function () { - var destination = this.destination; - var count = this.count; - if (count > 0) { - var total = this.count >= this.total ? this.total : this.count; - var ring = this.ring; - for (var i = 0; i < total; i++) { - var idx = (count++) % total; - destination.next(ring[idx]); - } + ZipBufferIterator.prototype.hasValue = function () { + return this.buffer.length > 0; + }; + ZipBufferIterator.prototype.hasCompleted = function () { + return this.buffer.length === 0 && this.isComplete; + }; + ZipBufferIterator.prototype.notifyComplete = function () { + if (this.buffer.length > 0) { + this.isComplete = true; + this.parent.notifyInactive(); + } + else { + this.destination.complete(); } - destination.complete(); }; - return TakeLastSubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); -//# sourceMappingURL=takeLast.js.map + ZipBufferIterator.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { + this.buffer.push(innerValue); + this.parent.checkIterators(); + }; + ZipBufferIterator.prototype.subscribe = function (value, index) { + return Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_5__["subscribeToResult"])(this, this.observable, this, index); + }; + return ZipBufferIterator; +}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_4__["OuterSubscriber"])); +//# sourceMappingURL=zip.js.map /***/ }), -/* 272 */ +/* 270 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "mapTo", function() { return mapTo; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); -/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ +/* harmony import */ var _internal_operators_audit__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(271); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "audit", function() { return _internal_operators_audit__WEBPACK_IMPORTED_MODULE_0__["audit"]; }); + +/* harmony import */ var _internal_operators_auditTime__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(272); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "auditTime", function() { return _internal_operators_auditTime__WEBPACK_IMPORTED_MODULE_1__["auditTime"]; }); + +/* harmony import */ var _internal_operators_buffer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(273); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buffer", function() { return _internal_operators_buffer__WEBPACK_IMPORTED_MODULE_2__["buffer"]; }); + +/* harmony import */ var _internal_operators_bufferCount__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(274); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferCount", function() { return _internal_operators_bufferCount__WEBPACK_IMPORTED_MODULE_3__["bufferCount"]; }); + +/* harmony import */ var _internal_operators_bufferTime__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(275); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferTime", function() { return _internal_operators_bufferTime__WEBPACK_IMPORTED_MODULE_4__["bufferTime"]; }); + +/* harmony import */ var _internal_operators_bufferToggle__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(276); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferToggle", function() { return _internal_operators_bufferToggle__WEBPACK_IMPORTED_MODULE_5__["bufferToggle"]; }); + +/* harmony import */ var _internal_operators_bufferWhen__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(277); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bufferWhen", function() { return _internal_operators_bufferWhen__WEBPACK_IMPORTED_MODULE_6__["bufferWhen"]; }); + +/* harmony import */ var _internal_operators_catchError__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(278); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "catchError", function() { return _internal_operators_catchError__WEBPACK_IMPORTED_MODULE_7__["catchError"]; }); + +/* harmony import */ var _internal_operators_combineAll__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(279); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "combineAll", function() { return _internal_operators_combineAll__WEBPACK_IMPORTED_MODULE_8__["combineAll"]; }); + +/* harmony import */ var _internal_operators_combineLatest__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(280); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "combineLatest", function() { return _internal_operators_combineLatest__WEBPACK_IMPORTED_MODULE_9__["combineLatest"]; }); + +/* harmony import */ var _internal_operators_concat__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(281); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concat", function() { return _internal_operators_concat__WEBPACK_IMPORTED_MODULE_10__["concat"]; }); + +/* harmony import */ var _internal_operators_concatAll__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(240); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concatAll", function() { return _internal_operators_concatAll__WEBPACK_IMPORTED_MODULE_11__["concatAll"]; }); + +/* harmony import */ var _internal_operators_concatMap__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(282); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concatMap", function() { return _internal_operators_concatMap__WEBPACK_IMPORTED_MODULE_12__["concatMap"]; }); + +/* harmony import */ var _internal_operators_concatMapTo__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(283); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concatMapTo", function() { return _internal_operators_concatMapTo__WEBPACK_IMPORTED_MODULE_13__["concatMapTo"]; }); + +/* harmony import */ var _internal_operators_count__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(284); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "count", function() { return _internal_operators_count__WEBPACK_IMPORTED_MODULE_14__["count"]; }); + +/* harmony import */ var _internal_operators_debounce__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(285); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "debounce", function() { return _internal_operators_debounce__WEBPACK_IMPORTED_MODULE_15__["debounce"]; }); + +/* harmony import */ var _internal_operators_debounceTime__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(286); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "debounceTime", function() { return _internal_operators_debounceTime__WEBPACK_IMPORTED_MODULE_16__["debounceTime"]; }); + +/* harmony import */ var _internal_operators_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(287); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "defaultIfEmpty", function() { return _internal_operators_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_17__["defaultIfEmpty"]; }); + +/* harmony import */ var _internal_operators_delay__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(288); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "delay", function() { return _internal_operators_delay__WEBPACK_IMPORTED_MODULE_18__["delay"]; }); + +/* harmony import */ var _internal_operators_delayWhen__WEBPACK_IMPORTED_MODULE_19__ = __webpack_require__(290); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "delayWhen", function() { return _internal_operators_delayWhen__WEBPACK_IMPORTED_MODULE_19__["delayWhen"]; }); + +/* harmony import */ var _internal_operators_dematerialize__WEBPACK_IMPORTED_MODULE_20__ = __webpack_require__(291); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "dematerialize", function() { return _internal_operators_dematerialize__WEBPACK_IMPORTED_MODULE_20__["dematerialize"]; }); + +/* harmony import */ var _internal_operators_distinct__WEBPACK_IMPORTED_MODULE_21__ = __webpack_require__(292); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "distinct", function() { return _internal_operators_distinct__WEBPACK_IMPORTED_MODULE_21__["distinct"]; }); + +/* harmony import */ var _internal_operators_distinctUntilChanged__WEBPACK_IMPORTED_MODULE_22__ = __webpack_require__(293); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "distinctUntilChanged", function() { return _internal_operators_distinctUntilChanged__WEBPACK_IMPORTED_MODULE_22__["distinctUntilChanged"]; }); + +/* harmony import */ var _internal_operators_distinctUntilKeyChanged__WEBPACK_IMPORTED_MODULE_23__ = __webpack_require__(294); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "distinctUntilKeyChanged", function() { return _internal_operators_distinctUntilKeyChanged__WEBPACK_IMPORTED_MODULE_23__["distinctUntilKeyChanged"]; }); + +/* harmony import */ var _internal_operators_elementAt__WEBPACK_IMPORTED_MODULE_24__ = __webpack_require__(295); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "elementAt", function() { return _internal_operators_elementAt__WEBPACK_IMPORTED_MODULE_24__["elementAt"]; }); + +/* harmony import */ var _internal_operators_endWith__WEBPACK_IMPORTED_MODULE_25__ = __webpack_require__(298); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "endWith", function() { return _internal_operators_endWith__WEBPACK_IMPORTED_MODULE_25__["endWith"]; }); + +/* harmony import */ var _internal_operators_every__WEBPACK_IMPORTED_MODULE_26__ = __webpack_require__(299); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "every", function() { return _internal_operators_every__WEBPACK_IMPORTED_MODULE_26__["every"]; }); + +/* harmony import */ var _internal_operators_exhaust__WEBPACK_IMPORTED_MODULE_27__ = __webpack_require__(300); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "exhaust", function() { return _internal_operators_exhaust__WEBPACK_IMPORTED_MODULE_27__["exhaust"]; }); + +/* harmony import */ var _internal_operators_exhaustMap__WEBPACK_IMPORTED_MODULE_28__ = __webpack_require__(301); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "exhaustMap", function() { return _internal_operators_exhaustMap__WEBPACK_IMPORTED_MODULE_28__["exhaustMap"]; }); + +/* harmony import */ var _internal_operators_expand__WEBPACK_IMPORTED_MODULE_29__ = __webpack_require__(302); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "expand", function() { return _internal_operators_expand__WEBPACK_IMPORTED_MODULE_29__["expand"]; }); + +/* harmony import */ var _internal_operators_filter__WEBPACK_IMPORTED_MODULE_30__ = __webpack_require__(264); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "filter", function() { return _internal_operators_filter__WEBPACK_IMPORTED_MODULE_30__["filter"]; }); + +/* harmony import */ var _internal_operators_finalize__WEBPACK_IMPORTED_MODULE_31__ = __webpack_require__(303); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "finalize", function() { return _internal_operators_finalize__WEBPACK_IMPORTED_MODULE_31__["finalize"]; }); + +/* harmony import */ var _internal_operators_find__WEBPACK_IMPORTED_MODULE_32__ = __webpack_require__(304); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "find", function() { return _internal_operators_find__WEBPACK_IMPORTED_MODULE_32__["find"]; }); + +/* harmony import */ var _internal_operators_findIndex__WEBPACK_IMPORTED_MODULE_33__ = __webpack_require__(305); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "findIndex", function() { return _internal_operators_findIndex__WEBPACK_IMPORTED_MODULE_33__["findIndex"]; }); + +/* harmony import */ var _internal_operators_first__WEBPACK_IMPORTED_MODULE_34__ = __webpack_require__(306); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "first", function() { return _internal_operators_first__WEBPACK_IMPORTED_MODULE_34__["first"]; }); + +/* harmony import */ var _internal_operators_groupBy__WEBPACK_IMPORTED_MODULE_35__ = __webpack_require__(191); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "groupBy", function() { return _internal_operators_groupBy__WEBPACK_IMPORTED_MODULE_35__["groupBy"]; }); + +/* harmony import */ var _internal_operators_ignoreElements__WEBPACK_IMPORTED_MODULE_36__ = __webpack_require__(307); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "ignoreElements", function() { return _internal_operators_ignoreElements__WEBPACK_IMPORTED_MODULE_36__["ignoreElements"]; }); + +/* harmony import */ var _internal_operators_isEmpty__WEBPACK_IMPORTED_MODULE_37__ = __webpack_require__(308); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "isEmpty", function() { return _internal_operators_isEmpty__WEBPACK_IMPORTED_MODULE_37__["isEmpty"]; }); + +/* harmony import */ var _internal_operators_last__WEBPACK_IMPORTED_MODULE_38__ = __webpack_require__(309); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "last", function() { return _internal_operators_last__WEBPACK_IMPORTED_MODULE_38__["last"]; }); + +/* harmony import */ var _internal_operators_map__WEBPACK_IMPORTED_MODULE_39__ = __webpack_require__(226); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "map", function() { return _internal_operators_map__WEBPACK_IMPORTED_MODULE_39__["map"]; }); + +/* harmony import */ var _internal_operators_mapTo__WEBPACK_IMPORTED_MODULE_40__ = __webpack_require__(311); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mapTo", function() { return _internal_operators_mapTo__WEBPACK_IMPORTED_MODULE_40__["mapTo"]; }); + +/* harmony import */ var _internal_operators_materialize__WEBPACK_IMPORTED_MODULE_41__ = __webpack_require__(312); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "materialize", function() { return _internal_operators_materialize__WEBPACK_IMPORTED_MODULE_41__["materialize"]; }); + +/* harmony import */ var _internal_operators_max__WEBPACK_IMPORTED_MODULE_42__ = __webpack_require__(313); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "max", function() { return _internal_operators_max__WEBPACK_IMPORTED_MODULE_42__["max"]; }); + +/* harmony import */ var _internal_operators_merge__WEBPACK_IMPORTED_MODULE_43__ = __webpack_require__(316); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "merge", function() { return _internal_operators_merge__WEBPACK_IMPORTED_MODULE_43__["merge"]; }); + +/* harmony import */ var _internal_operators_mergeAll__WEBPACK_IMPORTED_MODULE_44__ = __webpack_require__(241); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mergeAll", function() { return _internal_operators_mergeAll__WEBPACK_IMPORTED_MODULE_44__["mergeAll"]; }); + +/* harmony import */ var _internal_operators_mergeMap__WEBPACK_IMPORTED_MODULE_45__ = __webpack_require__(242); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mergeMap", function() { return _internal_operators_mergeMap__WEBPACK_IMPORTED_MODULE_45__["mergeMap"]; }); + +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "flatMap", function() { return _internal_operators_mergeMap__WEBPACK_IMPORTED_MODULE_45__["mergeMap"]; }); + +/* harmony import */ var _internal_operators_mergeMapTo__WEBPACK_IMPORTED_MODULE_46__ = __webpack_require__(317); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mergeMapTo", function() { return _internal_operators_mergeMapTo__WEBPACK_IMPORTED_MODULE_46__["mergeMapTo"]; }); + +/* harmony import */ var _internal_operators_mergeScan__WEBPACK_IMPORTED_MODULE_47__ = __webpack_require__(318); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "mergeScan", function() { return _internal_operators_mergeScan__WEBPACK_IMPORTED_MODULE_47__["mergeScan"]; }); + +/* harmony import */ var _internal_operators_min__WEBPACK_IMPORTED_MODULE_48__ = __webpack_require__(319); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "min", function() { return _internal_operators_min__WEBPACK_IMPORTED_MODULE_48__["min"]; }); + +/* harmony import */ var _internal_operators_multicast__WEBPACK_IMPORTED_MODULE_49__ = __webpack_require__(320); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "multicast", function() { return _internal_operators_multicast__WEBPACK_IMPORTED_MODULE_49__["multicast"]; }); + +/* harmony import */ var _internal_operators_observeOn__WEBPACK_IMPORTED_MODULE_50__ = __webpack_require__(201); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "observeOn", function() { return _internal_operators_observeOn__WEBPACK_IMPORTED_MODULE_50__["observeOn"]; }); + +/* harmony import */ var _internal_operators_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_51__ = __webpack_require__(321); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "onErrorResumeNext", function() { return _internal_operators_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_51__["onErrorResumeNext"]; }); + +/* harmony import */ var _internal_operators_pairwise__WEBPACK_IMPORTED_MODULE_52__ = __webpack_require__(322); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "pairwise", function() { return _internal_operators_pairwise__WEBPACK_IMPORTED_MODULE_52__["pairwise"]; }); + +/* harmony import */ var _internal_operators_partition__WEBPACK_IMPORTED_MODULE_53__ = __webpack_require__(323); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "partition", function() { return _internal_operators_partition__WEBPACK_IMPORTED_MODULE_53__["partition"]; }); + +/* harmony import */ var _internal_operators_pluck__WEBPACK_IMPORTED_MODULE_54__ = __webpack_require__(324); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "pluck", function() { return _internal_operators_pluck__WEBPACK_IMPORTED_MODULE_54__["pluck"]; }); + +/* harmony import */ var _internal_operators_publish__WEBPACK_IMPORTED_MODULE_55__ = __webpack_require__(325); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publish", function() { return _internal_operators_publish__WEBPACK_IMPORTED_MODULE_55__["publish"]; }); + +/* harmony import */ var _internal_operators_publishBehavior__WEBPACK_IMPORTED_MODULE_56__ = __webpack_require__(326); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publishBehavior", function() { return _internal_operators_publishBehavior__WEBPACK_IMPORTED_MODULE_56__["publishBehavior"]; }); + +/* harmony import */ var _internal_operators_publishLast__WEBPACK_IMPORTED_MODULE_57__ = __webpack_require__(327); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publishLast", function() { return _internal_operators_publishLast__WEBPACK_IMPORTED_MODULE_57__["publishLast"]; }); + +/* harmony import */ var _internal_operators_publishReplay__WEBPACK_IMPORTED_MODULE_58__ = __webpack_require__(328); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "publishReplay", function() { return _internal_operators_publishReplay__WEBPACK_IMPORTED_MODULE_58__["publishReplay"]; }); + +/* harmony import */ var _internal_operators_race__WEBPACK_IMPORTED_MODULE_59__ = __webpack_require__(329); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "race", function() { return _internal_operators_race__WEBPACK_IMPORTED_MODULE_59__["race"]; }); + +/* harmony import */ var _internal_operators_reduce__WEBPACK_IMPORTED_MODULE_60__ = __webpack_require__(314); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "reduce", function() { return _internal_operators_reduce__WEBPACK_IMPORTED_MODULE_60__["reduce"]; }); + +/* harmony import */ var _internal_operators_repeat__WEBPACK_IMPORTED_MODULE_61__ = __webpack_require__(330); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "repeat", function() { return _internal_operators_repeat__WEBPACK_IMPORTED_MODULE_61__["repeat"]; }); + +/* harmony import */ var _internal_operators_repeatWhen__WEBPACK_IMPORTED_MODULE_62__ = __webpack_require__(331); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "repeatWhen", function() { return _internal_operators_repeatWhen__WEBPACK_IMPORTED_MODULE_62__["repeatWhen"]; }); + +/* harmony import */ var _internal_operators_retry__WEBPACK_IMPORTED_MODULE_63__ = __webpack_require__(332); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "retry", function() { return _internal_operators_retry__WEBPACK_IMPORTED_MODULE_63__["retry"]; }); + +/* harmony import */ var _internal_operators_retryWhen__WEBPACK_IMPORTED_MODULE_64__ = __webpack_require__(333); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "retryWhen", function() { return _internal_operators_retryWhen__WEBPACK_IMPORTED_MODULE_64__["retryWhen"]; }); + +/* harmony import */ var _internal_operators_refCount__WEBPACK_IMPORTED_MODULE_65__ = __webpack_require__(190); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "refCount", function() { return _internal_operators_refCount__WEBPACK_IMPORTED_MODULE_65__["refCount"]; }); + +/* harmony import */ var _internal_operators_sample__WEBPACK_IMPORTED_MODULE_66__ = __webpack_require__(334); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "sample", function() { return _internal_operators_sample__WEBPACK_IMPORTED_MODULE_66__["sample"]; }); + +/* harmony import */ var _internal_operators_sampleTime__WEBPACK_IMPORTED_MODULE_67__ = __webpack_require__(335); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "sampleTime", function() { return _internal_operators_sampleTime__WEBPACK_IMPORTED_MODULE_67__["sampleTime"]; }); + +/* harmony import */ var _internal_operators_scan__WEBPACK_IMPORTED_MODULE_68__ = __webpack_require__(315); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "scan", function() { return _internal_operators_scan__WEBPACK_IMPORTED_MODULE_68__["scan"]; }); + +/* harmony import */ var _internal_operators_sequenceEqual__WEBPACK_IMPORTED_MODULE_69__ = __webpack_require__(336); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "sequenceEqual", function() { return _internal_operators_sequenceEqual__WEBPACK_IMPORTED_MODULE_69__["sequenceEqual"]; }); + +/* harmony import */ var _internal_operators_share__WEBPACK_IMPORTED_MODULE_70__ = __webpack_require__(337); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "share", function() { return _internal_operators_share__WEBPACK_IMPORTED_MODULE_70__["share"]; }); + +/* harmony import */ var _internal_operators_shareReplay__WEBPACK_IMPORTED_MODULE_71__ = __webpack_require__(338); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "shareReplay", function() { return _internal_operators_shareReplay__WEBPACK_IMPORTED_MODULE_71__["shareReplay"]; }); + +/* harmony import */ var _internal_operators_single__WEBPACK_IMPORTED_MODULE_72__ = __webpack_require__(339); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "single", function() { return _internal_operators_single__WEBPACK_IMPORTED_MODULE_72__["single"]; }); + +/* harmony import */ var _internal_operators_skip__WEBPACK_IMPORTED_MODULE_73__ = __webpack_require__(340); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skip", function() { return _internal_operators_skip__WEBPACK_IMPORTED_MODULE_73__["skip"]; }); + +/* harmony import */ var _internal_operators_skipLast__WEBPACK_IMPORTED_MODULE_74__ = __webpack_require__(341); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skipLast", function() { return _internal_operators_skipLast__WEBPACK_IMPORTED_MODULE_74__["skipLast"]; }); + +/* harmony import */ var _internal_operators_skipUntil__WEBPACK_IMPORTED_MODULE_75__ = __webpack_require__(342); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skipUntil", function() { return _internal_operators_skipUntil__WEBPACK_IMPORTED_MODULE_75__["skipUntil"]; }); + +/* harmony import */ var _internal_operators_skipWhile__WEBPACK_IMPORTED_MODULE_76__ = __webpack_require__(343); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "skipWhile", function() { return _internal_operators_skipWhile__WEBPACK_IMPORTED_MODULE_76__["skipWhile"]; }); + +/* harmony import */ var _internal_operators_startWith__WEBPACK_IMPORTED_MODULE_77__ = __webpack_require__(344); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "startWith", function() { return _internal_operators_startWith__WEBPACK_IMPORTED_MODULE_77__["startWith"]; }); + +/* harmony import */ var _internal_operators_subscribeOn__WEBPACK_IMPORTED_MODULE_78__ = __webpack_require__(345); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "subscribeOn", function() { return _internal_operators_subscribeOn__WEBPACK_IMPORTED_MODULE_78__["subscribeOn"]; }); + +/* harmony import */ var _internal_operators_switchAll__WEBPACK_IMPORTED_MODULE_79__ = __webpack_require__(347); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "switchAll", function() { return _internal_operators_switchAll__WEBPACK_IMPORTED_MODULE_79__["switchAll"]; }); + +/* harmony import */ var _internal_operators_switchMap__WEBPACK_IMPORTED_MODULE_80__ = __webpack_require__(348); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "switchMap", function() { return _internal_operators_switchMap__WEBPACK_IMPORTED_MODULE_80__["switchMap"]; }); + +/* harmony import */ var _internal_operators_switchMapTo__WEBPACK_IMPORTED_MODULE_81__ = __webpack_require__(349); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "switchMapTo", function() { return _internal_operators_switchMapTo__WEBPACK_IMPORTED_MODULE_81__["switchMapTo"]; }); + +/* harmony import */ var _internal_operators_take__WEBPACK_IMPORTED_MODULE_82__ = __webpack_require__(297); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "take", function() { return _internal_operators_take__WEBPACK_IMPORTED_MODULE_82__["take"]; }); + +/* harmony import */ var _internal_operators_takeLast__WEBPACK_IMPORTED_MODULE_83__ = __webpack_require__(310); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "takeLast", function() { return _internal_operators_takeLast__WEBPACK_IMPORTED_MODULE_83__["takeLast"]; }); + +/* harmony import */ var _internal_operators_takeUntil__WEBPACK_IMPORTED_MODULE_84__ = __webpack_require__(350); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "takeUntil", function() { return _internal_operators_takeUntil__WEBPACK_IMPORTED_MODULE_84__["takeUntil"]; }); + +/* harmony import */ var _internal_operators_takeWhile__WEBPACK_IMPORTED_MODULE_85__ = __webpack_require__(351); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "takeWhile", function() { return _internal_operators_takeWhile__WEBPACK_IMPORTED_MODULE_85__["takeWhile"]; }); + +/* harmony import */ var _internal_operators_tap__WEBPACK_IMPORTED_MODULE_86__ = __webpack_require__(352); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "tap", function() { return _internal_operators_tap__WEBPACK_IMPORTED_MODULE_86__["tap"]; }); + +/* harmony import */ var _internal_operators_throttle__WEBPACK_IMPORTED_MODULE_87__ = __webpack_require__(353); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "throttle", function() { return _internal_operators_throttle__WEBPACK_IMPORTED_MODULE_87__["throttle"]; }); + +/* harmony import */ var _internal_operators_throttleTime__WEBPACK_IMPORTED_MODULE_88__ = __webpack_require__(354); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "throttleTime", function() { return _internal_operators_throttleTime__WEBPACK_IMPORTED_MODULE_88__["throttleTime"]; }); + +/* harmony import */ var _internal_operators_throwIfEmpty__WEBPACK_IMPORTED_MODULE_89__ = __webpack_require__(296); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "throwIfEmpty", function() { return _internal_operators_throwIfEmpty__WEBPACK_IMPORTED_MODULE_89__["throwIfEmpty"]; }); + +/* harmony import */ var _internal_operators_timeInterval__WEBPACK_IMPORTED_MODULE_90__ = __webpack_require__(355); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timeInterval", function() { return _internal_operators_timeInterval__WEBPACK_IMPORTED_MODULE_90__["timeInterval"]; }); + +/* harmony import */ var _internal_operators_timeout__WEBPACK_IMPORTED_MODULE_91__ = __webpack_require__(356); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timeout", function() { return _internal_operators_timeout__WEBPACK_IMPORTED_MODULE_91__["timeout"]; }); + +/* harmony import */ var _internal_operators_timeoutWith__WEBPACK_IMPORTED_MODULE_92__ = __webpack_require__(357); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timeoutWith", function() { return _internal_operators_timeoutWith__WEBPACK_IMPORTED_MODULE_92__["timeoutWith"]; }); + +/* harmony import */ var _internal_operators_timestamp__WEBPACK_IMPORTED_MODULE_93__ = __webpack_require__(358); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timestamp", function() { return _internal_operators_timestamp__WEBPACK_IMPORTED_MODULE_93__["timestamp"]; }); + +/* harmony import */ var _internal_operators_toArray__WEBPACK_IMPORTED_MODULE_94__ = __webpack_require__(359); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "toArray", function() { return _internal_operators_toArray__WEBPACK_IMPORTED_MODULE_94__["toArray"]; }); + +/* harmony import */ var _internal_operators_window__WEBPACK_IMPORTED_MODULE_95__ = __webpack_require__(360); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "window", function() { return _internal_operators_window__WEBPACK_IMPORTED_MODULE_95__["window"]; }); + +/* harmony import */ var _internal_operators_windowCount__WEBPACK_IMPORTED_MODULE_96__ = __webpack_require__(361); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowCount", function() { return _internal_operators_windowCount__WEBPACK_IMPORTED_MODULE_96__["windowCount"]; }); + +/* harmony import */ var _internal_operators_windowTime__WEBPACK_IMPORTED_MODULE_97__ = __webpack_require__(362); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowTime", function() { return _internal_operators_windowTime__WEBPACK_IMPORTED_MODULE_97__["windowTime"]; }); + +/* harmony import */ var _internal_operators_windowToggle__WEBPACK_IMPORTED_MODULE_98__ = __webpack_require__(363); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowToggle", function() { return _internal_operators_windowToggle__WEBPACK_IMPORTED_MODULE_98__["windowToggle"]; }); + +/* harmony import */ var _internal_operators_windowWhen__WEBPACK_IMPORTED_MODULE_99__ = __webpack_require__(364); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "windowWhen", function() { return _internal_operators_windowWhen__WEBPACK_IMPORTED_MODULE_99__["windowWhen"]; }); + +/* harmony import */ var _internal_operators_withLatestFrom__WEBPACK_IMPORTED_MODULE_100__ = __webpack_require__(365); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "withLatestFrom", function() { return _internal_operators_withLatestFrom__WEBPACK_IMPORTED_MODULE_100__["withLatestFrom"]; }); + +/* harmony import */ var _internal_operators_zip__WEBPACK_IMPORTED_MODULE_101__ = __webpack_require__(366); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "zip", function() { return _internal_operators_zip__WEBPACK_IMPORTED_MODULE_101__["zip"]; }); + +/* harmony import */ var _internal_operators_zipAll__WEBPACK_IMPORTED_MODULE_102__ = __webpack_require__(367); +/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "zipAll", function() { return _internal_operators_zipAll__WEBPACK_IMPORTED_MODULE_102__["zipAll"]; }); + +/** PURE_IMPORTS_START PURE_IMPORTS_END */ + + + + + + + + + + + + + + + + + + + + -function mapTo(value) { - return function (source) { return source.lift(new MapToOperator(value)); }; -} -var MapToOperator = /*@__PURE__*/ (function () { - function MapToOperator(value) { - this.value = value; - } - MapToOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new MapToSubscriber(subscriber, this.value)); - }; - return MapToOperator; -}()); -var MapToSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](MapToSubscriber, _super); - function MapToSubscriber(destination, value) { - var _this = _super.call(this, destination) || this; - _this.value = value; - return _this; - } - MapToSubscriber.prototype._next = function (x) { - this.destination.next(this.value); - }; - return MapToSubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); -//# sourceMappingURL=mapTo.js.map -/***/ }), -/* 273 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "materialize", function() { return materialize; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); -/* harmony import */ var _Notification__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(241); -/** PURE_IMPORTS_START tslib,_Subscriber,_Notification PURE_IMPORTS_END */ -function materialize() { - return function materializeOperatorFunction(source) { - return source.lift(new MaterializeOperator()); - }; -} -var MaterializeOperator = /*@__PURE__*/ (function () { - function MaterializeOperator() { - } - MaterializeOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new MaterializeSubscriber(subscriber)); - }; - return MaterializeOperator; -}()); -var MaterializeSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](MaterializeSubscriber, _super); - function MaterializeSubscriber(destination) { - return _super.call(this, destination) || this; - } - MaterializeSubscriber.prototype._next = function (value) { - this.destination.next(_Notification__WEBPACK_IMPORTED_MODULE_2__["Notification"].createNext(value)); - }; - MaterializeSubscriber.prototype._error = function (err) { - var destination = this.destination; - destination.next(_Notification__WEBPACK_IMPORTED_MODULE_2__["Notification"].createError(err)); - destination.complete(); - }; - MaterializeSubscriber.prototype._complete = function () { - var destination = this.destination; - destination.next(_Notification__WEBPACK_IMPORTED_MODULE_2__["Notification"].createComplete()); - destination.complete(); - }; - return MaterializeSubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); -//# sourceMappingURL=materialize.js.map -/***/ }), -/* 274 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "max", function() { return max; }); -/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(275); -/** PURE_IMPORTS_START _reduce PURE_IMPORTS_END */ -function max(comparer) { - var max = (typeof comparer === 'function') - ? function (x, y) { return comparer(x, y) > 0 ? x : y; } - : function (x, y) { return x > y ? x : y; }; - return Object(_reduce__WEBPACK_IMPORTED_MODULE_0__["reduce"])(max); -} -//# sourceMappingURL=max.js.map -/***/ }), -/* 275 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "reduce", function() { return reduce; }); -/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(276); -/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(271); -/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(238); -/* harmony import */ var _util_pipe__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(196); -/** PURE_IMPORTS_START _scan,_takeLast,_defaultIfEmpty,_util_pipe PURE_IMPORTS_END */ -function reduce(accumulator, seed) { - if (arguments.length >= 2) { - return function reduceOperatorFunctionWithSeed(source) { - return Object(_util_pipe__WEBPACK_IMPORTED_MODULE_3__["pipe"])(Object(_scan__WEBPACK_IMPORTED_MODULE_0__["scan"])(accumulator, seed), Object(_takeLast__WEBPACK_IMPORTED_MODULE_1__["takeLast"])(1), Object(_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_2__["defaultIfEmpty"])(seed))(source); - }; - } - return function reduceOperatorFunction(source) { - return Object(_util_pipe__WEBPACK_IMPORTED_MODULE_3__["pipe"])(Object(_scan__WEBPACK_IMPORTED_MODULE_0__["scan"])(function (acc, value, index) { return accumulator(acc, value, index + 1); }), Object(_takeLast__WEBPACK_IMPORTED_MODULE_1__["takeLast"])(1))(source); - }; -} -//# sourceMappingURL=reduce.js.map -/***/ }), -/* 276 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "scan", function() { return scan; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); -/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ -function scan(accumulator, seed) { - var hasSeed = false; - if (arguments.length >= 2) { - hasSeed = true; - } - return function scanOperatorFunction(source) { - return source.lift(new ScanOperator(accumulator, seed, hasSeed)); - }; -} -var ScanOperator = /*@__PURE__*/ (function () { - function ScanOperator(accumulator, seed, hasSeed) { - if (hasSeed === void 0) { - hasSeed = false; - } - this.accumulator = accumulator; - this.seed = seed; - this.hasSeed = hasSeed; - } - ScanOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new ScanSubscriber(subscriber, this.accumulator, this.seed, this.hasSeed)); - }; - return ScanOperator; -}()); -var ScanSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](ScanSubscriber, _super); - function ScanSubscriber(destination, accumulator, _seed, hasSeed) { - var _this = _super.call(this, destination) || this; - _this.accumulator = accumulator; - _this._seed = _seed; - _this.hasSeed = hasSeed; - _this.index = 0; - return _this; - } - Object.defineProperty(ScanSubscriber.prototype, "seed", { - get: function () { - return this._seed; - }, - set: function (value) { - this.hasSeed = true; - this._seed = value; - }, - enumerable: true, - configurable: true - }); - ScanSubscriber.prototype._next = function (value) { - if (!this.hasSeed) { - this.seed = value; - this.destination.next(value); - } - else { - return this._tryNext(value); - } - }; - ScanSubscriber.prototype._tryNext = function (value) { - var index = this.index++; - var result; - try { - result = this.accumulator(this.seed, value, index); - } - catch (err) { - this.destination.error(err); - } - this.seed = result; - this.destination.next(result); - }; - return ScanSubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); -//# sourceMappingURL=scan.js.map -/***/ }), -/* 277 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "merge", function() { return merge; }); -/* harmony import */ var _observable_merge__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(278); -/** PURE_IMPORTS_START _observable_merge PURE_IMPORTS_END */ -function merge() { - var observables = []; - for (var _i = 0; _i < arguments.length; _i++) { - observables[_i] = arguments[_i]; - } - return function (source) { return source.lift.call(_observable_merge__WEBPACK_IMPORTED_MODULE_0__["merge"].apply(void 0, [source].concat(observables))); }; -} -//# sourceMappingURL=merge.js.map -/***/ }), -/* 278 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "merge", function() { return merge; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(193); -/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(206); -/* harmony import */ var _operators_mergeAll__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(229); -/* harmony import */ var _fromArray__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(215); -/** PURE_IMPORTS_START _Observable,_util_isScheduler,_operators_mergeAll,_fromArray PURE_IMPORTS_END */ -function merge() { - var observables = []; - for (var _i = 0; _i < arguments.length; _i++) { - observables[_i] = arguments[_i]; - } - var concurrent = Number.POSITIVE_INFINITY; - var scheduler = null; - var last = observables[observables.length - 1]; - if (Object(_util_isScheduler__WEBPACK_IMPORTED_MODULE_1__["isScheduler"])(last)) { - scheduler = observables.pop(); - if (observables.length > 1 && typeof observables[observables.length - 1] === 'number') { - concurrent = observables.pop(); - } - } - else if (typeof last === 'number') { - concurrent = observables.pop(); - } - if (scheduler === null && observables.length === 1 && observables[0] instanceof _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"]) { - return observables[0]; - } - return Object(_operators_mergeAll__WEBPACK_IMPORTED_MODULE_2__["mergeAll"])(concurrent)(Object(_fromArray__WEBPACK_IMPORTED_MODULE_3__["fromArray"])(observables, scheduler)); -} -//# sourceMappingURL=merge.js.map -/***/ }), -/* 279 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "mergeMapTo", function() { return mergeMapTo; }); -/* harmony import */ var _mergeMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(230); -/** PURE_IMPORTS_START _mergeMap PURE_IMPORTS_END */ -function mergeMapTo(innerObservable, resultSelector, concurrent) { - if (concurrent === void 0) { - concurrent = Number.POSITIVE_INFINITY; - } - if (typeof resultSelector === 'function') { - return Object(_mergeMap__WEBPACK_IMPORTED_MODULE_0__["mergeMap"])(function () { return innerObservable; }, resultSelector, concurrent); - } - if (typeof resultSelector === 'number') { - concurrent = resultSelector; - } - return Object(_mergeMap__WEBPACK_IMPORTED_MODULE_0__["mergeMap"])(function () { return innerObservable; }, concurrent); -} -//# sourceMappingURL=mergeMapTo.js.map + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +//# sourceMappingURL=index.js.map /***/ }), -/* 280 */ +/* 271 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "mergeScan", function() { return mergeScan; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MergeScanOperator", function() { return MergeScanOperator; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MergeScanSubscriber", function() { return MergeScanSubscriber; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "audit", function() { return audit; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(182); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(171); -/* harmony import */ var _InnerSubscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(183); -/** PURE_IMPORTS_START tslib,_util_subscribeToResult,_OuterSubscriber,_InnerSubscriber PURE_IMPORTS_END */ - +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(229); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(230); +/** PURE_IMPORTS_START tslib,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ -function mergeScan(accumulator, seed, concurrent) { - if (concurrent === void 0) { - concurrent = Number.POSITIVE_INFINITY; - } - return function (source) { return source.lift(new MergeScanOperator(accumulator, seed, concurrent)); }; +function audit(durationSelector) { + return function auditOperatorFunction(source) { + return source.lift(new AuditOperator(durationSelector)); + }; } -var MergeScanOperator = /*@__PURE__*/ (function () { - function MergeScanOperator(accumulator, seed, concurrent) { - this.accumulator = accumulator; - this.seed = seed; - this.concurrent = concurrent; +var AuditOperator = /*@__PURE__*/ (function () { + function AuditOperator(durationSelector) { + this.durationSelector = durationSelector; } - MergeScanOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new MergeScanSubscriber(subscriber, this.accumulator, this.seed, this.concurrent)); + AuditOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new AuditSubscriber(subscriber, this.durationSelector)); }; - return MergeScanOperator; + return AuditOperator; }()); - -var MergeScanSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](MergeScanSubscriber, _super); - function MergeScanSubscriber(destination, accumulator, acc, concurrent) { +var AuditSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](AuditSubscriber, _super); + function AuditSubscriber(destination, durationSelector) { var _this = _super.call(this, destination) || this; - _this.accumulator = accumulator; - _this.acc = acc; - _this.concurrent = concurrent; + _this.durationSelector = durationSelector; _this.hasValue = false; - _this.hasCompleted = false; - _this.buffer = []; - _this.active = 0; - _this.index = 0; return _this; } - MergeScanSubscriber.prototype._next = function (value) { - if (this.active < this.concurrent) { - var index = this.index++; - var destination = this.destination; - var ish = void 0; + AuditSubscriber.prototype._next = function (value) { + this.value = value; + this.hasValue = true; + if (!this.throttled) { + var duration = void 0; try { - var accumulator = this.accumulator; - ish = accumulator(this.acc, value, index); + var durationSelector = this.durationSelector; + duration = durationSelector(value); } - catch (e) { - return destination.error(e); + catch (err) { + return this.destination.error(err); } - this.active++; - this._innerSub(ish, value, index); - } - else { - this.buffer.push(value); - } - }; - MergeScanSubscriber.prototype._innerSub = function (ish, value, index) { - var innerSubscriber = new _InnerSubscriber__WEBPACK_IMPORTED_MODULE_3__["InnerSubscriber"](this, undefined, undefined); - var destination = this.destination; - destination.add(innerSubscriber); - Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_1__["subscribeToResult"])(this, ish, value, index, innerSubscriber); - }; - MergeScanSubscriber.prototype._complete = function () { - this.hasCompleted = true; - if (this.active === 0 && this.buffer.length === 0) { - if (this.hasValue === false) { - this.destination.next(this.acc); + var innerSubscription = Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__["subscribeToResult"])(this, duration); + if (!innerSubscription || innerSubscription.closed) { + this.clearThrottle(); + } + else { + this.add(this.throttled = innerSubscription); } - this.destination.complete(); } - this.unsubscribe(); - }; - MergeScanSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { - var destination = this.destination; - this.acc = innerValue; - this.hasValue = true; - destination.next(innerValue); }; - MergeScanSubscriber.prototype.notifyComplete = function (innerSub) { - var buffer = this.buffer; - var destination = this.destination; - destination.remove(innerSub); - this.active--; - if (buffer.length > 0) { - this._next(buffer.shift()); + AuditSubscriber.prototype.clearThrottle = function () { + var _a = this, value = _a.value, hasValue = _a.hasValue, throttled = _a.throttled; + if (throttled) { + this.remove(throttled); + this.throttled = null; + throttled.unsubscribe(); } - else if (this.active === 0 && this.hasCompleted) { - if (this.hasValue === false) { - this.destination.next(this.acc); - } - this.destination.complete(); + if (hasValue) { + this.value = null; + this.hasValue = false; + this.destination.next(value); } }; - return MergeScanSubscriber; -}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_2__["OuterSubscriber"])); - -//# sourceMappingURL=mergeScan.js.map + AuditSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex) { + this.clearThrottle(); + }; + AuditSubscriber.prototype.notifyComplete = function () { + this.clearThrottle(); + }; + return AuditSubscriber; +}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__["OuterSubscriber"])); +//# sourceMappingURL=audit.js.map /***/ }), -/* 281 */ +/* 272 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "min", function() { return min; }); -/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(275); -/** PURE_IMPORTS_START _reduce PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "auditTime", function() { return auditTime; }); +/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(215); +/* harmony import */ var _audit__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(271); +/* harmony import */ var _observable_timer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(267); +/** PURE_IMPORTS_START _scheduler_async,_audit,_observable_timer PURE_IMPORTS_END */ -function min(comparer) { - var min = (typeof comparer === 'function') - ? function (x, y) { return comparer(x, y) < 0 ? x : y; } - : function (x, y) { return x < y ? x : y; }; - return Object(_reduce__WEBPACK_IMPORTED_MODULE_0__["reduce"])(min); -} -//# sourceMappingURL=min.js.map -/***/ }), -/* 282 */ +function auditTime(duration, scheduler) { + if (scheduler === void 0) { + scheduler = _scheduler_async__WEBPACK_IMPORTED_MODULE_0__["async"]; + } + return Object(_audit__WEBPACK_IMPORTED_MODULE_1__["audit"])(function () { return Object(_observable_timer__WEBPACK_IMPORTED_MODULE_2__["timer"])(duration, scheduler); }); +} +//# sourceMappingURL=auditTime.js.map + + +/***/ }), +/* 273 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "multicast", function() { return multicast; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MulticastOperator", function() { return MulticastOperator; }); -/* harmony import */ var _observable_ConnectableObservable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(283); -/** PURE_IMPORTS_START _observable_ConnectableObservable PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buffer", function() { return buffer; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(229); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(230); +/** PURE_IMPORTS_START tslib,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ -function multicast(subjectOrSubjectFactory, selector) { - return function multicastOperatorFunction(source) { - var subjectFactory; - if (typeof subjectOrSubjectFactory === 'function') { - subjectFactory = subjectOrSubjectFactory; - } - else { - subjectFactory = function subjectFactory() { - return subjectOrSubjectFactory; - }; - } - if (typeof selector === 'function') { - return source.lift(new MulticastOperator(subjectFactory, selector)); - } - var connectable = Object.create(source, _observable_ConnectableObservable__WEBPACK_IMPORTED_MODULE_0__["connectableObservableDescriptor"]); - connectable.source = source; - connectable.subjectFactory = subjectFactory; - return connectable; + + +function buffer(closingNotifier) { + return function bufferOperatorFunction(source) { + return source.lift(new BufferOperator(closingNotifier)); }; } -var MulticastOperator = /*@__PURE__*/ (function () { - function MulticastOperator(subjectFactory, selector) { - this.subjectFactory = subjectFactory; - this.selector = selector; +var BufferOperator = /*@__PURE__*/ (function () { + function BufferOperator(closingNotifier) { + this.closingNotifier = closingNotifier; } - MulticastOperator.prototype.call = function (subscriber, source) { - var selector = this.selector; - var subject = this.subjectFactory(); - var subscription = selector(subject).subscribe(subscriber); - subscription.add(source.subscribe(subject)); - return subscription; + BufferOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new BufferSubscriber(subscriber, this.closingNotifier)); }; - return MulticastOperator; + return BufferOperator; }()); - -//# sourceMappingURL=multicast.js.map +var BufferSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](BufferSubscriber, _super); + function BufferSubscriber(destination, closingNotifier) { + var _this = _super.call(this, destination) || this; + _this.buffer = []; + _this.add(Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__["subscribeToResult"])(_this, closingNotifier)); + return _this; + } + BufferSubscriber.prototype._next = function (value) { + this.buffer.push(value); + }; + BufferSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { + var buffer = this.buffer; + this.buffer = []; + this.destination.next(buffer); + }; + return BufferSubscriber; +}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__["OuterSubscriber"])); +//# sourceMappingURL=buffer.js.map /***/ }), -/* 283 */ +/* 274 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ConnectableObservable", function() { return ConnectableObservable; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "connectableObservableDescriptor", function() { return connectableObservableDescriptor; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "bufferCount", function() { return bufferCount; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(265); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(193); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(172); -/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(177); -/* harmony import */ var _operators_refCount__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(284); -/** PURE_IMPORTS_START tslib,_Subject,_Observable,_Subscriber,_Subscription,_operators_refCount PURE_IMPORTS_END */ - - - - +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); +/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ -var ConnectableObservable = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](ConnectableObservable, _super); - function ConnectableObservable(source, subjectFactory) { - var _this = _super.call(this) || this; - _this.source = source; - _this.subjectFactory = subjectFactory; - _this._refCount = 0; - _this._isComplete = false; - return _this; +function bufferCount(bufferSize, startBufferEvery) { + if (startBufferEvery === void 0) { + startBufferEvery = null; } - ConnectableObservable.prototype._subscribe = function (subscriber) { - return this.getSubject().subscribe(subscriber); + return function bufferCountOperatorFunction(source) { + return source.lift(new BufferCountOperator(bufferSize, startBufferEvery)); }; - ConnectableObservable.prototype.getSubject = function () { - var subject = this._subject; - if (!subject || subject.isStopped) { - this._subject = this.subjectFactory(); +} +var BufferCountOperator = /*@__PURE__*/ (function () { + function BufferCountOperator(bufferSize, startBufferEvery) { + this.bufferSize = bufferSize; + this.startBufferEvery = startBufferEvery; + if (!startBufferEvery || bufferSize === startBufferEvery) { + this.subscriberClass = BufferCountSubscriber; } - return this._subject; - }; - ConnectableObservable.prototype.connect = function () { - var connection = this._connection; - if (!connection) { - this._isComplete = false; - connection = this._connection = new _Subscription__WEBPACK_IMPORTED_MODULE_4__["Subscription"](); - connection.add(this.source - .subscribe(new ConnectableSubscriber(this.getSubject(), this))); - if (connection.closed) { - this._connection = null; - connection = _Subscription__WEBPACK_IMPORTED_MODULE_4__["Subscription"].EMPTY; - } + else { + this.subscriberClass = BufferSkipCountSubscriber; } - return connection; - }; - ConnectableObservable.prototype.refCount = function () { - return Object(_operators_refCount__WEBPACK_IMPORTED_MODULE_5__["refCount"])()(this); - }; - return ConnectableObservable; -}(_Observable__WEBPACK_IMPORTED_MODULE_2__["Observable"])); - -var connectableObservableDescriptor = /*@__PURE__*/ (function () { - var connectableProto = ConnectableObservable.prototype; - return { - operator: { value: null }, - _refCount: { value: 0, writable: true }, - _subject: { value: null, writable: true }, - _connection: { value: null, writable: true }, - _subscribe: { value: connectableProto._subscribe }, - _isComplete: { value: connectableProto._isComplete, writable: true }, - getSubject: { value: connectableProto.getSubject }, - connect: { value: connectableProto.connect }, - refCount: { value: connectableProto.refCount } + } + BufferCountOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new this.subscriberClass(subscriber, this.bufferSize, this.startBufferEvery)); }; -})(); -var ConnectableSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](ConnectableSubscriber, _super); - function ConnectableSubscriber(destination, connectable) { + return BufferCountOperator; +}()); +var BufferCountSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](BufferCountSubscriber, _super); + function BufferCountSubscriber(destination, bufferSize) { var _this = _super.call(this, destination) || this; - _this.connectable = connectable; + _this.bufferSize = bufferSize; + _this.buffer = []; return _this; } - ConnectableSubscriber.prototype._error = function (err) { - this._unsubscribe(); - _super.prototype._error.call(this, err); - }; - ConnectableSubscriber.prototype._complete = function () { - this.connectable._isComplete = true; - this._unsubscribe(); - _super.prototype._complete.call(this); - }; - ConnectableSubscriber.prototype._unsubscribe = function () { - var connectable = this.connectable; - if (connectable) { - this.connectable = null; - var connection = connectable._connection; - connectable._refCount = 0; - connectable._subject = null; - connectable._connection = null; - if (connection) { - connection.unsubscribe(); - } + BufferCountSubscriber.prototype._next = function (value) { + var buffer = this.buffer; + buffer.push(value); + if (buffer.length == this.bufferSize) { + this.destination.next(buffer); + this.buffer = []; } }; - return ConnectableSubscriber; -}(_Subject__WEBPACK_IMPORTED_MODULE_1__["SubjectSubscriber"])); -var RefCountOperator = /*@__PURE__*/ (function () { - function RefCountOperator(connectable) { - this.connectable = connectable; - } - RefCountOperator.prototype.call = function (subscriber, source) { - var connectable = this.connectable; - connectable._refCount++; - var refCounter = new RefCountSubscriber(subscriber, connectable); - var subscription = source.subscribe(refCounter); - if (!refCounter.closed) { - refCounter.connection = connectable.connect(); + BufferCountSubscriber.prototype._complete = function () { + var buffer = this.buffer; + if (buffer.length > 0) { + this.destination.next(buffer); } - return subscription; + _super.prototype._complete.call(this); }; - return RefCountOperator; -}()); -var RefCountSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](RefCountSubscriber, _super); - function RefCountSubscriber(destination, connectable) { + return BufferCountSubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); +var BufferSkipCountSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](BufferSkipCountSubscriber, _super); + function BufferSkipCountSubscriber(destination, bufferSize, startBufferEvery) { var _this = _super.call(this, destination) || this; - _this.connectable = connectable; + _this.bufferSize = bufferSize; + _this.startBufferEvery = startBufferEvery; + _this.buffers = []; + _this.count = 0; return _this; } - RefCountSubscriber.prototype._unsubscribe = function () { - var connectable = this.connectable; - if (!connectable) { - this.connection = null; - return; - } - this.connectable = null; - var refCount = connectable._refCount; - if (refCount <= 0) { - this.connection = null; - return; + BufferSkipCountSubscriber.prototype._next = function (value) { + var _a = this, bufferSize = _a.bufferSize, startBufferEvery = _a.startBufferEvery, buffers = _a.buffers, count = _a.count; + this.count++; + if (count % startBufferEvery === 0) { + buffers.push([]); } - connectable._refCount = refCount - 1; - if (refCount > 1) { - this.connection = null; - return; + for (var i = buffers.length; i--;) { + var buffer = buffers[i]; + buffer.push(value); + if (buffer.length === bufferSize) { + buffers.splice(i, 1); + this.destination.next(buffer); + } } - var connection = this.connection; - var sharedConnection = connectable._connection; - this.connection = null; - if (sharedConnection && (!connection || sharedConnection === connection)) { - sharedConnection.unsubscribe(); + }; + BufferSkipCountSubscriber.prototype._complete = function () { + var _a = this, buffers = _a.buffers, destination = _a.destination; + while (buffers.length > 0) { + var buffer = buffers.shift(); + if (buffer.length > 0) { + destination.next(buffer); + } } + _super.prototype._complete.call(this); }; - return RefCountSubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_3__["Subscriber"])); -//# sourceMappingURL=ConnectableObservable.js.map + return BufferSkipCountSubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); +//# sourceMappingURL=bufferCount.js.map /***/ }), -/* 284 */ +/* 275 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "refCount", function() { return refCount; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "bufferTime", function() { return bufferTime; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); -/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ +/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(215); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(172); +/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(205); +/** PURE_IMPORTS_START tslib,_scheduler_async,_Subscriber,_util_isScheduler PURE_IMPORTS_END */ -function refCount() { - return function refCountOperatorFunction(source) { - return source.lift(new RefCountOperator(source)); + + +function bufferTime(bufferTimeSpan) { + var length = arguments.length; + var scheduler = _scheduler_async__WEBPACK_IMPORTED_MODULE_1__["async"]; + if (Object(_util_isScheduler__WEBPACK_IMPORTED_MODULE_3__["isScheduler"])(arguments[arguments.length - 1])) { + scheduler = arguments[arguments.length - 1]; + length--; + } + var bufferCreationInterval = null; + if (length >= 2) { + bufferCreationInterval = arguments[1]; + } + var maxBufferSize = Number.POSITIVE_INFINITY; + if (length >= 3) { + maxBufferSize = arguments[2]; + } + return function bufferTimeOperatorFunction(source) { + return source.lift(new BufferTimeOperator(bufferTimeSpan, bufferCreationInterval, maxBufferSize, scheduler)); }; } -var RefCountOperator = /*@__PURE__*/ (function () { - function RefCountOperator(connectable) { - this.connectable = connectable; +var BufferTimeOperator = /*@__PURE__*/ (function () { + function BufferTimeOperator(bufferTimeSpan, bufferCreationInterval, maxBufferSize, scheduler) { + this.bufferTimeSpan = bufferTimeSpan; + this.bufferCreationInterval = bufferCreationInterval; + this.maxBufferSize = maxBufferSize; + this.scheduler = scheduler; } - RefCountOperator.prototype.call = function (subscriber, source) { - var connectable = this.connectable; - connectable._refCount++; - var refCounter = new RefCountSubscriber(subscriber, connectable); - var subscription = source.subscribe(refCounter); - if (!refCounter.closed) { - refCounter.connection = connectable.connect(); - } - return subscription; + BufferTimeOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new BufferTimeSubscriber(subscriber, this.bufferTimeSpan, this.bufferCreationInterval, this.maxBufferSize, this.scheduler)); }; - return RefCountOperator; + return BufferTimeOperator; }()); -var RefCountSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](RefCountSubscriber, _super); - function RefCountSubscriber(destination, connectable) { +var Context = /*@__PURE__*/ (function () { + function Context() { + this.buffer = []; + } + return Context; +}()); +var BufferTimeSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](BufferTimeSubscriber, _super); + function BufferTimeSubscriber(destination, bufferTimeSpan, bufferCreationInterval, maxBufferSize, scheduler) { var _this = _super.call(this, destination) || this; - _this.connectable = connectable; + _this.bufferTimeSpan = bufferTimeSpan; + _this.bufferCreationInterval = bufferCreationInterval; + _this.maxBufferSize = maxBufferSize; + _this.scheduler = scheduler; + _this.contexts = []; + var context = _this.openContext(); + _this.timespanOnly = bufferCreationInterval == null || bufferCreationInterval < 0; + if (_this.timespanOnly) { + var timeSpanOnlyState = { subscriber: _this, context: context, bufferTimeSpan: bufferTimeSpan }; + _this.add(context.closeAction = scheduler.schedule(dispatchBufferTimeSpanOnly, bufferTimeSpan, timeSpanOnlyState)); + } + else { + var closeState = { subscriber: _this, context: context }; + var creationState = { bufferTimeSpan: bufferTimeSpan, bufferCreationInterval: bufferCreationInterval, subscriber: _this, scheduler: scheduler }; + _this.add(context.closeAction = scheduler.schedule(dispatchBufferClose, bufferTimeSpan, closeState)); + _this.add(scheduler.schedule(dispatchBufferCreation, bufferCreationInterval, creationState)); + } return _this; } - RefCountSubscriber.prototype._unsubscribe = function () { - var connectable = this.connectable; - if (!connectable) { - this.connection = null; - return; + BufferTimeSubscriber.prototype._next = function (value) { + var contexts = this.contexts; + var len = contexts.length; + var filledBufferContext; + for (var i = 0; i < len; i++) { + var context_1 = contexts[i]; + var buffer = context_1.buffer; + buffer.push(value); + if (buffer.length == this.maxBufferSize) { + filledBufferContext = context_1; + } } - this.connectable = null; - var refCount = connectable._refCount; - if (refCount <= 0) { - this.connection = null; - return; + if (filledBufferContext) { + this.onBufferFull(filledBufferContext); } - connectable._refCount = refCount - 1; - if (refCount > 1) { - this.connection = null; - return; + }; + BufferTimeSubscriber.prototype._error = function (err) { + this.contexts.length = 0; + _super.prototype._error.call(this, err); + }; + BufferTimeSubscriber.prototype._complete = function () { + var _a = this, contexts = _a.contexts, destination = _a.destination; + while (contexts.length > 0) { + var context_2 = contexts.shift(); + destination.next(context_2.buffer); } - var connection = this.connection; - var sharedConnection = connectable._connection; - this.connection = null; - if (sharedConnection && (!connection || sharedConnection === connection)) { - sharedConnection.unsubscribe(); + _super.prototype._complete.call(this); + }; + BufferTimeSubscriber.prototype._unsubscribe = function () { + this.contexts = null; + }; + BufferTimeSubscriber.prototype.onBufferFull = function (context) { + this.closeContext(context); + var closeAction = context.closeAction; + closeAction.unsubscribe(); + this.remove(closeAction); + if (!this.closed && this.timespanOnly) { + context = this.openContext(); + var bufferTimeSpan = this.bufferTimeSpan; + var timeSpanOnlyState = { subscriber: this, context: context, bufferTimeSpan: bufferTimeSpan }; + this.add(context.closeAction = this.scheduler.schedule(dispatchBufferTimeSpanOnly, bufferTimeSpan, timeSpanOnlyState)); } }; - return RefCountSubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); -//# sourceMappingURL=refCount.js.map + BufferTimeSubscriber.prototype.openContext = function () { + var context = new Context(); + this.contexts.push(context); + return context; + }; + BufferTimeSubscriber.prototype.closeContext = function (context) { + this.destination.next(context.buffer); + var contexts = this.contexts; + var spliceIndex = contexts ? contexts.indexOf(context) : -1; + if (spliceIndex >= 0) { + contexts.splice(contexts.indexOf(context), 1); + } + }; + return BufferTimeSubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_2__["Subscriber"])); +function dispatchBufferTimeSpanOnly(state) { + var subscriber = state.subscriber; + var prevContext = state.context; + if (prevContext) { + subscriber.closeContext(prevContext); + } + if (!subscriber.closed) { + state.context = subscriber.openContext(); + state.context.closeAction = this.schedule(state, state.bufferTimeSpan); + } +} +function dispatchBufferCreation(state) { + var bufferCreationInterval = state.bufferCreationInterval, bufferTimeSpan = state.bufferTimeSpan, subscriber = state.subscriber, scheduler = state.scheduler; + var context = subscriber.openContext(); + var action = this; + if (!subscriber.closed) { + subscriber.add(context.closeAction = scheduler.schedule(dispatchBufferClose, bufferTimeSpan, { subscriber: subscriber, context: context })); + action.schedule(state, bufferCreationInterval); + } +} +function dispatchBufferClose(arg) { + var subscriber = arg.subscriber, context = arg.context; + subscriber.closeContext(context); +} +//# sourceMappingURL=bufferTime.js.map /***/ }), -/* 285 */ +/* 276 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "observeOn", function() { return observeOn; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ObserveOnOperator", function() { return ObserveOnOperator; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ObserveOnSubscriber", function() { return ObserveOnSubscriber; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ObserveOnMessage", function() { return ObserveOnMessage; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "bufferToggle", function() { return bufferToggle; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); -/* harmony import */ var _Notification__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(241); -/** PURE_IMPORTS_START tslib,_Subscriber,_Notification PURE_IMPORTS_END */ +/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(177); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(230); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(229); +/** PURE_IMPORTS_START tslib,_Subscription,_util_subscribeToResult,_OuterSubscriber PURE_IMPORTS_END */ -function observeOn(scheduler, delay) { - if (delay === void 0) { - delay = 0; - } - return function observeOnOperatorFunction(source) { - return source.lift(new ObserveOnOperator(scheduler, delay)); + +function bufferToggle(openings, closingSelector) { + return function bufferToggleOperatorFunction(source) { + return source.lift(new BufferToggleOperator(openings, closingSelector)); }; } -var ObserveOnOperator = /*@__PURE__*/ (function () { - function ObserveOnOperator(scheduler, delay) { - if (delay === void 0) { - delay = 0; - } - this.scheduler = scheduler; - this.delay = delay; +var BufferToggleOperator = /*@__PURE__*/ (function () { + function BufferToggleOperator(openings, closingSelector) { + this.openings = openings; + this.closingSelector = closingSelector; } - ObserveOnOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new ObserveOnSubscriber(subscriber, this.scheduler, this.delay)); + BufferToggleOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new BufferToggleSubscriber(subscriber, this.openings, this.closingSelector)); }; - return ObserveOnOperator; + return BufferToggleOperator; }()); - -var ObserveOnSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](ObserveOnSubscriber, _super); - function ObserveOnSubscriber(destination, scheduler, delay) { - if (delay === void 0) { - delay = 0; - } +var BufferToggleSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](BufferToggleSubscriber, _super); + function BufferToggleSubscriber(destination, openings, closingSelector) { var _this = _super.call(this, destination) || this; - _this.scheduler = scheduler; - _this.delay = delay; - return _this; - } - ObserveOnSubscriber.dispatch = function (arg) { - var notification = arg.notification, destination = arg.destination; - notification.observe(destination); - this.unsubscribe(); + _this.openings = openings; + _this.closingSelector = closingSelector; + _this.contexts = []; + _this.add(Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__["subscribeToResult"])(_this, openings)); + return _this; + } + BufferToggleSubscriber.prototype._next = function (value) { + var contexts = this.contexts; + var len = contexts.length; + for (var i = 0; i < len; i++) { + contexts[i].buffer.push(value); + } }; - ObserveOnSubscriber.prototype.scheduleMessage = function (notification) { - var destination = this.destination; - destination.add(this.scheduler.schedule(ObserveOnSubscriber.dispatch, this.delay, new ObserveOnMessage(notification, this.destination))); + BufferToggleSubscriber.prototype._error = function (err) { + var contexts = this.contexts; + while (contexts.length > 0) { + var context_1 = contexts.shift(); + context_1.subscription.unsubscribe(); + context_1.buffer = null; + context_1.subscription = null; + } + this.contexts = null; + _super.prototype._error.call(this, err); }; - ObserveOnSubscriber.prototype._next = function (value) { - this.scheduleMessage(_Notification__WEBPACK_IMPORTED_MODULE_2__["Notification"].createNext(value)); + BufferToggleSubscriber.prototype._complete = function () { + var contexts = this.contexts; + while (contexts.length > 0) { + var context_2 = contexts.shift(); + this.destination.next(context_2.buffer); + context_2.subscription.unsubscribe(); + context_2.buffer = null; + context_2.subscription = null; + } + this.contexts = null; + _super.prototype._complete.call(this); }; - ObserveOnSubscriber.prototype._error = function (err) { - this.scheduleMessage(_Notification__WEBPACK_IMPORTED_MODULE_2__["Notification"].createError(err)); - this.unsubscribe(); + BufferToggleSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { + outerValue ? this.closeBuffer(outerValue) : this.openBuffer(innerValue); }; - ObserveOnSubscriber.prototype._complete = function () { - this.scheduleMessage(_Notification__WEBPACK_IMPORTED_MODULE_2__["Notification"].createComplete()); - this.unsubscribe(); + BufferToggleSubscriber.prototype.notifyComplete = function (innerSub) { + this.closeBuffer(innerSub.context); }; - return ObserveOnSubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); - -var ObserveOnMessage = /*@__PURE__*/ (function () { - function ObserveOnMessage(notification, destination) { - this.notification = notification; - this.destination = destination; - } - return ObserveOnMessage; -}()); - -//# sourceMappingURL=observeOn.js.map + BufferToggleSubscriber.prototype.openBuffer = function (value) { + try { + var closingSelector = this.closingSelector; + var closingNotifier = closingSelector.call(this, value); + if (closingNotifier) { + this.trySubscribe(closingNotifier); + } + } + catch (err) { + this._error(err); + } + }; + BufferToggleSubscriber.prototype.closeBuffer = function (context) { + var contexts = this.contexts; + if (contexts && context) { + var buffer = context.buffer, subscription = context.subscription; + this.destination.next(buffer); + contexts.splice(contexts.indexOf(context), 1); + this.remove(subscription); + subscription.unsubscribe(); + } + }; + BufferToggleSubscriber.prototype.trySubscribe = function (closingNotifier) { + var contexts = this.contexts; + var buffer = []; + var subscription = new _Subscription__WEBPACK_IMPORTED_MODULE_1__["Subscription"](); + var context = { buffer: buffer, subscription: subscription }; + contexts.push(context); + var innerSubscription = Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__["subscribeToResult"])(this, closingNotifier, context); + if (!innerSubscription || innerSubscription.closed) { + this.closeBuffer(context); + } + else { + innerSubscription.context = context; + this.add(innerSubscription); + subscription.add(innerSubscription); + } + }; + return BufferToggleSubscriber; +}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__["OuterSubscriber"])); +//# sourceMappingURL=bufferToggle.js.map /***/ }), -/* 286 */ +/* 277 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "onErrorResumeNext", function() { return onErrorResumeNext; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "onErrorResumeNextStatic", function() { return onErrorResumeNextStatic; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "bufferWhen", function() { return bufferWhen; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _observable_from__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(218); -/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(178); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(171); -/* harmony import */ var _InnerSubscriber__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(183); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(182); -/** PURE_IMPORTS_START tslib,_observable_from,_util_isArray,_OuterSubscriber,_InnerSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ - - +/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(177); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(229); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(230); +/** PURE_IMPORTS_START tslib,_Subscription,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ -function onErrorResumeNext() { - var nextSources = []; - for (var _i = 0; _i < arguments.length; _i++) { - nextSources[_i] = arguments[_i]; - } - if (nextSources.length === 1 && Object(_util_isArray__WEBPACK_IMPORTED_MODULE_2__["isArray"])(nextSources[0])) { - nextSources = nextSources[0]; - } - return function (source) { return source.lift(new OnErrorResumeNextOperator(nextSources)); }; -} -function onErrorResumeNextStatic() { - var nextSources = []; - for (var _i = 0; _i < arguments.length; _i++) { - nextSources[_i] = arguments[_i]; - } - var source = null; - if (nextSources.length === 1 && Object(_util_isArray__WEBPACK_IMPORTED_MODULE_2__["isArray"])(nextSources[0])) { - nextSources = nextSources[0]; - } - source = nextSources.shift(); - return Object(_observable_from__WEBPACK_IMPORTED_MODULE_1__["from"])(source, null).lift(new OnErrorResumeNextOperator(nextSources)); +function bufferWhen(closingSelector) { + return function (source) { + return source.lift(new BufferWhenOperator(closingSelector)); + }; } -var OnErrorResumeNextOperator = /*@__PURE__*/ (function () { - function OnErrorResumeNextOperator(nextSources) { - this.nextSources = nextSources; +var BufferWhenOperator = /*@__PURE__*/ (function () { + function BufferWhenOperator(closingSelector) { + this.closingSelector = closingSelector; } - OnErrorResumeNextOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new OnErrorResumeNextSubscriber(subscriber, this.nextSources)); + BufferWhenOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new BufferWhenSubscriber(subscriber, this.closingSelector)); }; - return OnErrorResumeNextOperator; + return BufferWhenOperator; }()); -var OnErrorResumeNextSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](OnErrorResumeNextSubscriber, _super); - function OnErrorResumeNextSubscriber(destination, nextSources) { +var BufferWhenSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](BufferWhenSubscriber, _super); + function BufferWhenSubscriber(destination, closingSelector) { var _this = _super.call(this, destination) || this; - _this.destination = destination; - _this.nextSources = nextSources; + _this.closingSelector = closingSelector; + _this.subscribing = false; + _this.openBuffer(); return _this; } - OnErrorResumeNextSubscriber.prototype.notifyError = function (error, innerSub) { - this.subscribeToNextSource(); + BufferWhenSubscriber.prototype._next = function (value) { + this.buffer.push(value); }; - OnErrorResumeNextSubscriber.prototype.notifyComplete = function (innerSub) { - this.subscribeToNextSource(); + BufferWhenSubscriber.prototype._complete = function () { + var buffer = this.buffer; + if (buffer) { + this.destination.next(buffer); + } + _super.prototype._complete.call(this); }; - OnErrorResumeNextSubscriber.prototype._error = function (err) { - this.subscribeToNextSource(); - this.unsubscribe(); + BufferWhenSubscriber.prototype._unsubscribe = function () { + this.buffer = null; + this.subscribing = false; }; - OnErrorResumeNextSubscriber.prototype._complete = function () { - this.subscribeToNextSource(); - this.unsubscribe(); + BufferWhenSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { + this.openBuffer(); }; - OnErrorResumeNextSubscriber.prototype.subscribeToNextSource = function () { - var next = this.nextSources.shift(); - if (!!next) { - var innerSubscriber = new _InnerSubscriber__WEBPACK_IMPORTED_MODULE_4__["InnerSubscriber"](this, undefined, undefined); - var destination = this.destination; - destination.add(innerSubscriber); - Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_5__["subscribeToResult"])(this, next, undefined, undefined, innerSubscriber); + BufferWhenSubscriber.prototype.notifyComplete = function () { + if (this.subscribing) { + this.complete(); } else { - this.destination.complete(); + this.openBuffer(); } }; - return OnErrorResumeNextSubscriber; -}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__["OuterSubscriber"])); -//# sourceMappingURL=onErrorResumeNext.js.map + BufferWhenSubscriber.prototype.openBuffer = function () { + var closingSubscription = this.closingSubscription; + if (closingSubscription) { + this.remove(closingSubscription); + closingSubscription.unsubscribe(); + } + var buffer = this.buffer; + if (this.buffer) { + this.destination.next(buffer); + } + this.buffer = []; + var closingNotifier; + try { + var closingSelector = this.closingSelector; + closingNotifier = closingSelector(); + } + catch (err) { + return this.error(err); + } + closingSubscription = new _Subscription__WEBPACK_IMPORTED_MODULE_1__["Subscription"](); + this.closingSubscription = closingSubscription; + this.add(closingSubscription); + this.subscribing = true; + closingSubscription.add(Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__["subscribeToResult"])(this, closingNotifier)); + this.subscribing = false; + }; + return BufferWhenSubscriber; +}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_2__["OuterSubscriber"])); +//# sourceMappingURL=bufferWhen.js.map /***/ }), -/* 287 */ +/* 278 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "pairwise", function() { return pairwise; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "catchError", function() { return catchError; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); -/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(229); +/* harmony import */ var _InnerSubscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(231); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(230); +/** PURE_IMPORTS_START tslib,_OuterSubscriber,_InnerSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ -function pairwise() { - return function (source) { return source.lift(new PairwiseOperator()); }; + + +function catchError(selector) { + return function catchErrorOperatorFunction(source) { + var operator = new CatchOperator(selector); + var caught = source.lift(operator); + return (operator.caught = caught); + }; } -var PairwiseOperator = /*@__PURE__*/ (function () { - function PairwiseOperator() { +var CatchOperator = /*@__PURE__*/ (function () { + function CatchOperator(selector) { + this.selector = selector; } - PairwiseOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new PairwiseSubscriber(subscriber)); + CatchOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new CatchSubscriber(subscriber, this.selector, this.caught)); }; - return PairwiseOperator; + return CatchOperator; }()); -var PairwiseSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](PairwiseSubscriber, _super); - function PairwiseSubscriber(destination) { +var CatchSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](CatchSubscriber, _super); + function CatchSubscriber(destination, selector, caught) { var _this = _super.call(this, destination) || this; - _this.hasPrev = false; + _this.selector = selector; + _this.caught = caught; return _this; } - PairwiseSubscriber.prototype._next = function (value) { - var pair; - if (this.hasPrev) { - pair = [this.prev, value]; - } - else { - this.hasPrev = true; - } - this.prev = value; - if (pair) { - this.destination.next(pair); + CatchSubscriber.prototype.error = function (err) { + if (!this.isStopped) { + var result = void 0; + try { + result = this.selector(err, this.caught); + } + catch (err2) { + _super.prototype.error.call(this, err2); + return; + } + this._unsubscribeAndRecycle(); + var innerSubscriber = new _InnerSubscriber__WEBPACK_IMPORTED_MODULE_2__["InnerSubscriber"](this, undefined, undefined); + this.add(innerSubscriber); + Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__["subscribeToResult"])(this, result, undefined, undefined, innerSubscriber); } }; - return PairwiseSubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); -//# sourceMappingURL=pairwise.js.map + return CatchSubscriber; +}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__["OuterSubscriber"])); +//# sourceMappingURL=catchError.js.map /***/ }), -/* 288 */ +/* 279 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "partition", function() { return partition; }); -/* harmony import */ var _util_not__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(289); -/* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(251); -/** PURE_IMPORTS_START _util_not,_filter PURE_IMPORTS_END */ - +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "combineAll", function() { return combineAll; }); +/* harmony import */ var _observable_combineLatest__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(228); +/** PURE_IMPORTS_START _observable_combineLatest PURE_IMPORTS_END */ -function partition(predicate, thisArg) { - return function (source) { - return [ - Object(_filter__WEBPACK_IMPORTED_MODULE_1__["filter"])(predicate, thisArg)(source), - Object(_filter__WEBPACK_IMPORTED_MODULE_1__["filter"])(Object(_util_not__WEBPACK_IMPORTED_MODULE_0__["not"])(predicate, thisArg))(source) - ]; - }; +function combineAll(project) { + return function (source) { return source.lift(new _observable_combineLatest__WEBPACK_IMPORTED_MODULE_0__["CombineLatestOperator"](project)); }; } -//# sourceMappingURL=partition.js.map +//# sourceMappingURL=combineAll.js.map /***/ }), -/* 289 */ +/* 280 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "not", function() { return not; }); -/** PURE_IMPORTS_START PURE_IMPORTS_END */ -function not(pred, thisArg) { - function notPred() { - return !(notPred.pred.apply(notPred.thisArg, arguments)); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "combineLatest", function() { return combineLatest; }); +/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(178); +/* harmony import */ var _observable_combineLatest__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(228); +/* harmony import */ var _observable_from__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(243); +/** PURE_IMPORTS_START _util_isArray,_observable_combineLatest,_observable_from PURE_IMPORTS_END */ + + + +var none = {}; +function combineLatest() { + var observables = []; + for (var _i = 0; _i < arguments.length; _i++) { + observables[_i] = arguments[_i]; } - notPred.pred = pred; - notPred.thisArg = thisArg; - return notPred; + var project = null; + if (typeof observables[observables.length - 1] === 'function') { + project = observables.pop(); + } + if (observables.length === 1 && Object(_util_isArray__WEBPACK_IMPORTED_MODULE_0__["isArray"])(observables[0])) { + observables = observables[0].slice(); + } + return function (source) { return source.lift.call(Object(_observable_from__WEBPACK_IMPORTED_MODULE_2__["from"])([source].concat(observables)), new _observable_combineLatest__WEBPACK_IMPORTED_MODULE_1__["CombineLatestOperator"](project)); }; } -//# sourceMappingURL=not.js.map +//# sourceMappingURL=combineLatest.js.map /***/ }), -/* 290 */ +/* 281 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "pluck", function() { return pluck; }); -/* harmony import */ var _map__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(231); -/** PURE_IMPORTS_START _map PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "concat", function() { return concat; }); +/* harmony import */ var _observable_concat__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(239); +/** PURE_IMPORTS_START _observable_concat PURE_IMPORTS_END */ -function pluck() { - var properties = []; +function concat() { + var observables = []; for (var _i = 0; _i < arguments.length; _i++) { - properties[_i] = arguments[_i]; - } - var length = properties.length; - if (length === 0) { - throw new Error('list of properties cannot be empty.'); + observables[_i] = arguments[_i]; } - return function (source) { return Object(_map__WEBPACK_IMPORTED_MODULE_0__["map"])(plucker(properties, length))(source); }; -} -function plucker(props, length) { - var mapper = function (x) { - var currentProp = x; - for (var i = 0; i < length; i++) { - var p = currentProp[props[i]]; - if (typeof p !== 'undefined') { - currentProp = p; - } - else { - return undefined; - } - } - return currentProp; - }; - return mapper; + return function (source) { return source.lift.call(_observable_concat__WEBPACK_IMPORTED_MODULE_0__["concat"].apply(void 0, [source].concat(observables))); }; } -//# sourceMappingURL=pluck.js.map +//# sourceMappingURL=concat.js.map /***/ }), -/* 291 */ +/* 282 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publish", function() { return publish; }); -/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(265); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(282); -/** PURE_IMPORTS_START _Subject,_multicast PURE_IMPORTS_END */ - +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "concatMap", function() { return concatMap; }); +/* harmony import */ var _mergeMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(242); +/** PURE_IMPORTS_START _mergeMap PURE_IMPORTS_END */ -function publish(selector) { - return selector ? - Object(_multicast__WEBPACK_IMPORTED_MODULE_1__["multicast"])(function () { return new _Subject__WEBPACK_IMPORTED_MODULE_0__["Subject"](); }, selector) : - Object(_multicast__WEBPACK_IMPORTED_MODULE_1__["multicast"])(new _Subject__WEBPACK_IMPORTED_MODULE_0__["Subject"]()); +function concatMap(project, resultSelector) { + return Object(_mergeMap__WEBPACK_IMPORTED_MODULE_0__["mergeMap"])(project, resultSelector, 1); } -//# sourceMappingURL=publish.js.map +//# sourceMappingURL=concatMap.js.map /***/ }), -/* 292 */ +/* 283 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publishBehavior", function() { return publishBehavior; }); -/* harmony import */ var _BehaviorSubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(293); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(282); -/** PURE_IMPORTS_START _BehaviorSubject,_multicast PURE_IMPORTS_END */ - +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "concatMapTo", function() { return concatMapTo; }); +/* harmony import */ var _concatMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(282); +/** PURE_IMPORTS_START _concatMap PURE_IMPORTS_END */ -function publishBehavior(value) { - return function (source) { return Object(_multicast__WEBPACK_IMPORTED_MODULE_1__["multicast"])(new _BehaviorSubject__WEBPACK_IMPORTED_MODULE_0__["BehaviorSubject"](value))(source); }; +function concatMapTo(innerObservable, resultSelector) { + return Object(_concatMap__WEBPACK_IMPORTED_MODULE_0__["concatMap"])(function () { return innerObservable; }, resultSelector); } -//# sourceMappingURL=publishBehavior.js.map +//# sourceMappingURL=concatMapTo.js.map /***/ }), -/* 293 */ +/* 284 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "BehaviorSubject", function() { return BehaviorSubject; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "count", function() { return count; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(265); -/* harmony import */ var _util_ObjectUnsubscribedError__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(266); -/** PURE_IMPORTS_START tslib,_Subject,_util_ObjectUnsubscribedError PURE_IMPORTS_END */ - +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); +/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ -var BehaviorSubject = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](BehaviorSubject, _super); - function BehaviorSubject(_value) { - var _this = _super.call(this) || this; - _this._value = _value; +function count(predicate) { + return function (source) { return source.lift(new CountOperator(predicate, source)); }; +} +var CountOperator = /*@__PURE__*/ (function () { + function CountOperator(predicate, source) { + this.predicate = predicate; + this.source = source; + } + CountOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new CountSubscriber(subscriber, this.predicate, this.source)); + }; + return CountOperator; +}()); +var CountSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](CountSubscriber, _super); + function CountSubscriber(destination, predicate, source) { + var _this = _super.call(this, destination) || this; + _this.predicate = predicate; + _this.source = source; + _this.count = 0; + _this.index = 0; return _this; } - Object.defineProperty(BehaviorSubject.prototype, "value", { - get: function () { - return this.getValue(); - }, - enumerable: true, - configurable: true - }); - BehaviorSubject.prototype._subscribe = function (subscriber) { - var subscription = _super.prototype._subscribe.call(this, subscriber); - if (subscription && !subscription.closed) { - subscriber.next(this._value); + CountSubscriber.prototype._next = function (value) { + if (this.predicate) { + this._tryPredicate(value); + } + else { + this.count++; } - return subscription; }; - BehaviorSubject.prototype.getValue = function () { - if (this.hasError) { - throw this.thrownError; + CountSubscriber.prototype._tryPredicate = function (value) { + var result; + try { + result = this.predicate(value, this.index++, this.source); } - else if (this.closed) { - throw new _util_ObjectUnsubscribedError__WEBPACK_IMPORTED_MODULE_2__["ObjectUnsubscribedError"](); + catch (err) { + this.destination.error(err); + return; } - else { - return this._value; + if (result) { + this.count++; } }; - BehaviorSubject.prototype.next = function (value) { - _super.prototype.next.call(this, this._value = value); + CountSubscriber.prototype._complete = function () { + this.destination.next(this.count); + this.destination.complete(); }; - return BehaviorSubject; -}(_Subject__WEBPACK_IMPORTED_MODULE_1__["Subject"])); - -//# sourceMappingURL=BehaviorSubject.js.map + return CountSubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); +//# sourceMappingURL=count.js.map /***/ }), -/* 294 */ +/* 285 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publishLast", function() { return publishLast; }); -/* harmony import */ var _AsyncSubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(295); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(282); -/** PURE_IMPORTS_START _AsyncSubject,_multicast PURE_IMPORTS_END */ - - -function publishLast() { - return function (source) { return Object(_multicast__WEBPACK_IMPORTED_MODULE_1__["multicast"])(new _AsyncSubject__WEBPACK_IMPORTED_MODULE_0__["AsyncSubject"]())(source); }; -} -//# sourceMappingURL=publishLast.js.map +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "debounce", function() { return debounce; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(229); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(230); +/** PURE_IMPORTS_START tslib,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ -/***/ }), -/* 295 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "AsyncSubject", function() { return AsyncSubject; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(265); -/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(177); -/** PURE_IMPORTS_START tslib,_Subject,_Subscription PURE_IMPORTS_END */ - - - -var AsyncSubject = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](AsyncSubject, _super); - function AsyncSubject() { - var _this = _super !== null && _super.apply(this, arguments) || this; - _this.value = null; - _this.hasNext = false; - _this.hasCompleted = false; +function debounce(durationSelector) { + return function (source) { return source.lift(new DebounceOperator(durationSelector)); }; +} +var DebounceOperator = /*@__PURE__*/ (function () { + function DebounceOperator(durationSelector) { + this.durationSelector = durationSelector; + } + DebounceOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new DebounceSubscriber(subscriber, this.durationSelector)); + }; + return DebounceOperator; +}()); +var DebounceSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](DebounceSubscriber, _super); + function DebounceSubscriber(destination, durationSelector) { + var _this = _super.call(this, destination) || this; + _this.durationSelector = durationSelector; + _this.hasValue = false; + _this.durationSubscription = null; return _this; } - AsyncSubject.prototype._subscribe = function (subscriber) { - if (this.hasError) { - subscriber.error(this.thrownError); - return _Subscription__WEBPACK_IMPORTED_MODULE_2__["Subscription"].EMPTY; + DebounceSubscriber.prototype._next = function (value) { + try { + var result = this.durationSelector.call(this, value); + if (result) { + this._tryNext(value, result); + } } - else if (this.hasCompleted && this.hasNext) { - subscriber.next(this.value); - subscriber.complete(); - return _Subscription__WEBPACK_IMPORTED_MODULE_2__["Subscription"].EMPTY; + catch (err) { + this.destination.error(err); } - return _super.prototype._subscribe.call(this, subscriber); }; - AsyncSubject.prototype.next = function (value) { - if (!this.hasCompleted) { - this.value = value; - this.hasNext = true; - } + DebounceSubscriber.prototype._complete = function () { + this.emitValue(); + this.destination.complete(); }; - AsyncSubject.prototype.error = function (error) { - if (!this.hasCompleted) { - _super.prototype.error.call(this, error); + DebounceSubscriber.prototype._tryNext = function (value, duration) { + var subscription = this.durationSubscription; + this.value = value; + this.hasValue = true; + if (subscription) { + subscription.unsubscribe(); + this.remove(subscription); + } + subscription = Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__["subscribeToResult"])(this, duration); + if (subscription && !subscription.closed) { + this.add(this.durationSubscription = subscription); } }; - AsyncSubject.prototype.complete = function () { - this.hasCompleted = true; - if (this.hasNext) { - _super.prototype.next.call(this, this.value); + DebounceSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { + this.emitValue(); + }; + DebounceSubscriber.prototype.notifyComplete = function () { + this.emitValue(); + }; + DebounceSubscriber.prototype.emitValue = function () { + if (this.hasValue) { + var value = this.value; + var subscription = this.durationSubscription; + if (subscription) { + this.durationSubscription = null; + subscription.unsubscribe(); + this.remove(subscription); + } + this.value = null; + this.hasValue = false; + _super.prototype._next.call(this, value); } - _super.prototype.complete.call(this); }; - return AsyncSubject; -}(_Subject__WEBPACK_IMPORTED_MODULE_1__["Subject"])); - -//# sourceMappingURL=AsyncSubject.js.map - - -/***/ }), -/* 296 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publishReplay", function() { return publishReplay; }); -/* harmony import */ var _ReplaySubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(297); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(282); -/** PURE_IMPORTS_START _ReplaySubject,_multicast PURE_IMPORTS_END */ - - -function publishReplay(bufferSize, windowTime, selectorOrScheduler, scheduler) { - if (selectorOrScheduler && typeof selectorOrScheduler !== 'function') { - scheduler = selectorOrScheduler; - } - var selector = typeof selectorOrScheduler === 'function' ? selectorOrScheduler : undefined; - var subject = new _ReplaySubject__WEBPACK_IMPORTED_MODULE_0__["ReplaySubject"](bufferSize, windowTime, scheduler); - return function (source) { return Object(_multicast__WEBPACK_IMPORTED_MODULE_1__["multicast"])(function () { return subject; }, selector)(source); }; -} -//# sourceMappingURL=publishReplay.js.map + return DebounceSubscriber; +}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__["OuterSubscriber"])); +//# sourceMappingURL=debounce.js.map /***/ }), -/* 297 */ +/* 286 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ReplaySubject", function() { return ReplaySubject; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "debounceTime", function() { return debounceTime; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(265); -/* harmony import */ var _scheduler_queue__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(298); -/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(177); -/* harmony import */ var _operators_observeOn__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(285); -/* harmony import */ var _util_ObjectUnsubscribedError__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(266); -/* harmony import */ var _SubjectSubscription__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(267); -/** PURE_IMPORTS_START tslib,_Subject,_scheduler_queue,_Subscription,_operators_observeOn,_util_ObjectUnsubscribedError,_SubjectSubscription PURE_IMPORTS_END */ - - - - +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); +/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(215); +/** PURE_IMPORTS_START tslib,_Subscriber,_scheduler_async PURE_IMPORTS_END */ -var ReplaySubject = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](ReplaySubject, _super); - function ReplaySubject(bufferSize, windowTime, scheduler) { - if (bufferSize === void 0) { - bufferSize = Number.POSITIVE_INFINITY; - } - if (windowTime === void 0) { - windowTime = Number.POSITIVE_INFINITY; - } - var _this = _super.call(this) || this; +function debounceTime(dueTime, scheduler) { + if (scheduler === void 0) { + scheduler = _scheduler_async__WEBPACK_IMPORTED_MODULE_2__["async"]; + } + return function (source) { return source.lift(new DebounceTimeOperator(dueTime, scheduler)); }; +} +var DebounceTimeOperator = /*@__PURE__*/ (function () { + function DebounceTimeOperator(dueTime, scheduler) { + this.dueTime = dueTime; + this.scheduler = scheduler; + } + DebounceTimeOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new DebounceTimeSubscriber(subscriber, this.dueTime, this.scheduler)); + }; + return DebounceTimeOperator; +}()); +var DebounceTimeSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](DebounceTimeSubscriber, _super); + function DebounceTimeSubscriber(destination, dueTime, scheduler) { + var _this = _super.call(this, destination) || this; + _this.dueTime = dueTime; _this.scheduler = scheduler; - _this._events = []; - _this._infiniteTimeWindow = false; - _this._bufferSize = bufferSize < 1 ? 1 : bufferSize; - _this._windowTime = windowTime < 1 ? 1 : windowTime; - if (windowTime === Number.POSITIVE_INFINITY) { - _this._infiniteTimeWindow = true; - _this.next = _this.nextInfiniteTimeWindow; - } - else { - _this.next = _this.nextTimeWindow; - } + _this.debouncedSubscription = null; + _this.lastValue = null; + _this.hasValue = false; return _this; } - ReplaySubject.prototype.nextInfiniteTimeWindow = function (value) { - var _events = this._events; - _events.push(value); - if (_events.length > this._bufferSize) { - _events.shift(); - } - _super.prototype.next.call(this, value); + DebounceTimeSubscriber.prototype._next = function (value) { + this.clearDebounce(); + this.lastValue = value; + this.hasValue = true; + this.add(this.debouncedSubscription = this.scheduler.schedule(dispatchNext, this.dueTime, this)); }; - ReplaySubject.prototype.nextTimeWindow = function (value) { - this._events.push(new ReplayEvent(this._getNow(), value)); - this._trimBufferThenGetEvents(); - _super.prototype.next.call(this, value); + DebounceTimeSubscriber.prototype._complete = function () { + this.debouncedNext(); + this.destination.complete(); }; - ReplaySubject.prototype._subscribe = function (subscriber) { - var _infiniteTimeWindow = this._infiniteTimeWindow; - var _events = _infiniteTimeWindow ? this._events : this._trimBufferThenGetEvents(); - var scheduler = this.scheduler; - var len = _events.length; - var subscription; - if (this.closed) { - throw new _util_ObjectUnsubscribedError__WEBPACK_IMPORTED_MODULE_5__["ObjectUnsubscribedError"](); - } - else if (this.isStopped || this.hasError) { - subscription = _Subscription__WEBPACK_IMPORTED_MODULE_3__["Subscription"].EMPTY; - } - else { - this.observers.push(subscriber); - subscription = new _SubjectSubscription__WEBPACK_IMPORTED_MODULE_6__["SubjectSubscription"](this, subscriber); - } - if (scheduler) { - subscriber.add(subscriber = new _operators_observeOn__WEBPACK_IMPORTED_MODULE_4__["ObserveOnSubscriber"](subscriber, scheduler)); - } - if (_infiniteTimeWindow) { - for (var i = 0; i < len && !subscriber.closed; i++) { - subscriber.next(_events[i]); - } - } - else { - for (var i = 0; i < len && !subscriber.closed; i++) { - subscriber.next(_events[i].value); - } - } - if (this.hasError) { - subscriber.error(this.thrownError); - } - else if (this.isStopped) { - subscriber.complete(); + DebounceTimeSubscriber.prototype.debouncedNext = function () { + this.clearDebounce(); + if (this.hasValue) { + var lastValue = this.lastValue; + this.lastValue = null; + this.hasValue = false; + this.destination.next(lastValue); } - return subscription; - }; - ReplaySubject.prototype._getNow = function () { - return (this.scheduler || _scheduler_queue__WEBPACK_IMPORTED_MODULE_2__["queue"]).now(); }; - ReplaySubject.prototype._trimBufferThenGetEvents = function () { - var now = this._getNow(); - var _bufferSize = this._bufferSize; - var _windowTime = this._windowTime; - var _events = this._events; - var eventsCount = _events.length; - var spliceCount = 0; - while (spliceCount < eventsCount) { - if ((now - _events[spliceCount].time) < _windowTime) { - break; - } - spliceCount++; - } - if (eventsCount > _bufferSize) { - spliceCount = Math.max(spliceCount, eventsCount - _bufferSize); - } - if (spliceCount > 0) { - _events.splice(0, spliceCount); + DebounceTimeSubscriber.prototype.clearDebounce = function () { + var debouncedSubscription = this.debouncedSubscription; + if (debouncedSubscription !== null) { + this.remove(debouncedSubscription); + debouncedSubscription.unsubscribe(); + this.debouncedSubscription = null; } - return _events; }; - return ReplaySubject; -}(_Subject__WEBPACK_IMPORTED_MODULE_1__["Subject"])); - -var ReplayEvent = /*@__PURE__*/ (function () { - function ReplayEvent(time, value) { - this.time = time; - this.value = value; - } - return ReplayEvent; -}()); -//# sourceMappingURL=ReplaySubject.js.map + return DebounceTimeSubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); +function dispatchNext(subscriber) { + subscriber.debouncedNext(); +} +//# sourceMappingURL=debounceTime.js.map /***/ }), -/* 298 */ +/* 287 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "queue", function() { return queue; }); -/* harmony import */ var _QueueAction__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(299); -/* harmony import */ var _QueueScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(300); -/** PURE_IMPORTS_START _QueueAction,_QueueScheduler PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "defaultIfEmpty", function() { return defaultIfEmpty; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); +/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ -var queue = /*@__PURE__*/ new _QueueScheduler__WEBPACK_IMPORTED_MODULE_1__["QueueScheduler"](_QueueAction__WEBPACK_IMPORTED_MODULE_0__["QueueAction"]); -//# sourceMappingURL=queue.js.map +function defaultIfEmpty(defaultValue) { + if (defaultValue === void 0) { + defaultValue = null; + } + return function (source) { return source.lift(new DefaultIfEmptyOperator(defaultValue)); }; +} +var DefaultIfEmptyOperator = /*@__PURE__*/ (function () { + function DefaultIfEmptyOperator(defaultValue) { + this.defaultValue = defaultValue; + } + DefaultIfEmptyOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new DefaultIfEmptySubscriber(subscriber, this.defaultValue)); + }; + return DefaultIfEmptyOperator; +}()); +var DefaultIfEmptySubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](DefaultIfEmptySubscriber, _super); + function DefaultIfEmptySubscriber(destination, defaultValue) { + var _this = _super.call(this, destination) || this; + _this.defaultValue = defaultValue; + _this.isEmpty = true; + return _this; + } + DefaultIfEmptySubscriber.prototype._next = function (value) { + this.isEmpty = false; + this.destination.next(value); + }; + DefaultIfEmptySubscriber.prototype._complete = function () { + if (this.isEmpty) { + this.destination.next(this.defaultValue); + } + this.destination.complete(); + }; + return DefaultIfEmptySubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); +//# sourceMappingURL=defaultIfEmpty.js.map /***/ }), -/* 299 */ +/* 288 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "QueueAction", function() { return QueueAction; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "delay", function() { return delay; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _AsyncAction__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(200); -/** PURE_IMPORTS_START tslib,_AsyncAction PURE_IMPORTS_END */ +/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(215); +/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(289); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(172); +/* harmony import */ var _Notification__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(202); +/** PURE_IMPORTS_START tslib,_scheduler_async,_util_isDate,_Subscriber,_Notification PURE_IMPORTS_END */ -var QueueAction = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](QueueAction, _super); - function QueueAction(scheduler, work) { - var _this = _super.call(this, scheduler, work) || this; + + + +function delay(delay, scheduler) { + if (scheduler === void 0) { + scheduler = _scheduler_async__WEBPACK_IMPORTED_MODULE_1__["async"]; + } + var absoluteDelay = Object(_util_isDate__WEBPACK_IMPORTED_MODULE_2__["isDate"])(delay); + var delayFor = absoluteDelay ? (+delay - scheduler.now()) : Math.abs(delay); + return function (source) { return source.lift(new DelayOperator(delayFor, scheduler)); }; +} +var DelayOperator = /*@__PURE__*/ (function () { + function DelayOperator(delay, scheduler) { + this.delay = delay; + this.scheduler = scheduler; + } + DelayOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new DelaySubscriber(subscriber, this.delay, this.scheduler)); + }; + return DelayOperator; +}()); +var DelaySubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](DelaySubscriber, _super); + function DelaySubscriber(destination, delay, scheduler) { + var _this = _super.call(this, destination) || this; + _this.delay = delay; _this.scheduler = scheduler; - _this.work = work; + _this.queue = []; + _this.active = false; + _this.errored = false; return _this; } - QueueAction.prototype.schedule = function (state, delay) { - if (delay === void 0) { - delay = 0; + DelaySubscriber.dispatch = function (state) { + var source = state.source; + var queue = source.queue; + var scheduler = state.scheduler; + var destination = state.destination; + while (queue.length > 0 && (queue[0].time - scheduler.now()) <= 0) { + queue.shift().notification.observe(destination); } - if (delay > 0) { - return _super.prototype.schedule.call(this, state, delay); + if (queue.length > 0) { + var delay_1 = Math.max(0, queue[0].time - scheduler.now()); + this.schedule(state, delay_1); + } + else { + this.unsubscribe(); + source.active = false; } - this.delay = delay; - this.state = state; - this.scheduler.flush(this); - return this; }; - QueueAction.prototype.execute = function (state, delay) { - return (delay > 0 || this.closed) ? - _super.prototype.execute.call(this, state, delay) : - this._execute(state, delay); + DelaySubscriber.prototype._schedule = function (scheduler) { + this.active = true; + var destination = this.destination; + destination.add(scheduler.schedule(DelaySubscriber.dispatch, this.delay, { + source: this, destination: this.destination, scheduler: scheduler + })); }; - QueueAction.prototype.requestAsyncId = function (scheduler, id, delay) { - if (delay === void 0) { - delay = 0; + DelaySubscriber.prototype.scheduleNotification = function (notification) { + if (this.errored === true) { + return; } - if ((delay !== null && delay > 0) || (delay === null && this.delay > 0)) { - return _super.prototype.requestAsyncId.call(this, scheduler, id, delay); + var scheduler = this.scheduler; + var message = new DelayMessage(scheduler.now() + this.delay, notification); + this.queue.push(message); + if (this.active === false) { + this._schedule(scheduler); } - return scheduler.flush(this); }; - return QueueAction; -}(_AsyncAction__WEBPACK_IMPORTED_MODULE_1__["AsyncAction"])); - -//# sourceMappingURL=QueueAction.js.map - - -/***/ }), -/* 300 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "QueueScheduler", function() { return QueueScheduler; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _AsyncScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(202); -/** PURE_IMPORTS_START tslib,_AsyncScheduler PURE_IMPORTS_END */ - - -var QueueScheduler = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](QueueScheduler, _super); - function QueueScheduler() { - return _super !== null && _super.apply(this, arguments) || this; + DelaySubscriber.prototype._next = function (value) { + this.scheduleNotification(_Notification__WEBPACK_IMPORTED_MODULE_4__["Notification"].createNext(value)); + }; + DelaySubscriber.prototype._error = function (err) { + this.errored = true; + this.queue = []; + this.destination.error(err); + this.unsubscribe(); + }; + DelaySubscriber.prototype._complete = function () { + this.scheduleNotification(_Notification__WEBPACK_IMPORTED_MODULE_4__["Notification"].createComplete()); + this.unsubscribe(); + }; + return DelaySubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_3__["Subscriber"])); +var DelayMessage = /*@__PURE__*/ (function () { + function DelayMessage(time, notification) { + this.time = time; + this.notification = notification; } - return QueueScheduler; -}(_AsyncScheduler__WEBPACK_IMPORTED_MODULE_1__["AsyncScheduler"])); - -//# sourceMappingURL=QueueScheduler.js.map + return DelayMessage; +}()); +//# sourceMappingURL=delay.js.map /***/ }), -/* 301 */ +/* 289 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "race", function() { return race; }); -/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(178); -/* harmony import */ var _observable_race__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(302); -/** PURE_IMPORTS_START _util_isArray,_observable_race PURE_IMPORTS_END */ - - -function race() { - var observables = []; - for (var _i = 0; _i < arguments.length; _i++) { - observables[_i] = arguments[_i]; - } - return function raceOperatorFunction(source) { - if (observables.length === 1 && Object(_util_isArray__WEBPACK_IMPORTED_MODULE_0__["isArray"])(observables[0])) { - observables = observables[0]; - } - return source.lift.call(_observable_race__WEBPACK_IMPORTED_MODULE_1__["race"].apply(void 0, [source].concat(observables))); - }; +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isDate", function() { return isDate; }); +/** PURE_IMPORTS_START PURE_IMPORTS_END */ +function isDate(value) { + return value instanceof Date && !isNaN(+value); } -//# sourceMappingURL=race.js.map +//# sourceMappingURL=isDate.js.map /***/ }), -/* 302 */ +/* 290 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "race", function() { return race; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "RaceOperator", function() { return RaceOperator; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "RaceSubscriber", function() { return RaceSubscriber; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "delayWhen", function() { return delayWhen; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(178); -/* harmony import */ var _fromArray__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(215); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(171); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(182); -/** PURE_IMPORTS_START tslib,_util_isArray,_fromArray,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(170); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(229); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(230); +/** PURE_IMPORTS_START tslib,_Subscriber,_Observable,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ -function race() { - var observables = []; - for (var _i = 0; _i < arguments.length; _i++) { - observables[_i] = arguments[_i]; +function delayWhen(delayDurationSelector, subscriptionDelay) { + if (subscriptionDelay) { + return function (source) { + return new SubscriptionDelayObservable(source, subscriptionDelay) + .lift(new DelayWhenOperator(delayDurationSelector)); + }; } - if (observables.length === 1) { - if (Object(_util_isArray__WEBPACK_IMPORTED_MODULE_1__["isArray"])(observables[0])) { - observables = observables[0]; - } - else { - return observables[0]; - } - } - return Object(_fromArray__WEBPACK_IMPORTED_MODULE_2__["fromArray"])(observables, undefined).lift(new RaceOperator()); + return function (source) { return source.lift(new DelayWhenOperator(delayDurationSelector)); }; } -var RaceOperator = /*@__PURE__*/ (function () { - function RaceOperator() { +var DelayWhenOperator = /*@__PURE__*/ (function () { + function DelayWhenOperator(delayDurationSelector) { + this.delayDurationSelector = delayDurationSelector; } - RaceOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new RaceSubscriber(subscriber)); + DelayWhenOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new DelayWhenSubscriber(subscriber, this.delayDurationSelector)); }; - return RaceOperator; + return DelayWhenOperator; }()); - -var RaceSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](RaceSubscriber, _super); - function RaceSubscriber(destination) { +var DelayWhenSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](DelayWhenSubscriber, _super); + function DelayWhenSubscriber(destination, delayDurationSelector) { var _this = _super.call(this, destination) || this; - _this.hasFirst = false; - _this.observables = []; - _this.subscriptions = []; + _this.delayDurationSelector = delayDurationSelector; + _this.completed = false; + _this.delayNotifierSubscriptions = []; + _this.index = 0; return _this; } - RaceSubscriber.prototype._next = function (observable) { - this.observables.push(observable); + DelayWhenSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { + this.destination.next(outerValue); + this.removeSubscription(innerSub); + this.tryComplete(); }; - RaceSubscriber.prototype._complete = function () { - var observables = this.observables; - var len = observables.length; - if (len === 0) { - this.destination.complete(); + DelayWhenSubscriber.prototype.notifyError = function (error, innerSub) { + this._error(error); + }; + DelayWhenSubscriber.prototype.notifyComplete = function (innerSub) { + var value = this.removeSubscription(innerSub); + if (value) { + this.destination.next(value); } - else { - for (var i = 0; i < len && !this.hasFirst; i++) { - var observable = observables[i]; - var subscription = Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__["subscribeToResult"])(this, observable, observable, i); - if (this.subscriptions) { - this.subscriptions.push(subscription); - } - this.add(subscription); + this.tryComplete(); + }; + DelayWhenSubscriber.prototype._next = function (value) { + var index = this.index++; + try { + var delayNotifier = this.delayDurationSelector(value, index); + if (delayNotifier) { + this.tryDelay(delayNotifier, value); } - this.observables = null; + } + catch (err) { + this.destination.error(err); } }; - RaceSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { - if (!this.hasFirst) { - this.hasFirst = true; - for (var i = 0; i < this.subscriptions.length; i++) { - if (i !== outerIndex) { - var subscription = this.subscriptions[i]; - subscription.unsubscribe(); - this.remove(subscription); - } - } - this.subscriptions = null; + DelayWhenSubscriber.prototype._complete = function () { + this.completed = true; + this.tryComplete(); + this.unsubscribe(); + }; + DelayWhenSubscriber.prototype.removeSubscription = function (subscription) { + subscription.unsubscribe(); + var subscriptionIdx = this.delayNotifierSubscriptions.indexOf(subscription); + if (subscriptionIdx !== -1) { + this.delayNotifierSubscriptions.splice(subscriptionIdx, 1); } - this.destination.next(innerValue); + return subscription.outerValue; }; - return RaceSubscriber; + DelayWhenSubscriber.prototype.tryDelay = function (delayNotifier, value) { + var notifierSubscription = Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__["subscribeToResult"])(this, delayNotifier, value); + if (notifierSubscription && !notifierSubscription.closed) { + var destination = this.destination; + destination.add(notifierSubscription); + this.delayNotifierSubscriptions.push(notifierSubscription); + } + }; + DelayWhenSubscriber.prototype.tryComplete = function () { + if (this.completed && this.delayNotifierSubscriptions.length === 0) { + this.destination.complete(); + } + }; + return DelayWhenSubscriber; }(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__["OuterSubscriber"])); - -//# sourceMappingURL=race.js.map +var SubscriptionDelayObservable = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](SubscriptionDelayObservable, _super); + function SubscriptionDelayObservable(source, subscriptionDelay) { + var _this = _super.call(this) || this; + _this.source = source; + _this.subscriptionDelay = subscriptionDelay; + return _this; + } + SubscriptionDelayObservable.prototype._subscribe = function (subscriber) { + this.subscriptionDelay.subscribe(new SubscriptionDelaySubscriber(subscriber, this.source)); + }; + return SubscriptionDelayObservable; +}(_Observable__WEBPACK_IMPORTED_MODULE_2__["Observable"])); +var SubscriptionDelaySubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](SubscriptionDelaySubscriber, _super); + function SubscriptionDelaySubscriber(parent, source) { + var _this = _super.call(this) || this; + _this.parent = parent; + _this.source = source; + _this.sourceSubscribed = false; + return _this; + } + SubscriptionDelaySubscriber.prototype._next = function (unused) { + this.subscribeToSource(); + }; + SubscriptionDelaySubscriber.prototype._error = function (err) { + this.unsubscribe(); + this.parent.error(err); + }; + SubscriptionDelaySubscriber.prototype._complete = function () { + this.unsubscribe(); + this.subscribeToSource(); + }; + SubscriptionDelaySubscriber.prototype.subscribeToSource = function () { + if (!this.sourceSubscribed) { + this.sourceSubscribed = true; + this.unsubscribe(); + this.source.subscribe(this.parent); + } + }; + return SubscriptionDelaySubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); +//# sourceMappingURL=delayWhen.js.map /***/ }), -/* 303 */ +/* 291 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "repeat", function() { return repeat; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "dematerialize", function() { return dematerialize; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); /* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); -/* harmony import */ var _observable_empty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(242); -/** PURE_IMPORTS_START tslib,_Subscriber,_observable_empty PURE_IMPORTS_END */ - +/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ -function repeat(count) { - if (count === void 0) { - count = -1; - } - return function (source) { - if (count === 0) { - return Object(_observable_empty__WEBPACK_IMPORTED_MODULE_2__["empty"])(); - } - else if (count < 0) { - return source.lift(new RepeatOperator(-1, source)); - } - else { - return source.lift(new RepeatOperator(count - 1, source)); - } +function dematerialize() { + return function dematerializeOperatorFunction(source) { + return source.lift(new DeMaterializeOperator()); }; } -var RepeatOperator = /*@__PURE__*/ (function () { - function RepeatOperator(count, source) { - this.count = count; - this.source = source; +var DeMaterializeOperator = /*@__PURE__*/ (function () { + function DeMaterializeOperator() { } - RepeatOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new RepeatSubscriber(subscriber, this.count, this.source)); + DeMaterializeOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new DeMaterializeSubscriber(subscriber)); }; - return RepeatOperator; + return DeMaterializeOperator; }()); -var RepeatSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](RepeatSubscriber, _super); - function RepeatSubscriber(destination, count, source) { - var _this = _super.call(this, destination) || this; - _this.count = count; - _this.source = source; - return _this; +var DeMaterializeSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](DeMaterializeSubscriber, _super); + function DeMaterializeSubscriber(destination) { + return _super.call(this, destination) || this; } - RepeatSubscriber.prototype.complete = function () { - if (!this.isStopped) { - var _a = this, source = _a.source, count = _a.count; - if (count === 0) { - return _super.prototype.complete.call(this); - } - else if (count > -1) { - this.count = count - 1; - } - source.subscribe(this._unsubscribeAndRecycle()); - } + DeMaterializeSubscriber.prototype._next = function (value) { + value.observe(this.destination); }; - return RepeatSubscriber; + return DeMaterializeSubscriber; }(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); -//# sourceMappingURL=repeat.js.map +//# sourceMappingURL=dematerialize.js.map /***/ }), -/* 304 */ +/* 292 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "repeatWhen", function() { return repeatWhen; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "distinct", function() { return distinct; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "DistinctSubscriber", function() { return DistinctSubscriber; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(265); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(171); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(182); -/** PURE_IMPORTS_START tslib,_Subject,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ - +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(229); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(230); +/** PURE_IMPORTS_START tslib,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ -function repeatWhen(notifier) { - return function (source) { return source.lift(new RepeatWhenOperator(notifier)); }; +function distinct(keySelector, flushes) { + return function (source) { return source.lift(new DistinctOperator(keySelector, flushes)); }; } -var RepeatWhenOperator = /*@__PURE__*/ (function () { - function RepeatWhenOperator(notifier) { - this.notifier = notifier; +var DistinctOperator = /*@__PURE__*/ (function () { + function DistinctOperator(keySelector, flushes) { + this.keySelector = keySelector; + this.flushes = flushes; } - RepeatWhenOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new RepeatWhenSubscriber(subscriber, this.notifier, source)); + DistinctOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new DistinctSubscriber(subscriber, this.keySelector, this.flushes)); }; - return RepeatWhenOperator; + return DistinctOperator; }()); -var RepeatWhenSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](RepeatWhenSubscriber, _super); - function RepeatWhenSubscriber(destination, notifier, source) { +var DistinctSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](DistinctSubscriber, _super); + function DistinctSubscriber(destination, keySelector, flushes) { var _this = _super.call(this, destination) || this; - _this.notifier = notifier; - _this.source = source; - _this.sourceIsBeingSubscribedTo = true; + _this.keySelector = keySelector; + _this.values = new Set(); + if (flushes) { + _this.add(Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__["subscribeToResult"])(_this, flushes)); + } return _this; } - RepeatWhenSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { - this.sourceIsBeingSubscribedTo = true; - this.source.subscribe(this); - }; - RepeatWhenSubscriber.prototype.notifyComplete = function (innerSub) { - if (this.sourceIsBeingSubscribedTo === false) { - return _super.prototype.complete.call(this); - } + DistinctSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { + this.values.clear(); }; - RepeatWhenSubscriber.prototype.complete = function () { - this.sourceIsBeingSubscribedTo = false; - if (!this.isStopped) { - if (!this.retries) { - this.subscribeToRetries(); - } - if (!this.retriesSubscription || this.retriesSubscription.closed) { - return _super.prototype.complete.call(this); - } - this._unsubscribeAndRecycle(); - this.notifications.next(); - } + DistinctSubscriber.prototype.notifyError = function (error, innerSub) { + this._error(error); }; - RepeatWhenSubscriber.prototype._unsubscribe = function () { - var _a = this, notifications = _a.notifications, retriesSubscription = _a.retriesSubscription; - if (notifications) { - notifications.unsubscribe(); - this.notifications = null; + DistinctSubscriber.prototype._next = function (value) { + if (this.keySelector) { + this._useKeySelector(value); } - if (retriesSubscription) { - retriesSubscription.unsubscribe(); - this.retriesSubscription = null; + else { + this._finalizeNext(value, value); } - this.retries = null; - }; - RepeatWhenSubscriber.prototype._unsubscribeAndRecycle = function () { - var _unsubscribe = this._unsubscribe; - this._unsubscribe = null; - _super.prototype._unsubscribeAndRecycle.call(this); - this._unsubscribe = _unsubscribe; - return this; }; - RepeatWhenSubscriber.prototype.subscribeToRetries = function () { - this.notifications = new _Subject__WEBPACK_IMPORTED_MODULE_1__["Subject"](); - var retries; + DistinctSubscriber.prototype._useKeySelector = function (value) { + var key; + var destination = this.destination; try { - var notifier = this.notifier; - retries = notifier(this.notifications); + key = this.keySelector(value); } - catch (e) { - return _super.prototype.complete.call(this); + catch (err) { + destination.error(err); + return; } - this.retries = retries; - this.retriesSubscription = Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__["subscribeToResult"])(this, retries); + this._finalizeNext(key, value); }; - return RepeatWhenSubscriber; -}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_2__["OuterSubscriber"])); -//# sourceMappingURL=repeatWhen.js.map + DistinctSubscriber.prototype._finalizeNext = function (key, value) { + var values = this.values; + if (!values.has(key)) { + values.add(key); + this.destination.next(value); + } + }; + return DistinctSubscriber; +}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__["OuterSubscriber"])); + +//# sourceMappingURL=distinct.js.map /***/ }), -/* 305 */ +/* 293 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "retry", function() { return retry; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "distinctUntilChanged", function() { return distinctUntilChanged; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); /* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); /** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ -function retry(count) { - if (count === void 0) { - count = -1; - } - return function (source) { return source.lift(new RetryOperator(count, source)); }; +function distinctUntilChanged(compare, keySelector) { + return function (source) { return source.lift(new DistinctUntilChangedOperator(compare, keySelector)); }; } -var RetryOperator = /*@__PURE__*/ (function () { - function RetryOperator(count, source) { - this.count = count; - this.source = source; +var DistinctUntilChangedOperator = /*@__PURE__*/ (function () { + function DistinctUntilChangedOperator(compare, keySelector) { + this.compare = compare; + this.keySelector = keySelector; } - RetryOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new RetrySubscriber(subscriber, this.count, this.source)); + DistinctUntilChangedOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new DistinctUntilChangedSubscriber(subscriber, this.compare, this.keySelector)); }; - return RetryOperator; + return DistinctUntilChangedOperator; }()); -var RetrySubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](RetrySubscriber, _super); - function RetrySubscriber(destination, count, source) { +var DistinctUntilChangedSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](DistinctUntilChangedSubscriber, _super); + function DistinctUntilChangedSubscriber(destination, compare, keySelector) { var _this = _super.call(this, destination) || this; - _this.count = count; - _this.source = source; + _this.keySelector = keySelector; + _this.hasKey = false; + if (typeof compare === 'function') { + _this.compare = compare; + } return _this; } - RetrySubscriber.prototype.error = function (err) { - if (!this.isStopped) { - var _a = this, source = _a.source, count = _a.count; - if (count === 0) { - return _super.prototype.error.call(this, err); + DistinctUntilChangedSubscriber.prototype.compare = function (x, y) { + return x === y; + }; + DistinctUntilChangedSubscriber.prototype._next = function (value) { + var key; + try { + var keySelector = this.keySelector; + key = keySelector ? keySelector(value) : value; + } + catch (err) { + return this.destination.error(err); + } + var result = false; + if (this.hasKey) { + try { + var compare = this.compare; + result = compare(this.key, key); } - else if (count > -1) { - this.count = count - 1; + catch (err) { + return this.destination.error(err); } - source.subscribe(this._unsubscribeAndRecycle()); + } + else { + this.hasKey = true; + } + if (!result) { + this.key = key; + this.destination.next(value); } }; - return RetrySubscriber; + return DistinctUntilChangedSubscriber; }(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); -//# sourceMappingURL=retry.js.map +//# sourceMappingURL=distinctUntilChanged.js.map /***/ }), -/* 306 */ +/* 294 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "retryWhen", function() { return retryWhen; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(265); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(171); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(182); -/** PURE_IMPORTS_START tslib,_Subject,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "distinctUntilKeyChanged", function() { return distinctUntilKeyChanged; }); +/* harmony import */ var _distinctUntilChanged__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(293); +/** PURE_IMPORTS_START _distinctUntilChanged PURE_IMPORTS_END */ + +function distinctUntilKeyChanged(key, compare) { + return Object(_distinctUntilChanged__WEBPACK_IMPORTED_MODULE_0__["distinctUntilChanged"])(function (x, y) { return compare ? compare(x[key], y[key]) : x[key] === y[key]; }); +} +//# sourceMappingURL=distinctUntilKeyChanged.js.map +/***/ }), +/* 295 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "elementAt", function() { return elementAt; }); +/* harmony import */ var _util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(222); +/* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(264); +/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(296); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(287); +/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(297); +/** PURE_IMPORTS_START _util_ArgumentOutOfRangeError,_filter,_throwIfEmpty,_defaultIfEmpty,_take PURE_IMPORTS_END */ -function retryWhen(notifier) { - return function (source) { return source.lift(new RetryWhenOperator(notifier, source)); }; -} -var RetryWhenOperator = /*@__PURE__*/ (function () { - function RetryWhenOperator(notifier, source) { - this.notifier = notifier; - this.source = source; - } - RetryWhenOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new RetryWhenSubscriber(subscriber, this.notifier, this.source)); - }; - return RetryWhenOperator; -}()); -var RetryWhenSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](RetryWhenSubscriber, _super); - function RetryWhenSubscriber(destination, notifier, source) { - var _this = _super.call(this, destination) || this; - _this.notifier = notifier; - _this.source = source; - return _this; + + + + +function elementAt(index, defaultValue) { + if (index < 0) { + throw new _util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_0__["ArgumentOutOfRangeError"](); } - RetryWhenSubscriber.prototype.error = function (err) { - if (!this.isStopped) { - var errors = this.errors; - var retries = this.retries; - var retriesSubscription = this.retriesSubscription; - if (!retries) { - errors = new _Subject__WEBPACK_IMPORTED_MODULE_1__["Subject"](); - try { - var notifier = this.notifier; - retries = notifier(errors); - } - catch (e) { - return _super.prototype.error.call(this, e); - } - retriesSubscription = Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__["subscribeToResult"])(this, retries); - } - else { - this.errors = null; - this.retriesSubscription = null; - } - this._unsubscribeAndRecycle(); - this.errors = errors; - this.retries = retries; - this.retriesSubscription = retriesSubscription; - errors.next(err); - } - }; - RetryWhenSubscriber.prototype._unsubscribe = function () { - var _a = this, errors = _a.errors, retriesSubscription = _a.retriesSubscription; - if (errors) { - errors.unsubscribe(); - this.errors = null; - } - if (retriesSubscription) { - retriesSubscription.unsubscribe(); - this.retriesSubscription = null; - } - this.retries = null; - }; - RetryWhenSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { - var _unsubscribe = this._unsubscribe; - this._unsubscribe = null; - this._unsubscribeAndRecycle(); - this._unsubscribe = _unsubscribe; - this.source.subscribe(this); + var hasDefaultValue = arguments.length >= 2; + return function (source) { + return source.pipe(Object(_filter__WEBPACK_IMPORTED_MODULE_1__["filter"])(function (v, i) { return i === index; }), Object(_take__WEBPACK_IMPORTED_MODULE_4__["take"])(1), hasDefaultValue + ? Object(_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__["defaultIfEmpty"])(defaultValue) + : Object(_throwIfEmpty__WEBPACK_IMPORTED_MODULE_2__["throwIfEmpty"])(function () { return new _util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_0__["ArgumentOutOfRangeError"](); })); }; - return RetryWhenSubscriber; -}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_2__["OuterSubscriber"])); -//# sourceMappingURL=retryWhen.js.map +} +//# sourceMappingURL=elementAt.js.map /***/ }), -/* 307 */ +/* 296 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "sample", function() { return sample; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "throwIfEmpty", function() { return throwIfEmpty; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(171); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(182); -/** PURE_IMPORTS_START tslib,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ +/* harmony import */ var _util_EmptyError__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(223); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(172); +/** PURE_IMPORTS_START tslib,_util_EmptyError,_Subscriber PURE_IMPORTS_END */ -function sample(notifier) { - return function (source) { return source.lift(new SampleOperator(notifier)); }; -} -var SampleOperator = /*@__PURE__*/ (function () { - function SampleOperator(notifier) { - this.notifier = notifier; - } - SampleOperator.prototype.call = function (subscriber, source) { - var sampleSubscriber = new SampleSubscriber(subscriber); - var subscription = source.subscribe(sampleSubscriber); - subscription.add(Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__["subscribeToResult"])(sampleSubscriber, this.notifier)); - return subscription; - }; - return SampleOperator; -}()); -var SampleSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](SampleSubscriber, _super); - function SampleSubscriber() { - var _this = _super !== null && _super.apply(this, arguments) || this; - _this.hasValue = false; - return _this; +function throwIfEmpty(errorFactory) { + if (errorFactory === void 0) { + errorFactory = defaultErrorFactory; } - SampleSubscriber.prototype._next = function (value) { - this.value = value; - this.hasValue = true; - }; - SampleSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { - this.emitValue(); - }; - SampleSubscriber.prototype.notifyComplete = function () { - this.emitValue(); - }; - SampleSubscriber.prototype.emitValue = function () { - if (this.hasValue) { - this.hasValue = false; - this.destination.next(this.value); - } + return function (source) { + return source.lift(new ThrowIfEmptyOperator(errorFactory)); }; - return SampleSubscriber; -}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__["OuterSubscriber"])); -//# sourceMappingURL=sample.js.map - - -/***/ }), -/* 308 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "sampleTime", function() { return sampleTime; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); -/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(199); -/** PURE_IMPORTS_START tslib,_Subscriber,_scheduler_async PURE_IMPORTS_END */ - - - -function sampleTime(period, scheduler) { - if (scheduler === void 0) { - scheduler = _scheduler_async__WEBPACK_IMPORTED_MODULE_2__["async"]; - } - return function (source) { return source.lift(new SampleTimeOperator(period, scheduler)); }; } -var SampleTimeOperator = /*@__PURE__*/ (function () { - function SampleTimeOperator(period, scheduler) { - this.period = period; - this.scheduler = scheduler; +var ThrowIfEmptyOperator = /*@__PURE__*/ (function () { + function ThrowIfEmptyOperator(errorFactory) { + this.errorFactory = errorFactory; } - SampleTimeOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new SampleTimeSubscriber(subscriber, this.period, this.scheduler)); + ThrowIfEmptyOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new ThrowIfEmptySubscriber(subscriber, this.errorFactory)); }; - return SampleTimeOperator; + return ThrowIfEmptyOperator; }()); -var SampleTimeSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](SampleTimeSubscriber, _super); - function SampleTimeSubscriber(destination, period, scheduler) { +var ThrowIfEmptySubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](ThrowIfEmptySubscriber, _super); + function ThrowIfEmptySubscriber(destination, errorFactory) { var _this = _super.call(this, destination) || this; - _this.period = period; - _this.scheduler = scheduler; + _this.errorFactory = errorFactory; _this.hasValue = false; - _this.add(scheduler.schedule(dispatchNotification, period, { subscriber: _this, period: period })); return _this; } - SampleTimeSubscriber.prototype._next = function (value) { - this.lastValue = value; + ThrowIfEmptySubscriber.prototype._next = function (value) { this.hasValue = true; + this.destination.next(value); }; - SampleTimeSubscriber.prototype.notifyNext = function () { - if (this.hasValue) { - this.hasValue = false; - this.destination.next(this.lastValue); + ThrowIfEmptySubscriber.prototype._complete = function () { + if (!this.hasValue) { + var err = void 0; + try { + err = this.errorFactory(); + } + catch (e) { + err = e; + } + this.destination.error(err); + } + else { + return this.destination.complete(); } }; - return SampleTimeSubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); -function dispatchNotification(state) { - var subscriber = state.subscriber, period = state.period; - subscriber.notifyNext(); - this.schedule(state, period); + return ThrowIfEmptySubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_2__["Subscriber"])); +function defaultErrorFactory() { + return new _util_EmptyError__WEBPACK_IMPORTED_MODULE_1__["EmptyError"](); } -//# sourceMappingURL=sampleTime.js.map +//# sourceMappingURL=throwIfEmpty.js.map /***/ }), -/* 309 */ +/* 297 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "sequenceEqual", function() { return sequenceEqual; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SequenceEqualOperator", function() { return SequenceEqualOperator; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SequenceEqualSubscriber", function() { return SequenceEqualSubscriber; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "take", function() { return take; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); /* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); -/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ +/* harmony import */ var _util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(222); +/* harmony import */ var _observable_empty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(203); +/** PURE_IMPORTS_START tslib,_Subscriber,_util_ArgumentOutOfRangeError,_observable_empty PURE_IMPORTS_END */ -function sequenceEqual(compareTo, comparator) { - return function (source) { return source.lift(new SequenceEqualOperator(compareTo, comparator)); }; -} -var SequenceEqualOperator = /*@__PURE__*/ (function () { - function SequenceEqualOperator(compareTo, comparator) { - this.compareTo = compareTo; - this.comparator = comparator; - } - SequenceEqualOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new SequenceEqualSubscriber(subscriber, this.compareTo, this.comparator)); - }; - return SequenceEqualOperator; -}()); -var SequenceEqualSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](SequenceEqualSubscriber, _super); - function SequenceEqualSubscriber(destination, compareTo, comparator) { - var _this = _super.call(this, destination) || this; - _this.compareTo = compareTo; - _this.comparator = comparator; - _this._a = []; - _this._b = []; - _this._oneComplete = false; - _this.destination.add(compareTo.subscribe(new SequenceEqualCompareToSubscriber(destination, _this))); - return _this; - } - SequenceEqualSubscriber.prototype._next = function (value) { - if (this._oneComplete && this._b.length === 0) { - this.emit(false); - } - else { - this._a.push(value); - this.checkValues(); - } - }; - SequenceEqualSubscriber.prototype._complete = function () { - if (this._oneComplete) { - this.emit(this._a.length === 0 && this._b.length === 0); - } - else { - this._oneComplete = true; - } - this.unsubscribe(); - }; - SequenceEqualSubscriber.prototype.checkValues = function () { - var _c = this, _a = _c._a, _b = _c._b, comparator = _c.comparator; - while (_a.length > 0 && _b.length > 0) { - var a = _a.shift(); - var b = _b.shift(); - var areEqual = false; - try { - areEqual = comparator ? comparator(a, b) : a === b; - } - catch (e) { - this.destination.error(e); - } - if (!areEqual) { - this.emit(false); - } - } - }; - SequenceEqualSubscriber.prototype.emit = function (value) { - var destination = this.destination; - destination.next(value); - destination.complete(); - }; - SequenceEqualSubscriber.prototype.nextB = function (value) { - if (this._oneComplete && this._a.length === 0) { - this.emit(false); + +function take(count) { + return function (source) { + if (count === 0) { + return Object(_observable_empty__WEBPACK_IMPORTED_MODULE_3__["empty"])(); } else { - this._b.push(value); - this.checkValues(); + return source.lift(new TakeOperator(count)); } }; - SequenceEqualSubscriber.prototype.completeB = function () { - if (this._oneComplete) { - this.emit(this._a.length === 0 && this._b.length === 0); - } - else { - this._oneComplete = true; +} +var TakeOperator = /*@__PURE__*/ (function () { + function TakeOperator(total) { + this.total = total; + if (this.total < 0) { + throw new _util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_2__["ArgumentOutOfRangeError"]; } + } + TakeOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new TakeSubscriber(subscriber, this.total)); }; - return SequenceEqualSubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); - -var SequenceEqualCompareToSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](SequenceEqualCompareToSubscriber, _super); - function SequenceEqualCompareToSubscriber(destination, parent) { + return TakeOperator; +}()); +var TakeSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](TakeSubscriber, _super); + function TakeSubscriber(destination, total) { var _this = _super.call(this, destination) || this; - _this.parent = parent; + _this.total = total; + _this.count = 0; return _this; } - SequenceEqualCompareToSubscriber.prototype._next = function (value) { - this.parent.nextB(value); - }; - SequenceEqualCompareToSubscriber.prototype._error = function (err) { - this.parent.error(err); - this.unsubscribe(); - }; - SequenceEqualCompareToSubscriber.prototype._complete = function () { - this.parent.completeB(); - this.unsubscribe(); + TakeSubscriber.prototype._next = function (value) { + var total = this.total; + var count = ++this.count; + if (count <= total) { + this.destination.next(value); + if (count === total) { + this.destination.complete(); + this.unsubscribe(); + } + } }; - return SequenceEqualCompareToSubscriber; + return TakeSubscriber; }(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); -//# sourceMappingURL=sequenceEqual.js.map +//# sourceMappingURL=take.js.map /***/ }), -/* 310 */ +/* 298 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "share", function() { return share; }); -/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(282); -/* harmony import */ var _refCount__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(284); -/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(265); -/** PURE_IMPORTS_START _multicast,_refCount,_Subject PURE_IMPORTS_END */ - - - -function shareSubjectFactory() { - return new _Subject__WEBPACK_IMPORTED_MODULE_2__["Subject"](); -} -function share() { - return function (source) { return Object(_refCount__WEBPACK_IMPORTED_MODULE_1__["refCount"])()(Object(_multicast__WEBPACK_IMPORTED_MODULE_0__["multicast"])(shareSubjectFactory)(source)); }; -} -//# sourceMappingURL=share.js.map - - -/***/ }), -/* 311 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "endWith", function() { return endWith; }); +/* harmony import */ var _observable_concat__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(239); +/* harmony import */ var _observable_of__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(204); +/** PURE_IMPORTS_START _observable_concat,_observable_of PURE_IMPORTS_END */ -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "shareReplay", function() { return shareReplay; }); -/* harmony import */ var _ReplaySubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(297); -/** PURE_IMPORTS_START _ReplaySubject PURE_IMPORTS_END */ -function shareReplay(configOrBufferSize, windowTime, scheduler) { - var config; - if (configOrBufferSize && typeof configOrBufferSize === 'object') { - config = configOrBufferSize; - } - else { - config = { - bufferSize: configOrBufferSize, - windowTime: windowTime, - refCount: false, - scheduler: scheduler - }; +function endWith() { + var array = []; + for (var _i = 0; _i < arguments.length; _i++) { + array[_i] = arguments[_i]; } - return function (source) { return source.lift(shareReplayOperator(config)); }; -} -function shareReplayOperator(_a) { - var _b = _a.bufferSize, bufferSize = _b === void 0 ? Number.POSITIVE_INFINITY : _b, _c = _a.windowTime, windowTime = _c === void 0 ? Number.POSITIVE_INFINITY : _c, useRefCount = _a.refCount, scheduler = _a.scheduler; - var subject; - var refCount = 0; - var subscription; - var hasError = false; - var isComplete = false; - return function shareReplayOperation(source) { - refCount++; - if (!subject || hasError) { - hasError = false; - subject = new _ReplaySubject__WEBPACK_IMPORTED_MODULE_0__["ReplaySubject"](bufferSize, windowTime, scheduler); - subscription = source.subscribe({ - next: function (value) { subject.next(value); }, - error: function (err) { - hasError = true; - subject.error(err); - }, - complete: function () { - isComplete = true; - subject.complete(); - }, - }); - } - var innerSub = subject.subscribe(this); - this.add(function () { - refCount--; - innerSub.unsubscribe(); - if (subscription && !isComplete && useRefCount && refCount === 0) { - subscription.unsubscribe(); - subscription = undefined; - subject = undefined; - } - }); - }; + return function (source) { return Object(_observable_concat__WEBPACK_IMPORTED_MODULE_0__["concat"])(source, _observable_of__WEBPACK_IMPORTED_MODULE_1__["of"].apply(void 0, array)); }; } -//# sourceMappingURL=shareReplay.js.map +//# sourceMappingURL=endWith.js.map /***/ }), -/* 312 */ +/* 299 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "single", function() { return single; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "every", function() { return every; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); /* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); -/* harmony import */ var _util_EmptyError__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(253); -/** PURE_IMPORTS_START tslib,_Subscriber,_util_EmptyError PURE_IMPORTS_END */ - +/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ -function single(predicate) { - return function (source) { return source.lift(new SingleOperator(predicate, source)); }; +function every(predicate, thisArg) { + return function (source) { return source.lift(new EveryOperator(predicate, thisArg, source)); }; } -var SingleOperator = /*@__PURE__*/ (function () { - function SingleOperator(predicate, source) { +var EveryOperator = /*@__PURE__*/ (function () { + function EveryOperator(predicate, thisArg, source) { this.predicate = predicate; + this.thisArg = thisArg; this.source = source; } - SingleOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new SingleSubscriber(subscriber, this.predicate, this.source)); + EveryOperator.prototype.call = function (observer, source) { + return source.subscribe(new EverySubscriber(observer, this.predicate, this.thisArg, this.source)); }; - return SingleOperator; + return EveryOperator; }()); -var SingleSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](SingleSubscriber, _super); - function SingleSubscriber(destination, predicate, source) { +var EverySubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](EverySubscriber, _super); + function EverySubscriber(destination, predicate, thisArg, source) { var _this = _super.call(this, destination) || this; _this.predicate = predicate; + _this.thisArg = thisArg; _this.source = source; - _this.seenValue = false; _this.index = 0; + _this.thisArg = thisArg || _this; return _this; } - SingleSubscriber.prototype.applySingleValue = function (value) { - if (this.seenValue) { - this.destination.error('Sequence contains more than one element'); - } - else { - this.seenValue = true; - this.singleValue = value; - } - }; - SingleSubscriber.prototype._next = function (value) { - var index = this.index++; - if (this.predicate) { - this.tryNext(value, index); - } - else { - this.applySingleValue(value); - } + EverySubscriber.prototype.notifyComplete = function (everyValueMatch) { + this.destination.next(everyValueMatch); + this.destination.complete(); }; - SingleSubscriber.prototype.tryNext = function (value, index) { + EverySubscriber.prototype._next = function (value) { + var result = false; try { - if (this.predicate(value, index, this.source)) { - this.applySingleValue(value); - } + result = this.predicate.call(this.thisArg, value, this.index++, this.source); } catch (err) { this.destination.error(err); + return; } - }; - SingleSubscriber.prototype._complete = function () { - var destination = this.destination; - if (this.index > 0) { - destination.next(this.seenValue ? this.singleValue : undefined); - destination.complete(); - } - else { - destination.error(new _util_EmptyError__WEBPACK_IMPORTED_MODULE_2__["EmptyError"]); + if (!result) { + this.notifyComplete(false); } }; - return SingleSubscriber; + EverySubscriber.prototype._complete = function () { + this.notifyComplete(true); + }; + return EverySubscriber; }(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); -//# sourceMappingURL=single.js.map +//# sourceMappingURL=every.js.map /***/ }), -/* 313 */ +/* 300 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "skip", function() { return skip; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "exhaust", function() { return exhaust; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); -/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(229); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(230); +/** PURE_IMPORTS_START tslib,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ -function skip(count) { - return function (source) { return source.lift(new SkipOperator(count)); }; + +function exhaust() { + return function (source) { return source.lift(new SwitchFirstOperator()); }; } -var SkipOperator = /*@__PURE__*/ (function () { - function SkipOperator(total) { - this.total = total; +var SwitchFirstOperator = /*@__PURE__*/ (function () { + function SwitchFirstOperator() { } - SkipOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new SkipSubscriber(subscriber, this.total)); + SwitchFirstOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new SwitchFirstSubscriber(subscriber)); }; - return SkipOperator; + return SwitchFirstOperator; }()); -var SkipSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](SkipSubscriber, _super); - function SkipSubscriber(destination, total) { +var SwitchFirstSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](SwitchFirstSubscriber, _super); + function SwitchFirstSubscriber(destination) { var _this = _super.call(this, destination) || this; - _this.total = total; - _this.count = 0; + _this.hasCompleted = false; + _this.hasSubscription = false; return _this; } - SkipSubscriber.prototype._next = function (x) { - if (++this.count > this.total) { - this.destination.next(x); + SwitchFirstSubscriber.prototype._next = function (value) { + if (!this.hasSubscription) { + this.hasSubscription = true; + this.add(Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__["subscribeToResult"])(this, value)); } }; - return SkipSubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); -//# sourceMappingURL=skip.js.map + SwitchFirstSubscriber.prototype._complete = function () { + this.hasCompleted = true; + if (!this.hasSubscription) { + this.destination.complete(); + } + }; + SwitchFirstSubscriber.prototype.notifyComplete = function (innerSub) { + this.remove(innerSub); + this.hasSubscription = false; + if (this.hasCompleted) { + this.destination.complete(); + } + }; + return SwitchFirstSubscriber; +}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__["OuterSubscriber"])); +//# sourceMappingURL=exhaust.js.map /***/ }), -/* 314 */ +/* 301 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "skipLast", function() { return skipLast; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "exhaustMap", function() { return exhaustMap; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); -/* harmony import */ var _util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(250); -/** PURE_IMPORTS_START tslib,_Subscriber,_util_ArgumentOutOfRangeError PURE_IMPORTS_END */ +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(229); +/* harmony import */ var _InnerSubscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(231); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(230); +/* harmony import */ var _map__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(226); +/* harmony import */ var _observable_from__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(243); +/** PURE_IMPORTS_START tslib,_OuterSubscriber,_InnerSubscriber,_util_subscribeToResult,_map,_observable_from PURE_IMPORTS_END */ -function skipLast(count) { - return function (source) { return source.lift(new SkipLastOperator(count)); }; -} -var SkipLastOperator = /*@__PURE__*/ (function () { - function SkipLastOperator(_skipCount) { - this._skipCount = _skipCount; - if (this._skipCount < 0) { - throw new _util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_2__["ArgumentOutOfRangeError"]; - } + + + +function exhaustMap(project, resultSelector) { + if (resultSelector) { + return function (source) { return source.pipe(exhaustMap(function (a, i) { return Object(_observable_from__WEBPACK_IMPORTED_MODULE_5__["from"])(project(a, i)).pipe(Object(_map__WEBPACK_IMPORTED_MODULE_4__["map"])(function (b, ii) { return resultSelector(a, b, i, ii); })); })); }; } - SkipLastOperator.prototype.call = function (subscriber, source) { - if (this._skipCount === 0) { - return source.subscribe(new _Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"](subscriber)); - } - else { - return source.subscribe(new SkipLastSubscriber(subscriber, this._skipCount)); - } + return function (source) { + return source.lift(new ExhaustMapOperator(project)); }; - return SkipLastOperator; -}()); -var SkipLastSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](SkipLastSubscriber, _super); - function SkipLastSubscriber(destination, _skipCount) { +} +var ExhaustMapOperator = /*@__PURE__*/ (function () { + function ExhaustMapOperator(project) { + this.project = project; + } + ExhaustMapOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new ExhaustMapSubscriber(subscriber, this.project)); + }; + return ExhaustMapOperator; +}()); +var ExhaustMapSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](ExhaustMapSubscriber, _super); + function ExhaustMapSubscriber(destination, project) { var _this = _super.call(this, destination) || this; - _this._skipCount = _skipCount; - _this._count = 0; - _this._ring = new Array(_skipCount); + _this.project = project; + _this.hasSubscription = false; + _this.hasCompleted = false; + _this.index = 0; return _this; } - SkipLastSubscriber.prototype._next = function (value) { - var skipCount = this._skipCount; - var count = this._count++; - if (count < skipCount) { - this._ring[count] = value; + ExhaustMapSubscriber.prototype._next = function (value) { + if (!this.hasSubscription) { + this.tryNext(value); } - else { - var currentIndex = count % skipCount; - var ring = this._ring; - var oldValue = ring[currentIndex]; - ring[currentIndex] = value; - this.destination.next(oldValue); + }; + ExhaustMapSubscriber.prototype.tryNext = function (value) { + var result; + var index = this.index++; + try { + result = this.project(value, index); + } + catch (err) { + this.destination.error(err); + return; } + this.hasSubscription = true; + this._innerSub(result, value, index); }; - return SkipLastSubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); -//# sourceMappingURL=skipLast.js.map + ExhaustMapSubscriber.prototype._innerSub = function (result, value, index) { + var innerSubscriber = new _InnerSubscriber__WEBPACK_IMPORTED_MODULE_2__["InnerSubscriber"](this, undefined, undefined); + var destination = this.destination; + destination.add(innerSubscriber); + Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__["subscribeToResult"])(this, result, value, index, innerSubscriber); + }; + ExhaustMapSubscriber.prototype._complete = function () { + this.hasCompleted = true; + if (!this.hasSubscription) { + this.destination.complete(); + } + this.unsubscribe(); + }; + ExhaustMapSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { + this.destination.next(innerValue); + }; + ExhaustMapSubscriber.prototype.notifyError = function (err) { + this.destination.error(err); + }; + ExhaustMapSubscriber.prototype.notifyComplete = function (innerSub) { + var destination = this.destination; + destination.remove(innerSub); + this.hasSubscription = false; + if (this.hasCompleted) { + this.destination.complete(); + } + }; + return ExhaustMapSubscriber; +}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__["OuterSubscriber"])); +//# sourceMappingURL=exhaustMap.js.map /***/ }), -/* 315 */ +/* 302 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "skipUntil", function() { return skipUntil; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "expand", function() { return expand; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ExpandOperator", function() { return ExpandOperator; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ExpandSubscriber", function() { return ExpandSubscriber; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(171); -/* harmony import */ var _InnerSubscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(183); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(182); -/** PURE_IMPORTS_START tslib,_OuterSubscriber,_InnerSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ - +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(229); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(230); +/** PURE_IMPORTS_START tslib,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ -function skipUntil(notifier) { - return function (source) { return source.lift(new SkipUntilOperator(notifier)); }; +function expand(project, concurrent, scheduler) { + if (concurrent === void 0) { + concurrent = Number.POSITIVE_INFINITY; + } + if (scheduler === void 0) { + scheduler = undefined; + } + concurrent = (concurrent || 0) < 1 ? Number.POSITIVE_INFINITY : concurrent; + return function (source) { return source.lift(new ExpandOperator(project, concurrent, scheduler)); }; } -var SkipUntilOperator = /*@__PURE__*/ (function () { - function SkipUntilOperator(notifier) { - this.notifier = notifier; +var ExpandOperator = /*@__PURE__*/ (function () { + function ExpandOperator(project, concurrent, scheduler) { + this.project = project; + this.concurrent = concurrent; + this.scheduler = scheduler; } - SkipUntilOperator.prototype.call = function (destination, source) { - return source.subscribe(new SkipUntilSubscriber(destination, this.notifier)); + ExpandOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new ExpandSubscriber(subscriber, this.project, this.concurrent, this.scheduler)); }; - return SkipUntilOperator; + return ExpandOperator; }()); -var SkipUntilSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](SkipUntilSubscriber, _super); - function SkipUntilSubscriber(destination, notifier) { + +var ExpandSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](ExpandSubscriber, _super); + function ExpandSubscriber(destination, project, concurrent, scheduler) { var _this = _super.call(this, destination) || this; - _this.hasValue = false; - var innerSubscriber = new _InnerSubscriber__WEBPACK_IMPORTED_MODULE_2__["InnerSubscriber"](_this, undefined, undefined); - _this.add(innerSubscriber); - _this.innerSubscription = innerSubscriber; - Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__["subscribeToResult"])(_this, notifier, undefined, undefined, innerSubscriber); + _this.project = project; + _this.concurrent = concurrent; + _this.scheduler = scheduler; + _this.index = 0; + _this.active = 0; + _this.hasCompleted = false; + if (concurrent < Number.POSITIVE_INFINITY) { + _this.buffer = []; + } return _this; } - SkipUntilSubscriber.prototype._next = function (value) { - if (this.hasValue) { - _super.prototype._next.call(this, value); + ExpandSubscriber.dispatch = function (arg) { + var subscriber = arg.subscriber, result = arg.result, value = arg.value, index = arg.index; + subscriber.subscribeToProjection(result, value, index); + }; + ExpandSubscriber.prototype._next = function (value) { + var destination = this.destination; + if (destination.closed) { + this._complete(); + return; + } + var index = this.index++; + if (this.active < this.concurrent) { + destination.next(value); + try { + var project = this.project; + var result = project(value, index); + if (!this.scheduler) { + this.subscribeToProjection(result, value, index); + } + else { + var state = { subscriber: this, result: result, value: value, index: index }; + var destination_1 = this.destination; + destination_1.add(this.scheduler.schedule(ExpandSubscriber.dispatch, 0, state)); + } + } + catch (e) { + destination.error(e); + } + } + else { + this.buffer.push(value); } }; - SkipUntilSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { - this.hasValue = true; - if (this.innerSubscription) { - this.innerSubscription.unsubscribe(); + ExpandSubscriber.prototype.subscribeToProjection = function (result, value, index) { + this.active++; + var destination = this.destination; + destination.add(Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__["subscribeToResult"])(this, result, value, index)); + }; + ExpandSubscriber.prototype._complete = function () { + this.hasCompleted = true; + if (this.hasCompleted && this.active === 0) { + this.destination.complete(); } + this.unsubscribe(); }; - SkipUntilSubscriber.prototype.notifyComplete = function () { + ExpandSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { + this._next(innerValue); }; - return SkipUntilSubscriber; + ExpandSubscriber.prototype.notifyComplete = function (innerSub) { + var buffer = this.buffer; + var destination = this.destination; + destination.remove(innerSub); + this.active--; + if (buffer && buffer.length > 0) { + this._next(buffer.shift()); + } + if (this.hasCompleted && this.active === 0) { + this.destination.complete(); + } + }; + return ExpandSubscriber; }(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__["OuterSubscriber"])); -//# sourceMappingURL=skipUntil.js.map + +//# sourceMappingURL=expand.js.map /***/ }), -/* 316 */ +/* 303 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "skipWhile", function() { return skipWhile; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "finalize", function() { return finalize; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); +/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(177); +/** PURE_IMPORTS_START tslib,_Subscriber,_Subscription PURE_IMPORTS_END */ + + + +function finalize(callback) { + return function (source) { return source.lift(new FinallyOperator(callback)); }; +} +var FinallyOperator = /*@__PURE__*/ (function () { + function FinallyOperator(callback) { + this.callback = callback; + } + FinallyOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new FinallySubscriber(subscriber, this.callback)); + }; + return FinallyOperator; +}()); +var FinallySubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](FinallySubscriber, _super); + function FinallySubscriber(destination, callback) { + var _this = _super.call(this, destination) || this; + _this.add(new _Subscription__WEBPACK_IMPORTED_MODULE_2__["Subscription"](callback)); + return _this; + } + return FinallySubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); +//# sourceMappingURL=finalize.js.map + + +/***/ }), +/* 304 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "find", function() { return find; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "FindValueOperator", function() { return FindValueOperator; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "FindValueSubscriber", function() { return FindValueSubscriber; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); /* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); /** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ -function skipWhile(predicate) { - return function (source) { return source.lift(new SkipWhileOperator(predicate)); }; +function find(predicate, thisArg) { + if (typeof predicate !== 'function') { + throw new TypeError('predicate is not a function'); + } + return function (source) { return source.lift(new FindValueOperator(predicate, source, false, thisArg)); }; } -var SkipWhileOperator = /*@__PURE__*/ (function () { - function SkipWhileOperator(predicate) { +var FindValueOperator = /*@__PURE__*/ (function () { + function FindValueOperator(predicate, source, yieldIndex, thisArg) { this.predicate = predicate; + this.source = source; + this.yieldIndex = yieldIndex; + this.thisArg = thisArg; } - SkipWhileOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new SkipWhileSubscriber(subscriber, this.predicate)); + FindValueOperator.prototype.call = function (observer, source) { + return source.subscribe(new FindValueSubscriber(observer, this.predicate, this.source, this.yieldIndex, this.thisArg)); }; - return SkipWhileOperator; + return FindValueOperator; }()); -var SkipWhileSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](SkipWhileSubscriber, _super); - function SkipWhileSubscriber(destination, predicate) { + +var FindValueSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](FindValueSubscriber, _super); + function FindValueSubscriber(destination, predicate, source, yieldIndex, thisArg) { var _this = _super.call(this, destination) || this; _this.predicate = predicate; - _this.skipping = true; + _this.source = source; + _this.yieldIndex = yieldIndex; + _this.thisArg = thisArg; _this.index = 0; return _this; } - SkipWhileSubscriber.prototype._next = function (value) { + FindValueSubscriber.prototype.notifyComplete = function (value) { var destination = this.destination; - if (this.skipping) { - this.tryCallPredicate(value); - } - if (!this.skipping) { - destination.next(value); - } + destination.next(value); + destination.complete(); + this.unsubscribe(); }; - SkipWhileSubscriber.prototype.tryCallPredicate = function (value) { + FindValueSubscriber.prototype._next = function (value) { + var _a = this, predicate = _a.predicate, thisArg = _a.thisArg; + var index = this.index++; try { - var result = this.predicate(value, this.index++); - this.skipping = Boolean(result); + var result = predicate.call(thisArg || this, value, index, this.source); + if (result) { + this.notifyComplete(this.yieldIndex ? index : value); + } } catch (err) { this.destination.error(err); } }; - return SkipWhileSubscriber; + FindValueSubscriber.prototype._complete = function () { + this.notifyComplete(this.yieldIndex ? -1 : undefined); + }; + return FindValueSubscriber; }(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); -//# sourceMappingURL=skipWhile.js.map + +//# sourceMappingURL=find.js.map /***/ }), -/* 317 */ +/* 305 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "startWith", function() { return startWith; }); -/* harmony import */ var _observable_concat__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(226); -/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(206); -/** PURE_IMPORTS_START _observable_concat,_util_isScheduler PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "findIndex", function() { return findIndex; }); +/* harmony import */ var _operators_find__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(304); +/** PURE_IMPORTS_START _operators_find PURE_IMPORTS_END */ +function findIndex(predicate, thisArg) { + return function (source) { return source.lift(new _operators_find__WEBPACK_IMPORTED_MODULE_0__["FindValueOperator"](predicate, source, true, thisArg)); }; +} +//# sourceMappingURL=findIndex.js.map -function startWith() { - var array = []; - for (var _i = 0; _i < arguments.length; _i++) { - array[_i] = arguments[_i]; - } - var scheduler = array[array.length - 1]; - if (Object(_util_isScheduler__WEBPACK_IMPORTED_MODULE_1__["isScheduler"])(scheduler)) { - array.pop(); - return function (source) { return Object(_observable_concat__WEBPACK_IMPORTED_MODULE_0__["concat"])(array, source, scheduler); }; - } - else { - return function (source) { return Object(_observable_concat__WEBPACK_IMPORTED_MODULE_0__["concat"])(array, source); }; - } + +/***/ }), +/* 306 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "first", function() { return first; }); +/* harmony import */ var _util_EmptyError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(223); +/* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(264); +/* harmony import */ var _take__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(297); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(287); +/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(296); +/* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(220); +/** PURE_IMPORTS_START _util_EmptyError,_filter,_take,_defaultIfEmpty,_throwIfEmpty,_util_identity PURE_IMPORTS_END */ + + + + + + +function first(predicate, defaultValue) { + var hasDefaultValue = arguments.length >= 2; + return function (source) { return source.pipe(predicate ? Object(_filter__WEBPACK_IMPORTED_MODULE_1__["filter"])(function (v, i) { return predicate(v, i, source); }) : _util_identity__WEBPACK_IMPORTED_MODULE_5__["identity"], Object(_take__WEBPACK_IMPORTED_MODULE_2__["take"])(1), hasDefaultValue ? Object(_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_3__["defaultIfEmpty"])(defaultValue) : Object(_throwIfEmpty__WEBPACK_IMPORTED_MODULE_4__["throwIfEmpty"])(function () { return new _util_EmptyError__WEBPACK_IMPORTED_MODULE_0__["EmptyError"](); })); }; } -//# sourceMappingURL=startWith.js.map +//# sourceMappingURL=first.js.map /***/ }), -/* 318 */ +/* 307 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "subscribeOn", function() { return subscribeOn; }); -/* harmony import */ var _observable_SubscribeOnObservable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(319); -/** PURE_IMPORTS_START _observable_SubscribeOnObservable PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ignoreElements", function() { return ignoreElements; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); +/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ -function subscribeOn(scheduler, delay) { - if (delay === void 0) { - delay = 0; - } - return function subscribeOnOperatorFunction(source) { - return source.lift(new SubscribeOnOperator(scheduler, delay)); + +function ignoreElements() { + return function ignoreElementsOperatorFunction(source) { + return source.lift(new IgnoreElementsOperator()); }; } -var SubscribeOnOperator = /*@__PURE__*/ (function () { - function SubscribeOnOperator(scheduler, delay) { - this.scheduler = scheduler; - this.delay = delay; +var IgnoreElementsOperator = /*@__PURE__*/ (function () { + function IgnoreElementsOperator() { } - SubscribeOnOperator.prototype.call = function (subscriber, source) { - return new _observable_SubscribeOnObservable__WEBPACK_IMPORTED_MODULE_0__["SubscribeOnObservable"](source, this.delay, this.scheduler).subscribe(subscriber); + IgnoreElementsOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new IgnoreElementsSubscriber(subscriber)); }; - return SubscribeOnOperator; + return IgnoreElementsOperator; }()); -//# sourceMappingURL=subscribeOn.js.map +var IgnoreElementsSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](IgnoreElementsSubscriber, _super); + function IgnoreElementsSubscriber() { + return _super !== null && _super.apply(this, arguments) || this; + } + IgnoreElementsSubscriber.prototype._next = function (unused) { + }; + return IgnoreElementsSubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); +//# sourceMappingURL=ignoreElements.js.map /***/ }), -/* 319 */ +/* 308 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SubscribeOnObservable", function() { return SubscribeOnObservable; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isEmpty", function() { return isEmpty; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(193); -/* harmony import */ var _scheduler_asap__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(320); -/* harmony import */ var _util_isNumeric__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(205); -/** PURE_IMPORTS_START tslib,_Observable,_scheduler_asap,_util_isNumeric PURE_IMPORTS_END */ - - +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); +/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ -var SubscribeOnObservable = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](SubscribeOnObservable, _super); - function SubscribeOnObservable(source, delayTime, scheduler) { - if (delayTime === void 0) { - delayTime = 0; - } - if (scheduler === void 0) { - scheduler = _scheduler_asap__WEBPACK_IMPORTED_MODULE_2__["asap"]; - } - var _this = _super.call(this) || this; - _this.source = source; - _this.delayTime = delayTime; - _this.scheduler = scheduler; - if (!Object(_util_isNumeric__WEBPACK_IMPORTED_MODULE_3__["isNumeric"])(delayTime) || delayTime < 0) { - _this.delayTime = 0; - } - if (!scheduler || typeof scheduler.schedule !== 'function') { - _this.scheduler = _scheduler_asap__WEBPACK_IMPORTED_MODULE_2__["asap"]; - } - return _this; +function isEmpty() { + return function (source) { return source.lift(new IsEmptyOperator()); }; +} +var IsEmptyOperator = /*@__PURE__*/ (function () { + function IsEmptyOperator() { } - SubscribeOnObservable.create = function (source, delay, scheduler) { - if (delay === void 0) { - delay = 0; - } - if (scheduler === void 0) { - scheduler = _scheduler_asap__WEBPACK_IMPORTED_MODULE_2__["asap"]; - } - return new SubscribeOnObservable(source, delay, scheduler); + IsEmptyOperator.prototype.call = function (observer, source) { + return source.subscribe(new IsEmptySubscriber(observer)); }; - SubscribeOnObservable.dispatch = function (arg) { - var source = arg.source, subscriber = arg.subscriber; - return this.add(source.subscribe(subscriber)); + return IsEmptyOperator; +}()); +var IsEmptySubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](IsEmptySubscriber, _super); + function IsEmptySubscriber(destination) { + return _super.call(this, destination) || this; + } + IsEmptySubscriber.prototype.notifyComplete = function (isEmpty) { + var destination = this.destination; + destination.next(isEmpty); + destination.complete(); }; - SubscribeOnObservable.prototype._subscribe = function (subscriber) { - var delay = this.delayTime; - var source = this.source; - var scheduler = this.scheduler; - return scheduler.schedule(SubscribeOnObservable.dispatch, delay, { - source: source, subscriber: subscriber - }); + IsEmptySubscriber.prototype._next = function (value) { + this.notifyComplete(false); }; - return SubscribeOnObservable; -}(_Observable__WEBPACK_IMPORTED_MODULE_1__["Observable"])); - -//# sourceMappingURL=SubscribeOnObservable.js.map + IsEmptySubscriber.prototype._complete = function () { + this.notifyComplete(true); + }; + return IsEmptySubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); +//# sourceMappingURL=isEmpty.js.map /***/ }), -/* 320 */ +/* 309 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "asap", function() { return asap; }); -/* harmony import */ var _AsapAction__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(321); -/* harmony import */ var _AsapScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(323); -/** PURE_IMPORTS_START _AsapAction,_AsapScheduler PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "last", function() { return last; }); +/* harmony import */ var _util_EmptyError__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(223); +/* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(264); +/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(310); +/* harmony import */ var _throwIfEmpty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(296); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(287); +/* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(220); +/** PURE_IMPORTS_START _util_EmptyError,_filter,_takeLast,_throwIfEmpty,_defaultIfEmpty,_util_identity PURE_IMPORTS_END */ -var asap = /*@__PURE__*/ new _AsapScheduler__WEBPACK_IMPORTED_MODULE_1__["AsapScheduler"](_AsapAction__WEBPACK_IMPORTED_MODULE_0__["AsapAction"]); -//# sourceMappingURL=asap.js.map + + + + +function last(predicate, defaultValue) { + var hasDefaultValue = arguments.length >= 2; + return function (source) { return source.pipe(predicate ? Object(_filter__WEBPACK_IMPORTED_MODULE_1__["filter"])(function (v, i) { return predicate(v, i, source); }) : _util_identity__WEBPACK_IMPORTED_MODULE_5__["identity"], Object(_takeLast__WEBPACK_IMPORTED_MODULE_2__["takeLast"])(1), hasDefaultValue ? Object(_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_4__["defaultIfEmpty"])(defaultValue) : Object(_throwIfEmpty__WEBPACK_IMPORTED_MODULE_3__["throwIfEmpty"])(function () { return new _util_EmptyError__WEBPACK_IMPORTED_MODULE_0__["EmptyError"](); })); }; +} +//# sourceMappingURL=last.js.map /***/ }), -/* 321 */ +/* 310 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "AsapAction", function() { return AsapAction; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "takeLast", function() { return takeLast; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _util_Immediate__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(322); -/* harmony import */ var _AsyncAction__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(200); -/** PURE_IMPORTS_START tslib,_util_Immediate,_AsyncAction PURE_IMPORTS_END */ +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); +/* harmony import */ var _util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(222); +/* harmony import */ var _observable_empty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(203); +/** PURE_IMPORTS_START tslib,_Subscriber,_util_ArgumentOutOfRangeError,_observable_empty PURE_IMPORTS_END */ -var AsapAction = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](AsapAction, _super); - function AsapAction(scheduler, work) { - var _this = _super.call(this, scheduler, work) || this; - _this.scheduler = scheduler; - _this.work = work; - return _this; - } - AsapAction.prototype.requestAsyncId = function (scheduler, id, delay) { - if (delay === void 0) { - delay = 0; + +function takeLast(count) { + return function takeLastOperatorFunction(source) { + if (count === 0) { + return Object(_observable_empty__WEBPACK_IMPORTED_MODULE_3__["empty"])(); } - if (delay !== null && delay > 0) { - return _super.prototype.requestAsyncId.call(this, scheduler, id, delay); + else { + return source.lift(new TakeLastOperator(count)); } - scheduler.actions.push(this); - return scheduler.scheduled || (scheduler.scheduled = _util_Immediate__WEBPACK_IMPORTED_MODULE_1__["Immediate"].setImmediate(scheduler.flush.bind(scheduler, null))); }; - AsapAction.prototype.recycleAsyncId = function (scheduler, id, delay) { - if (delay === void 0) { - delay = 0; +} +var TakeLastOperator = /*@__PURE__*/ (function () { + function TakeLastOperator(total) { + this.total = total; + if (this.total < 0) { + throw new _util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_2__["ArgumentOutOfRangeError"]; } - if ((delay !== null && delay > 0) || (delay === null && this.delay > 0)) { - return _super.prototype.recycleAsyncId.call(this, scheduler, id, delay); + } + TakeLastOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new TakeLastSubscriber(subscriber, this.total)); + }; + return TakeLastOperator; +}()); +var TakeLastSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](TakeLastSubscriber, _super); + function TakeLastSubscriber(destination, total) { + var _this = _super.call(this, destination) || this; + _this.total = total; + _this.ring = new Array(); + _this.count = 0; + return _this; + } + TakeLastSubscriber.prototype._next = function (value) { + var ring = this.ring; + var total = this.total; + var count = this.count++; + if (ring.length < total) { + ring.push(value); } - if (scheduler.actions.length === 0) { - _util_Immediate__WEBPACK_IMPORTED_MODULE_1__["Immediate"].clearImmediate(id); - scheduler.scheduled = undefined; + else { + var index = count % total; + ring[index] = value; } - return undefined; }; - return AsapAction; -}(_AsyncAction__WEBPACK_IMPORTED_MODULE_2__["AsyncAction"])); - -//# sourceMappingURL=AsapAction.js.map + TakeLastSubscriber.prototype._complete = function () { + var destination = this.destination; + var count = this.count; + if (count > 0) { + var total = this.count >= this.total ? this.total : this.count; + var ring = this.ring; + for (var i = 0; i < total; i++) { + var idx = (count++) % total; + destination.next(ring[idx]); + } + } + destination.complete(); + }; + return TakeLastSubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); +//# sourceMappingURL=takeLast.js.map /***/ }), -/* 322 */ +/* 311 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Immediate", function() { return Immediate; }); -/** PURE_IMPORTS_START PURE_IMPORTS_END */ -var nextHandle = 1; -var tasksByHandle = {}; -function runIfPresent(handle) { - var cb = tasksByHandle[handle]; - if (cb) { - cb(); - } +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "mapTo", function() { return mapTo; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); +/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ + + +function mapTo(value) { + return function (source) { return source.lift(new MapToOperator(value)); }; } -var Immediate = { - setImmediate: function (cb) { - var handle = nextHandle++; - tasksByHandle[handle] = cb; - Promise.resolve().then(function () { return runIfPresent(handle); }); - return handle; - }, - clearImmediate: function (handle) { - delete tasksByHandle[handle]; - }, -}; -//# sourceMappingURL=Immediate.js.map +var MapToOperator = /*@__PURE__*/ (function () { + function MapToOperator(value) { + this.value = value; + } + MapToOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new MapToSubscriber(subscriber, this.value)); + }; + return MapToOperator; +}()); +var MapToSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](MapToSubscriber, _super); + function MapToSubscriber(destination, value) { + var _this = _super.call(this, destination) || this; + _this.value = value; + return _this; + } + MapToSubscriber.prototype._next = function (x) { + this.destination.next(this.value); + }; + return MapToSubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); +//# sourceMappingURL=mapTo.js.map /***/ }), -/* 323 */ +/* 312 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "AsapScheduler", function() { return AsapScheduler; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "materialize", function() { return materialize; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _AsyncScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(202); -/** PURE_IMPORTS_START tslib,_AsyncScheduler PURE_IMPORTS_END */ +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); +/* harmony import */ var _Notification__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(202); +/** PURE_IMPORTS_START tslib,_Subscriber,_Notification PURE_IMPORTS_END */ -var AsapScheduler = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](AsapScheduler, _super); - function AsapScheduler() { - return _super !== null && _super.apply(this, arguments) || this; + +function materialize() { + return function materializeOperatorFunction(source) { + return source.lift(new MaterializeOperator()); + }; +} +var MaterializeOperator = /*@__PURE__*/ (function () { + function MaterializeOperator() { } - AsapScheduler.prototype.flush = function (action) { - this.active = true; - this.scheduled = undefined; - var actions = this.actions; - var error; - var index = -1; - var count = actions.length; - action = action || actions.shift(); - do { - if (error = action.execute(action.state, action.delay)) { - break; - } - } while (++index < count && (action = actions.shift())); - this.active = false; - if (error) { - while (++index < count && (action = actions.shift())) { - action.unsubscribe(); - } - throw error; - } + MaterializeOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new MaterializeSubscriber(subscriber)); }; - return AsapScheduler; -}(_AsyncScheduler__WEBPACK_IMPORTED_MODULE_1__["AsyncScheduler"])); - -//# sourceMappingURL=AsapScheduler.js.map + return MaterializeOperator; +}()); +var MaterializeSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](MaterializeSubscriber, _super); + function MaterializeSubscriber(destination) { + return _super.call(this, destination) || this; + } + MaterializeSubscriber.prototype._next = function (value) { + this.destination.next(_Notification__WEBPACK_IMPORTED_MODULE_2__["Notification"].createNext(value)); + }; + MaterializeSubscriber.prototype._error = function (err) { + var destination = this.destination; + destination.next(_Notification__WEBPACK_IMPORTED_MODULE_2__["Notification"].createError(err)); + destination.complete(); + }; + MaterializeSubscriber.prototype._complete = function () { + var destination = this.destination; + destination.next(_Notification__WEBPACK_IMPORTED_MODULE_2__["Notification"].createComplete()); + destination.complete(); + }; + return MaterializeSubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); +//# sourceMappingURL=materialize.js.map /***/ }), -/* 324 */ +/* 313 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "switchAll", function() { return switchAll; }); -/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(325); -/* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(232); -/** PURE_IMPORTS_START _switchMap,_util_identity PURE_IMPORTS_END */ - +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "max", function() { return max; }); +/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(314); +/** PURE_IMPORTS_START _reduce PURE_IMPORTS_END */ -function switchAll() { - return Object(_switchMap__WEBPACK_IMPORTED_MODULE_0__["switchMap"])(_util_identity__WEBPACK_IMPORTED_MODULE_1__["identity"]); +function max(comparer) { + var max = (typeof comparer === 'function') + ? function (x, y) { return comparer(x, y) > 0 ? x : y; } + : function (x, y) { return x > y ? x : y; }; + return Object(_reduce__WEBPACK_IMPORTED_MODULE_0__["reduce"])(max); } -//# sourceMappingURL=switchAll.js.map +//# sourceMappingURL=max.js.map /***/ }), -/* 325 */ +/* 314 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "switchMap", function() { return switchMap; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(171); -/* harmony import */ var _InnerSubscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(183); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(182); -/* harmony import */ var _map__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(231); -/* harmony import */ var _observable_from__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(218); -/** PURE_IMPORTS_START tslib,_OuterSubscriber,_InnerSubscriber,_util_subscribeToResult,_map,_observable_from PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "reduce", function() { return reduce; }); +/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(315); +/* harmony import */ var _takeLast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(310); +/* harmony import */ var _defaultIfEmpty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(287); +/* harmony import */ var _util_pipe__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(184); +/** PURE_IMPORTS_START _scan,_takeLast,_defaultIfEmpty,_util_pipe PURE_IMPORTS_END */ + +function reduce(accumulator, seed) { + if (arguments.length >= 2) { + return function reduceOperatorFunctionWithSeed(source) { + return Object(_util_pipe__WEBPACK_IMPORTED_MODULE_3__["pipe"])(Object(_scan__WEBPACK_IMPORTED_MODULE_0__["scan"])(accumulator, seed), Object(_takeLast__WEBPACK_IMPORTED_MODULE_1__["takeLast"])(1), Object(_defaultIfEmpty__WEBPACK_IMPORTED_MODULE_2__["defaultIfEmpty"])(seed))(source); + }; + } + return function reduceOperatorFunction(source) { + return Object(_util_pipe__WEBPACK_IMPORTED_MODULE_3__["pipe"])(Object(_scan__WEBPACK_IMPORTED_MODULE_0__["scan"])(function (acc, value, index) { return accumulator(acc, value, index + 1); }), Object(_takeLast__WEBPACK_IMPORTED_MODULE_1__["takeLast"])(1))(source); + }; +} +//# sourceMappingURL=reduce.js.map +/***/ }), +/* 315 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { -function switchMap(project, resultSelector) { - if (typeof resultSelector === 'function') { - return function (source) { return source.pipe(switchMap(function (a, i) { return Object(_observable_from__WEBPACK_IMPORTED_MODULE_5__["from"])(project(a, i)).pipe(Object(_map__WEBPACK_IMPORTED_MODULE_4__["map"])(function (b, ii) { return resultSelector(a, b, i, ii); })); })); }; +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "scan", function() { return scan; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); +/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ + + +function scan(accumulator, seed) { + var hasSeed = false; + if (arguments.length >= 2) { + hasSeed = true; } - return function (source) { return source.lift(new SwitchMapOperator(project)); }; + return function scanOperatorFunction(source) { + return source.lift(new ScanOperator(accumulator, seed, hasSeed)); + }; } -var SwitchMapOperator = /*@__PURE__*/ (function () { - function SwitchMapOperator(project) { - this.project = project; +var ScanOperator = /*@__PURE__*/ (function () { + function ScanOperator(accumulator, seed, hasSeed) { + if (hasSeed === void 0) { + hasSeed = false; + } + this.accumulator = accumulator; + this.seed = seed; + this.hasSeed = hasSeed; } - SwitchMapOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new SwitchMapSubscriber(subscriber, this.project)); + ScanOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new ScanSubscriber(subscriber, this.accumulator, this.seed, this.hasSeed)); }; - return SwitchMapOperator; + return ScanOperator; }()); -var SwitchMapSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](SwitchMapSubscriber, _super); - function SwitchMapSubscriber(destination, project) { +var ScanSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](ScanSubscriber, _super); + function ScanSubscriber(destination, accumulator, _seed, hasSeed) { var _this = _super.call(this, destination) || this; - _this.project = project; + _this.accumulator = accumulator; + _this._seed = _seed; + _this.hasSeed = hasSeed; _this.index = 0; return _this; } - SwitchMapSubscriber.prototype._next = function (value) { - var result; - var index = this.index++; - try { - result = this.project(value, index); - } - catch (error) { - this.destination.error(error); - return; + Object.defineProperty(ScanSubscriber.prototype, "seed", { + get: function () { + return this._seed; + }, + set: function (value) { + this.hasSeed = true; + this._seed = value; + }, + enumerable: true, + configurable: true + }); + ScanSubscriber.prototype._next = function (value) { + if (!this.hasSeed) { + this.seed = value; + this.destination.next(value); } - this._innerSub(result, value, index); - }; - SwitchMapSubscriber.prototype._innerSub = function (result, value, index) { - var innerSubscription = this.innerSubscription; - if (innerSubscription) { - innerSubscription.unsubscribe(); + else { + return this._tryNext(value); } - var innerSubscriber = new _InnerSubscriber__WEBPACK_IMPORTED_MODULE_2__["InnerSubscriber"](this, undefined, undefined); - var destination = this.destination; - destination.add(innerSubscriber); - this.innerSubscription = Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__["subscribeToResult"])(this, result, value, index, innerSubscriber); }; - SwitchMapSubscriber.prototype._complete = function () { - var innerSubscription = this.innerSubscription; - if (!innerSubscription || innerSubscription.closed) { - _super.prototype._complete.call(this); + ScanSubscriber.prototype._tryNext = function (value) { + var index = this.index++; + var result; + try { + result = this.accumulator(this.seed, value, index); } - this.unsubscribe(); - }; - SwitchMapSubscriber.prototype._unsubscribe = function () { - this.innerSubscription = null; - }; - SwitchMapSubscriber.prototype.notifyComplete = function (innerSub) { - var destination = this.destination; - destination.remove(innerSub); - this.innerSubscription = null; - if (this.isStopped) { - _super.prototype._complete.call(this); + catch (err) { + this.destination.error(err); } + this.seed = result; + this.destination.next(result); }; - SwitchMapSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { - this.destination.next(innerValue); - }; - return SwitchMapSubscriber; -}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__["OuterSubscriber"])); -//# sourceMappingURL=switchMap.js.map + return ScanSubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); +//# sourceMappingURL=scan.js.map /***/ }), -/* 326 */ +/* 316 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "switchMapTo", function() { return switchMapTo; }); -/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(325); -/** PURE_IMPORTS_START _switchMap PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "merge", function() { return merge; }); +/* harmony import */ var _observable_merge__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(258); +/** PURE_IMPORTS_START _observable_merge PURE_IMPORTS_END */ -function switchMapTo(innerObservable, resultSelector) { - return resultSelector ? Object(_switchMap__WEBPACK_IMPORTED_MODULE_0__["switchMap"])(function () { return innerObservable; }, resultSelector) : Object(_switchMap__WEBPACK_IMPORTED_MODULE_0__["switchMap"])(function () { return innerObservable; }); +function merge() { + var observables = []; + for (var _i = 0; _i < arguments.length; _i++) { + observables[_i] = arguments[_i]; + } + return function (source) { return source.lift.call(_observable_merge__WEBPACK_IMPORTED_MODULE_0__["merge"].apply(void 0, [source].concat(observables))); }; } -//# sourceMappingURL=switchMapTo.js.map +//# sourceMappingURL=merge.js.map /***/ }), -/* 327 */ +/* 317 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "takeUntil", function() { return takeUntil; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(171); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(182); -/** PURE_IMPORTS_START tslib,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ - - +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "mergeMapTo", function() { return mergeMapTo; }); +/* harmony import */ var _mergeMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(242); +/** PURE_IMPORTS_START _mergeMap PURE_IMPORTS_END */ -function takeUntil(notifier) { - return function (source) { return source.lift(new TakeUntilOperator(notifier)); }; -} -var TakeUntilOperator = /*@__PURE__*/ (function () { - function TakeUntilOperator(notifier) { - this.notifier = notifier; +function mergeMapTo(innerObservable, resultSelector, concurrent) { + if (concurrent === void 0) { + concurrent = Number.POSITIVE_INFINITY; } - TakeUntilOperator.prototype.call = function (subscriber, source) { - var takeUntilSubscriber = new TakeUntilSubscriber(subscriber); - var notifierSubscription = Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__["subscribeToResult"])(takeUntilSubscriber, this.notifier); - if (notifierSubscription && !takeUntilSubscriber.seenValue) { - takeUntilSubscriber.add(notifierSubscription); - return source.subscribe(takeUntilSubscriber); - } - return takeUntilSubscriber; - }; - return TakeUntilOperator; -}()); -var TakeUntilSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](TakeUntilSubscriber, _super); - function TakeUntilSubscriber(destination) { - var _this = _super.call(this, destination) || this; - _this.seenValue = false; - return _this; + if (typeof resultSelector === 'function') { + return Object(_mergeMap__WEBPACK_IMPORTED_MODULE_0__["mergeMap"])(function () { return innerObservable; }, resultSelector, concurrent); } - TakeUntilSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { - this.seenValue = true; - this.complete(); - }; - TakeUntilSubscriber.prototype.notifyComplete = function () { - }; - return TakeUntilSubscriber; -}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__["OuterSubscriber"])); -//# sourceMappingURL=takeUntil.js.map + if (typeof resultSelector === 'number') { + concurrent = resultSelector; + } + return Object(_mergeMap__WEBPACK_IMPORTED_MODULE_0__["mergeMap"])(function () { return innerObservable; }, concurrent); +} +//# sourceMappingURL=mergeMapTo.js.map /***/ }), -/* 328 */ +/* 318 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "takeWhile", function() { return takeWhile; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "mergeScan", function() { return mergeScan; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MergeScanOperator", function() { return MergeScanOperator; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MergeScanSubscriber", function() { return MergeScanSubscriber; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); -/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(230); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(229); +/* harmony import */ var _InnerSubscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(231); +/** PURE_IMPORTS_START tslib,_util_subscribeToResult,_OuterSubscriber,_InnerSubscriber PURE_IMPORTS_END */ -function takeWhile(predicate, inclusive) { - if (inclusive === void 0) { - inclusive = false; + + +function mergeScan(accumulator, seed, concurrent) { + if (concurrent === void 0) { + concurrent = Number.POSITIVE_INFINITY; } - return function (source) { - return source.lift(new TakeWhileOperator(predicate, inclusive)); - }; + return function (source) { return source.lift(new MergeScanOperator(accumulator, seed, concurrent)); }; } -var TakeWhileOperator = /*@__PURE__*/ (function () { - function TakeWhileOperator(predicate, inclusive) { - this.predicate = predicate; - this.inclusive = inclusive; +var MergeScanOperator = /*@__PURE__*/ (function () { + function MergeScanOperator(accumulator, seed, concurrent) { + this.accumulator = accumulator; + this.seed = seed; + this.concurrent = concurrent; } - TakeWhileOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new TakeWhileSubscriber(subscriber, this.predicate, this.inclusive)); + MergeScanOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new MergeScanSubscriber(subscriber, this.accumulator, this.seed, this.concurrent)); }; - return TakeWhileOperator; + return MergeScanOperator; }()); -var TakeWhileSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](TakeWhileSubscriber, _super); - function TakeWhileSubscriber(destination, predicate, inclusive) { + +var MergeScanSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](MergeScanSubscriber, _super); + function MergeScanSubscriber(destination, accumulator, acc, concurrent) { var _this = _super.call(this, destination) || this; - _this.predicate = predicate; - _this.inclusive = inclusive; + _this.accumulator = accumulator; + _this.acc = acc; + _this.concurrent = concurrent; + _this.hasValue = false; + _this.hasCompleted = false; + _this.buffer = []; + _this.active = 0; _this.index = 0; return _this; } - TakeWhileSubscriber.prototype._next = function (value) { - var destination = this.destination; - var result; - try { - result = this.predicate(value, this.index++); + MergeScanSubscriber.prototype._next = function (value) { + if (this.active < this.concurrent) { + var index = this.index++; + var destination = this.destination; + var ish = void 0; + try { + var accumulator = this.accumulator; + ish = accumulator(this.acc, value, index); + } + catch (e) { + return destination.error(e); + } + this.active++; + this._innerSub(ish, value, index); } - catch (err) { - destination.error(err); - return; + else { + this.buffer.push(value); } - this.nextOrComplete(value, result); }; - TakeWhileSubscriber.prototype.nextOrComplete = function (value, predicateResult) { + MergeScanSubscriber.prototype._innerSub = function (ish, value, index) { + var innerSubscriber = new _InnerSubscriber__WEBPACK_IMPORTED_MODULE_3__["InnerSubscriber"](this, undefined, undefined); var destination = this.destination; - if (Boolean(predicateResult)) { - destination.next(value); - } - else { - if (this.inclusive) { - destination.next(value); + destination.add(innerSubscriber); + Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_1__["subscribeToResult"])(this, ish, value, index, innerSubscriber); + }; + MergeScanSubscriber.prototype._complete = function () { + this.hasCompleted = true; + if (this.active === 0 && this.buffer.length === 0) { + if (this.hasValue === false) { + this.destination.next(this.acc); } - destination.complete(); + this.destination.complete(); } + this.unsubscribe(); }; - return TakeWhileSubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); -//# sourceMappingURL=takeWhile.js.map - - -/***/ }), -/* 329 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "tap", function() { return tap; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); -/* harmony import */ var _util_noop__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(197); -/* harmony import */ var _util_isFunction__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(173); -/** PURE_IMPORTS_START tslib,_Subscriber,_util_noop,_util_isFunction PURE_IMPORTS_END */ + MergeScanSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { + var destination = this.destination; + this.acc = innerValue; + this.hasValue = true; + destination.next(innerValue); + }; + MergeScanSubscriber.prototype.notifyComplete = function (innerSub) { + var buffer = this.buffer; + var destination = this.destination; + destination.remove(innerSub); + this.active--; + if (buffer.length > 0) { + this._next(buffer.shift()); + } + else if (this.active === 0 && this.hasCompleted) { + if (this.hasValue === false) { + this.destination.next(this.acc); + } + this.destination.complete(); + } + }; + return MergeScanSubscriber; +}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_2__["OuterSubscriber"])); +//# sourceMappingURL=mergeScan.js.map +/***/ }), +/* 319 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { -function tap(nextOrObserver, error, complete) { - return function tapOperatorFunction(source) { - return source.lift(new DoOperator(nextOrObserver, error, complete)); - }; +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "min", function() { return min; }); +/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(314); +/** PURE_IMPORTS_START _reduce PURE_IMPORTS_END */ + +function min(comparer) { + var min = (typeof comparer === 'function') + ? function (x, y) { return comparer(x, y) < 0 ? x : y; } + : function (x, y) { return x < y ? x : y; }; + return Object(_reduce__WEBPACK_IMPORTED_MODULE_0__["reduce"])(min); } -var DoOperator = /*@__PURE__*/ (function () { - function DoOperator(nextOrObserver, error, complete) { - this.nextOrObserver = nextOrObserver; - this.error = error; - this.complete = complete; - } - DoOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new TapSubscriber(subscriber, this.nextOrObserver, this.error, this.complete)); - }; - return DoOperator; -}()); -var TapSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](TapSubscriber, _super); - function TapSubscriber(destination, observerOrNext, error, complete) { - var _this = _super.call(this, destination) || this; - _this._tapNext = _util_noop__WEBPACK_IMPORTED_MODULE_2__["noop"]; - _this._tapError = _util_noop__WEBPACK_IMPORTED_MODULE_2__["noop"]; - _this._tapComplete = _util_noop__WEBPACK_IMPORTED_MODULE_2__["noop"]; - _this._tapError = error || _util_noop__WEBPACK_IMPORTED_MODULE_2__["noop"]; - _this._tapComplete = complete || _util_noop__WEBPACK_IMPORTED_MODULE_2__["noop"]; - if (Object(_util_isFunction__WEBPACK_IMPORTED_MODULE_3__["isFunction"])(observerOrNext)) { - _this._context = _this; - _this._tapNext = observerOrNext; - } - else if (observerOrNext) { - _this._context = observerOrNext; - _this._tapNext = observerOrNext.next || _util_noop__WEBPACK_IMPORTED_MODULE_2__["noop"]; - _this._tapError = observerOrNext.error || _util_noop__WEBPACK_IMPORTED_MODULE_2__["noop"]; - _this._tapComplete = observerOrNext.complete || _util_noop__WEBPACK_IMPORTED_MODULE_2__["noop"]; - } - return _this; - } - TapSubscriber.prototype._next = function (value) { - try { - this._tapNext.call(this._context, value); - } - catch (err) { - this.destination.error(err); - return; +//# sourceMappingURL=min.js.map + + +/***/ }), +/* 320 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "multicast", function() { return multicast; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "MulticastOperator", function() { return MulticastOperator; }); +/* harmony import */ var _observable_ConnectableObservable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(186); +/** PURE_IMPORTS_START _observable_ConnectableObservable PURE_IMPORTS_END */ + +function multicast(subjectOrSubjectFactory, selector) { + return function multicastOperatorFunction(source) { + var subjectFactory; + if (typeof subjectOrSubjectFactory === 'function') { + subjectFactory = subjectOrSubjectFactory; } - this.destination.next(value); - }; - TapSubscriber.prototype._error = function (err) { - try { - this._tapError.call(this._context, err); + else { + subjectFactory = function subjectFactory() { + return subjectOrSubjectFactory; + }; } - catch (err) { - this.destination.error(err); - return; + if (typeof selector === 'function') { + return source.lift(new MulticastOperator(subjectFactory, selector)); } - this.destination.error(err); + var connectable = Object.create(source, _observable_ConnectableObservable__WEBPACK_IMPORTED_MODULE_0__["connectableObservableDescriptor"]); + connectable.source = source; + connectable.subjectFactory = subjectFactory; + return connectable; }; - TapSubscriber.prototype._complete = function () { - try { - this._tapComplete.call(this._context); - } - catch (err) { - this.destination.error(err); - return; - } - return this.destination.complete(); +} +var MulticastOperator = /*@__PURE__*/ (function () { + function MulticastOperator(subjectFactory, selector) { + this.subjectFactory = subjectFactory; + this.selector = selector; + } + MulticastOperator.prototype.call = function (subscriber, source) { + var selector = this.selector; + var subject = this.subjectFactory(); + var subscription = selector(subject).subscribe(subscriber); + subscription.add(source.subscribe(subject)); + return subscription; }; - return TapSubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); -//# sourceMappingURL=tap.js.map + return MulticastOperator; +}()); + +//# sourceMappingURL=multicast.js.map /***/ }), -/* 330 */ +/* 321 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "defaultThrottleConfig", function() { return defaultThrottleConfig; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "throttle", function() { return throttle; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "onErrorResumeNext", function() { return onErrorResumeNext; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "onErrorResumeNextStatic", function() { return onErrorResumeNextStatic; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(171); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(182); -/** PURE_IMPORTS_START tslib,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ +/* harmony import */ var _observable_from__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(243); +/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(178); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(229); +/* harmony import */ var _InnerSubscriber__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(231); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(230); +/** PURE_IMPORTS_START tslib,_observable_from,_util_isArray,_OuterSubscriber,_InnerSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ -var defaultThrottleConfig = { - leading: true, - trailing: false -}; -function throttle(durationSelector, config) { - if (config === void 0) { - config = defaultThrottleConfig; + + + +function onErrorResumeNext() { + var nextSources = []; + for (var _i = 0; _i < arguments.length; _i++) { + nextSources[_i] = arguments[_i]; } - return function (source) { return source.lift(new ThrottleOperator(durationSelector, config.leading, config.trailing)); }; + if (nextSources.length === 1 && Object(_util_isArray__WEBPACK_IMPORTED_MODULE_2__["isArray"])(nextSources[0])) { + nextSources = nextSources[0]; + } + return function (source) { return source.lift(new OnErrorResumeNextOperator(nextSources)); }; } -var ThrottleOperator = /*@__PURE__*/ (function () { - function ThrottleOperator(durationSelector, leading, trailing) { - this.durationSelector = durationSelector; - this.leading = leading; - this.trailing = trailing; +function onErrorResumeNextStatic() { + var nextSources = []; + for (var _i = 0; _i < arguments.length; _i++) { + nextSources[_i] = arguments[_i]; } - ThrottleOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new ThrottleSubscriber(subscriber, this.durationSelector, this.leading, this.trailing)); + var source = null; + if (nextSources.length === 1 && Object(_util_isArray__WEBPACK_IMPORTED_MODULE_2__["isArray"])(nextSources[0])) { + nextSources = nextSources[0]; + } + source = nextSources.shift(); + return Object(_observable_from__WEBPACK_IMPORTED_MODULE_1__["from"])(source, null).lift(new OnErrorResumeNextOperator(nextSources)); +} +var OnErrorResumeNextOperator = /*@__PURE__*/ (function () { + function OnErrorResumeNextOperator(nextSources) { + this.nextSources = nextSources; + } + OnErrorResumeNextOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new OnErrorResumeNextSubscriber(subscriber, this.nextSources)); }; - return ThrottleOperator; + return OnErrorResumeNextOperator; }()); -var ThrottleSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](ThrottleSubscriber, _super); - function ThrottleSubscriber(destination, durationSelector, _leading, _trailing) { +var OnErrorResumeNextSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](OnErrorResumeNextSubscriber, _super); + function OnErrorResumeNextSubscriber(destination, nextSources) { var _this = _super.call(this, destination) || this; _this.destination = destination; - _this.durationSelector = durationSelector; - _this._leading = _leading; - _this._trailing = _trailing; - _this._hasValue = false; + _this.nextSources = nextSources; return _this; } - ThrottleSubscriber.prototype._next = function (value) { - this._hasValue = true; - this._sendValue = value; - if (!this._throttled) { - if (this._leading) { - this.send(); - } - else { - this.throttle(value); - } - } + OnErrorResumeNextSubscriber.prototype.notifyError = function (error, innerSub) { + this.subscribeToNextSource(); }; - ThrottleSubscriber.prototype.send = function () { - var _a = this, _hasValue = _a._hasValue, _sendValue = _a._sendValue; - if (_hasValue) { - this.destination.next(_sendValue); - this.throttle(_sendValue); - } - this._hasValue = false; - this._sendValue = null; + OnErrorResumeNextSubscriber.prototype.notifyComplete = function (innerSub) { + this.subscribeToNextSource(); }; - ThrottleSubscriber.prototype.throttle = function (value) { - var duration = this.tryDurationSelector(value); - if (!!duration) { - this.add(this._throttled = Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__["subscribeToResult"])(this, duration)); - } + OnErrorResumeNextSubscriber.prototype._error = function (err) { + this.subscribeToNextSource(); + this.unsubscribe(); }; - ThrottleSubscriber.prototype.tryDurationSelector = function (value) { - try { - return this.durationSelector(value); - } - catch (err) { - this.destination.error(err); - return null; - } + OnErrorResumeNextSubscriber.prototype._complete = function () { + this.subscribeToNextSource(); + this.unsubscribe(); }; - ThrottleSubscriber.prototype.throttlingDone = function () { - var _a = this, _throttled = _a._throttled, _trailing = _a._trailing; - if (_throttled) { - _throttled.unsubscribe(); + OnErrorResumeNextSubscriber.prototype.subscribeToNextSource = function () { + var next = this.nextSources.shift(); + if (!!next) { + var innerSubscriber = new _InnerSubscriber__WEBPACK_IMPORTED_MODULE_4__["InnerSubscriber"](this, undefined, undefined); + var destination = this.destination; + destination.add(innerSubscriber); + Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_5__["subscribeToResult"])(this, next, undefined, undefined, innerSubscriber); } - this._throttled = null; - if (_trailing) { - this.send(); + else { + this.destination.complete(); } }; - ThrottleSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { - this.throttlingDone(); - }; - ThrottleSubscriber.prototype.notifyComplete = function () { - this.throttlingDone(); - }; - return ThrottleSubscriber; -}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__["OuterSubscriber"])); -//# sourceMappingURL=throttle.js.map + return OnErrorResumeNextSubscriber; +}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__["OuterSubscriber"])); +//# sourceMappingURL=onErrorResumeNext.js.map /***/ }), -/* 331 */ +/* 322 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "throttleTime", function() { return throttleTime; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "pairwise", function() { return pairwise; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); /* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); -/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(199); -/* harmony import */ var _throttle__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(330); -/** PURE_IMPORTS_START tslib,_Subscriber,_scheduler_async,_throttle PURE_IMPORTS_END */ - - +/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ -function throttleTime(duration, scheduler, config) { - if (scheduler === void 0) { - scheduler = _scheduler_async__WEBPACK_IMPORTED_MODULE_2__["async"]; - } - if (config === void 0) { - config = _throttle__WEBPACK_IMPORTED_MODULE_3__["defaultThrottleConfig"]; - } - return function (source) { return source.lift(new ThrottleTimeOperator(duration, scheduler, config.leading, config.trailing)); }; +function pairwise() { + return function (source) { return source.lift(new PairwiseOperator()); }; } -var ThrottleTimeOperator = /*@__PURE__*/ (function () { - function ThrottleTimeOperator(duration, scheduler, leading, trailing) { - this.duration = duration; - this.scheduler = scheduler; - this.leading = leading; - this.trailing = trailing; +var PairwiseOperator = /*@__PURE__*/ (function () { + function PairwiseOperator() { } - ThrottleTimeOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new ThrottleTimeSubscriber(subscriber, this.duration, this.scheduler, this.leading, this.trailing)); + PairwiseOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new PairwiseSubscriber(subscriber)); }; - return ThrottleTimeOperator; + return PairwiseOperator; }()); -var ThrottleTimeSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](ThrottleTimeSubscriber, _super); - function ThrottleTimeSubscriber(destination, duration, scheduler, leading, trailing) { +var PairwiseSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](PairwiseSubscriber, _super); + function PairwiseSubscriber(destination) { var _this = _super.call(this, destination) || this; - _this.duration = duration; - _this.scheduler = scheduler; - _this.leading = leading; - _this.trailing = trailing; - _this._hasTrailingValue = false; - _this._trailingValue = null; + _this.hasPrev = false; return _this; } - ThrottleTimeSubscriber.prototype._next = function (value) { - if (this.throttled) { - if (this.trailing) { - this._trailingValue = value; - this._hasTrailingValue = true; - } - } - else { - this.add(this.throttled = this.scheduler.schedule(dispatchNext, this.duration, { subscriber: this })); - if (this.leading) { - this.destination.next(value); - } - else if (this.trailing) { - this._trailingValue = value; - this._hasTrailingValue = true; - } - } - }; - ThrottleTimeSubscriber.prototype._complete = function () { - if (this._hasTrailingValue) { - this.destination.next(this._trailingValue); - this.destination.complete(); + PairwiseSubscriber.prototype._next = function (value) { + var pair; + if (this.hasPrev) { + pair = [this.prev, value]; } else { - this.destination.complete(); + this.hasPrev = true; } - }; - ThrottleTimeSubscriber.prototype.clearThrottle = function () { - var throttled = this.throttled; - if (throttled) { - if (this.trailing && this._hasTrailingValue) { - this.destination.next(this._trailingValue); - this._trailingValue = null; - this._hasTrailingValue = false; - } - throttled.unsubscribe(); - this.remove(throttled); - this.throttled = null; + this.prev = value; + if (pair) { + this.destination.next(pair); } }; - return ThrottleTimeSubscriber; + return PairwiseSubscriber; }(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); -function dispatchNext(arg) { - var subscriber = arg.subscriber; - subscriber.clearThrottle(); -} -//# sourceMappingURL=throttleTime.js.map +//# sourceMappingURL=pairwise.js.map /***/ }), -/* 332 */ +/* 323 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timeInterval", function() { return timeInterval; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TimeInterval", function() { return TimeInterval; }); -/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(199); -/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(276); -/* harmony import */ var _observable_defer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(333); -/* harmony import */ var _map__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(231); -/** PURE_IMPORTS_START _scheduler_async,_scan,_observable_defer,_map PURE_IMPORTS_END */ - - +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "partition", function() { return partition; }); +/* harmony import */ var _util_not__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(263); +/* harmony import */ var _filter__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(264); +/** PURE_IMPORTS_START _util_not,_filter PURE_IMPORTS_END */ -function timeInterval(scheduler) { - if (scheduler === void 0) { - scheduler = _scheduler_async__WEBPACK_IMPORTED_MODULE_0__["async"]; - } +function partition(predicate, thisArg) { return function (source) { - return Object(_observable_defer__WEBPACK_IMPORTED_MODULE_2__["defer"])(function () { - return source.pipe(Object(_scan__WEBPACK_IMPORTED_MODULE_1__["scan"])(function (_a, value) { - var current = _a.current; - return ({ value: value, current: scheduler.now(), last: current }); - }, { current: scheduler.now(), value: undefined, last: undefined }), Object(_map__WEBPACK_IMPORTED_MODULE_3__["map"])(function (_a) { - var current = _a.current, last = _a.last, value = _a.value; - return new TimeInterval(value, current - last); - })); - }); + return [ + Object(_filter__WEBPACK_IMPORTED_MODULE_1__["filter"])(predicate, thisArg)(source), + Object(_filter__WEBPACK_IMPORTED_MODULE_1__["filter"])(Object(_util_not__WEBPACK_IMPORTED_MODULE_0__["not"])(predicate, thisArg))(source) + ]; }; } -var TimeInterval = /*@__PURE__*/ (function () { - function TimeInterval(value, interval) { - this.value = value; - this.interval = interval; - } - return TimeInterval; -}()); - -//# sourceMappingURL=timeInterval.js.map +//# sourceMappingURL=partition.js.map /***/ }), -/* 333 */ +/* 324 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "defer", function() { return defer; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(193); -/* harmony import */ var _from__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(218); -/* harmony import */ var _empty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(242); -/** PURE_IMPORTS_START _Observable,_from,_empty PURE_IMPORTS_END */ - - +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "pluck", function() { return pluck; }); +/* harmony import */ var _map__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(226); +/** PURE_IMPORTS_START _map PURE_IMPORTS_END */ -function defer(observableFactory) { - return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { - var input; - try { - input = observableFactory(); - } - catch (err) { - subscriber.error(err); - return undefined; +function pluck() { + var properties = []; + for (var _i = 0; _i < arguments.length; _i++) { + properties[_i] = arguments[_i]; + } + var length = properties.length; + if (length === 0) { + throw new Error('list of properties cannot be empty.'); + } + return function (source) { return Object(_map__WEBPACK_IMPORTED_MODULE_0__["map"])(plucker(properties, length))(source); }; +} +function plucker(props, length) { + var mapper = function (x) { + var currentProp = x; + for (var i = 0; i < length; i++) { + var p = currentProp[props[i]]; + if (typeof p !== 'undefined') { + currentProp = p; + } + else { + return undefined; + } } - var source = input ? Object(_from__WEBPACK_IMPORTED_MODULE_1__["from"])(input) : Object(_empty__WEBPACK_IMPORTED_MODULE_2__["empty"])(); - return source.subscribe(subscriber); - }); + return currentProp; + }; + return mapper; } -//# sourceMappingURL=defer.js.map +//# sourceMappingURL=pluck.js.map /***/ }), -/* 334 */ +/* 325 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timeout", function() { return timeout; }); -/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(199); -/* harmony import */ var _util_TimeoutError__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(335); -/* harmony import */ var _timeoutWith__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(336); -/* harmony import */ var _observable_throwError__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(243); -/** PURE_IMPORTS_START _scheduler_async,_util_TimeoutError,_timeoutWith,_observable_throwError PURE_IMPORTS_END */ - - +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publish", function() { return publish; }); +/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(187); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(320); +/** PURE_IMPORTS_START _Subject,_multicast PURE_IMPORTS_END */ -function timeout(due, scheduler) { - if (scheduler === void 0) { - scheduler = _scheduler_async__WEBPACK_IMPORTED_MODULE_0__["async"]; - } - return Object(_timeoutWith__WEBPACK_IMPORTED_MODULE_2__["timeoutWith"])(due, Object(_observable_throwError__WEBPACK_IMPORTED_MODULE_3__["throwError"])(new _util_TimeoutError__WEBPACK_IMPORTED_MODULE_1__["TimeoutError"]()), scheduler); +function publish(selector) { + return selector ? + Object(_multicast__WEBPACK_IMPORTED_MODULE_1__["multicast"])(function () { return new _Subject__WEBPACK_IMPORTED_MODULE_0__["Subject"](); }, selector) : + Object(_multicast__WEBPACK_IMPORTED_MODULE_1__["multicast"])(new _Subject__WEBPACK_IMPORTED_MODULE_0__["Subject"]()); } -//# sourceMappingURL=timeout.js.map +//# sourceMappingURL=publish.js.map /***/ }), -/* 335 */ +/* 326 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TimeoutError", function() { return TimeoutError; }); -/** PURE_IMPORTS_START PURE_IMPORTS_END */ -var TimeoutErrorImpl = /*@__PURE__*/ (function () { - function TimeoutErrorImpl() { - Error.call(this); - this.message = 'Timeout has occurred'; - this.name = 'TimeoutError'; - return this; - } - TimeoutErrorImpl.prototype = /*@__PURE__*/ Object.create(Error.prototype); - return TimeoutErrorImpl; -})(); -var TimeoutError = TimeoutErrorImpl; -//# sourceMappingURL=TimeoutError.js.map +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publishBehavior", function() { return publishBehavior; }); +/* harmony import */ var _BehaviorSubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(192); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(320); +/** PURE_IMPORTS_START _BehaviorSubject,_multicast PURE_IMPORTS_END */ + + +function publishBehavior(value) { + return function (source) { return Object(_multicast__WEBPACK_IMPORTED_MODULE_1__["multicast"])(new _BehaviorSubject__WEBPACK_IMPORTED_MODULE_0__["BehaviorSubject"](value))(source); }; +} +//# sourceMappingURL=publishBehavior.js.map /***/ }), -/* 336 */ +/* 327 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timeoutWith", function() { return timeoutWith; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(199); -/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(240); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(171); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(182); -/** PURE_IMPORTS_START tslib,_scheduler_async,_util_isDate,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publishLast", function() { return publishLast; }); +/* harmony import */ var _AsyncSubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(210); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(320); +/** PURE_IMPORTS_START _AsyncSubject,_multicast PURE_IMPORTS_END */ +function publishLast() { + return function (source) { return Object(_multicast__WEBPACK_IMPORTED_MODULE_1__["multicast"])(new _AsyncSubject__WEBPACK_IMPORTED_MODULE_0__["AsyncSubject"]())(source); }; +} +//# sourceMappingURL=publishLast.js.map +/***/ }), +/* 328 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { -function timeoutWith(due, withObservable, scheduler) { - if (scheduler === void 0) { - scheduler = _scheduler_async__WEBPACK_IMPORTED_MODULE_1__["async"]; +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "publishReplay", function() { return publishReplay; }); +/* harmony import */ var _ReplaySubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(193); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(320); +/** PURE_IMPORTS_START _ReplaySubject,_multicast PURE_IMPORTS_END */ + + +function publishReplay(bufferSize, windowTime, selectorOrScheduler, scheduler) { + if (selectorOrScheduler && typeof selectorOrScheduler !== 'function') { + scheduler = selectorOrScheduler; } - return function (source) { - var absoluteTimeout = Object(_util_isDate__WEBPACK_IMPORTED_MODULE_2__["isDate"])(due); - var waitFor = absoluteTimeout ? (+due - scheduler.now()) : Math.abs(due); - return source.lift(new TimeoutWithOperator(waitFor, absoluteTimeout, withObservable, scheduler)); - }; + var selector = typeof selectorOrScheduler === 'function' ? selectorOrScheduler : undefined; + var subject = new _ReplaySubject__WEBPACK_IMPORTED_MODULE_0__["ReplaySubject"](bufferSize, windowTime, scheduler); + return function (source) { return Object(_multicast__WEBPACK_IMPORTED_MODULE_1__["multicast"])(function () { return subject; }, selector)(source); }; } -var TimeoutWithOperator = /*@__PURE__*/ (function () { - function TimeoutWithOperator(waitFor, absoluteTimeout, withObservable, scheduler) { - this.waitFor = waitFor; - this.absoluteTimeout = absoluteTimeout; - this.withObservable = withObservable; - this.scheduler = scheduler; - } - TimeoutWithOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new TimeoutWithSubscriber(subscriber, this.absoluteTimeout, this.waitFor, this.withObservable, this.scheduler)); - }; - return TimeoutWithOperator; -}()); -var TimeoutWithSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](TimeoutWithSubscriber, _super); - function TimeoutWithSubscriber(destination, absoluteTimeout, waitFor, withObservable, scheduler) { - var _this = _super.call(this, destination) || this; - _this.absoluteTimeout = absoluteTimeout; - _this.waitFor = waitFor; - _this.withObservable = withObservable; - _this.scheduler = scheduler; - _this.action = null; - _this.scheduleTimeout(); - return _this; - } - TimeoutWithSubscriber.dispatchTimeout = function (subscriber) { - var withObservable = subscriber.withObservable; - subscriber._unsubscribeAndRecycle(); - subscriber.add(Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__["subscribeToResult"])(subscriber, withObservable)); - }; - TimeoutWithSubscriber.prototype.scheduleTimeout = function () { - var action = this.action; - if (action) { - this.action = action.schedule(this, this.waitFor); - } - else { - this.add(this.action = this.scheduler.schedule(TimeoutWithSubscriber.dispatchTimeout, this.waitFor, this)); - } - }; - TimeoutWithSubscriber.prototype._next = function (value) { - if (!this.absoluteTimeout) { - this.scheduleTimeout(); - } - _super.prototype._next.call(this, value); - }; - TimeoutWithSubscriber.prototype._unsubscribe = function () { - this.action = null; - this.scheduler = null; - this.withObservable = null; - }; - return TimeoutWithSubscriber; -}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__["OuterSubscriber"])); -//# sourceMappingURL=timeoutWith.js.map +//# sourceMappingURL=publishReplay.js.map /***/ }), -/* 337 */ +/* 329 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timestamp", function() { return timestamp; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Timestamp", function() { return Timestamp; }); -/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(199); -/* harmony import */ var _map__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(231); -/** PURE_IMPORTS_START _scheduler_async,_map PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "race", function() { return race; }); +/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(178); +/* harmony import */ var _observable_race__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(265); +/** PURE_IMPORTS_START _util_isArray,_observable_race PURE_IMPORTS_END */ -function timestamp(scheduler) { - if (scheduler === void 0) { - scheduler = _scheduler_async__WEBPACK_IMPORTED_MODULE_0__["async"]; +function race() { + var observables = []; + for (var _i = 0; _i < arguments.length; _i++) { + observables[_i] = arguments[_i]; } - return Object(_map__WEBPACK_IMPORTED_MODULE_1__["map"])(function (value) { return new Timestamp(value, scheduler.now()); }); + return function raceOperatorFunction(source) { + if (observables.length === 1 && Object(_util_isArray__WEBPACK_IMPORTED_MODULE_0__["isArray"])(observables[0])) { + observables = observables[0]; + } + return source.lift.call(_observable_race__WEBPACK_IMPORTED_MODULE_1__["race"].apply(void 0, [source].concat(observables))); + }; } -var Timestamp = /*@__PURE__*/ (function () { - function Timestamp(value, timestamp) { - this.value = value; - this.timestamp = timestamp; - } - return Timestamp; -}()); - -//# sourceMappingURL=timestamp.js.map +//# sourceMappingURL=race.js.map /***/ }), -/* 338 */ +/* 330 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "toArray", function() { return toArray; }); -/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(275); -/** PURE_IMPORTS_START _reduce PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "repeat", function() { return repeat; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); +/* harmony import */ var _observable_empty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(203); +/** PURE_IMPORTS_START tslib,_Subscriber,_observable_empty PURE_IMPORTS_END */ -function toArrayReducer(arr, item, index) { - if (index === 0) { - return [item]; + + +function repeat(count) { + if (count === void 0) { + count = -1; } - arr.push(item); - return arr; -} -function toArray() { - return Object(_reduce__WEBPACK_IMPORTED_MODULE_0__["reduce"])(toArrayReducer, []); + return function (source) { + if (count === 0) { + return Object(_observable_empty__WEBPACK_IMPORTED_MODULE_2__["empty"])(); + } + else if (count < 0) { + return source.lift(new RepeatOperator(-1, source)); + } + else { + return source.lift(new RepeatOperator(count - 1, source)); + } + }; } -//# sourceMappingURL=toArray.js.map +var RepeatOperator = /*@__PURE__*/ (function () { + function RepeatOperator(count, source) { + this.count = count; + this.source = source; + } + RepeatOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new RepeatSubscriber(subscriber, this.count, this.source)); + }; + return RepeatOperator; +}()); +var RepeatSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](RepeatSubscriber, _super); + function RepeatSubscriber(destination, count, source) { + var _this = _super.call(this, destination) || this; + _this.count = count; + _this.source = source; + return _this; + } + RepeatSubscriber.prototype.complete = function () { + if (!this.isStopped) { + var _a = this, source = _a.source, count = _a.count; + if (count === 0) { + return _super.prototype.complete.call(this); + } + else if (count > -1) { + this.count = count - 1; + } + source.subscribe(this._unsubscribeAndRecycle()); + } + }; + return RepeatSubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); +//# sourceMappingURL=repeat.js.map /***/ }), -/* 339 */ +/* 331 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "window", function() { return window; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "repeatWhen", function() { return repeatWhen; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(265); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(171); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(182); +/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(187); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(229); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(230); /** PURE_IMPORTS_START tslib,_Subject,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ -function window(windowBoundaries) { - return function windowOperatorFunction(source) { - return source.lift(new WindowOperator(windowBoundaries)); - }; +function repeatWhen(notifier) { + return function (source) { return source.lift(new RepeatWhenOperator(notifier)); }; } -var WindowOperator = /*@__PURE__*/ (function () { - function WindowOperator(windowBoundaries) { - this.windowBoundaries = windowBoundaries; +var RepeatWhenOperator = /*@__PURE__*/ (function () { + function RepeatWhenOperator(notifier) { + this.notifier = notifier; } - WindowOperator.prototype.call = function (subscriber, source) { - var windowSubscriber = new WindowSubscriber(subscriber); - var sourceSubscription = source.subscribe(windowSubscriber); - if (!sourceSubscription.closed) { - windowSubscriber.add(Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__["subscribeToResult"])(windowSubscriber, this.windowBoundaries)); - } - return sourceSubscription; + RepeatWhenOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new RepeatWhenSubscriber(subscriber, this.notifier, source)); }; - return WindowOperator; + return RepeatWhenOperator; }()); -var WindowSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](WindowSubscriber, _super); - function WindowSubscriber(destination) { +var RepeatWhenSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](RepeatWhenSubscriber, _super); + function RepeatWhenSubscriber(destination, notifier, source) { var _this = _super.call(this, destination) || this; - _this.window = new _Subject__WEBPACK_IMPORTED_MODULE_1__["Subject"](); - destination.next(_this.window); + _this.notifier = notifier; + _this.source = source; + _this.sourceIsBeingSubscribedTo = true; return _this; } - WindowSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { - this.openWindow(); - }; - WindowSubscriber.prototype.notifyError = function (error, innerSub) { - this._error(error); - }; - WindowSubscriber.prototype.notifyComplete = function (innerSub) { - this._complete(); + RepeatWhenSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { + this.sourceIsBeingSubscribedTo = true; + this.source.subscribe(this); }; - WindowSubscriber.prototype._next = function (value) { - this.window.next(value); + RepeatWhenSubscriber.prototype.notifyComplete = function (innerSub) { + if (this.sourceIsBeingSubscribedTo === false) { + return _super.prototype.complete.call(this); + } }; - WindowSubscriber.prototype._error = function (err) { - this.window.error(err); - this.destination.error(err); + RepeatWhenSubscriber.prototype.complete = function () { + this.sourceIsBeingSubscribedTo = false; + if (!this.isStopped) { + if (!this.retries) { + this.subscribeToRetries(); + } + if (!this.retriesSubscription || this.retriesSubscription.closed) { + return _super.prototype.complete.call(this); + } + this._unsubscribeAndRecycle(); + this.notifications.next(); + } }; - WindowSubscriber.prototype._complete = function () { - this.window.complete(); - this.destination.complete(); + RepeatWhenSubscriber.prototype._unsubscribe = function () { + var _a = this, notifications = _a.notifications, retriesSubscription = _a.retriesSubscription; + if (notifications) { + notifications.unsubscribe(); + this.notifications = null; + } + if (retriesSubscription) { + retriesSubscription.unsubscribe(); + this.retriesSubscription = null; + } + this.retries = null; }; - WindowSubscriber.prototype._unsubscribe = function () { - this.window = null; + RepeatWhenSubscriber.prototype._unsubscribeAndRecycle = function () { + var _unsubscribe = this._unsubscribe; + this._unsubscribe = null; + _super.prototype._unsubscribeAndRecycle.call(this); + this._unsubscribe = _unsubscribe; + return this; }; - WindowSubscriber.prototype.openWindow = function () { - var prevWindow = this.window; - if (prevWindow) { - prevWindow.complete(); + RepeatWhenSubscriber.prototype.subscribeToRetries = function () { + this.notifications = new _Subject__WEBPACK_IMPORTED_MODULE_1__["Subject"](); + var retries; + try { + var notifier = this.notifier; + retries = notifier(this.notifications); } - var destination = this.destination; - var newWindow = this.window = new _Subject__WEBPACK_IMPORTED_MODULE_1__["Subject"](); - destination.next(newWindow); + catch (e) { + return _super.prototype.complete.call(this); + } + this.retries = retries; + this.retriesSubscription = Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__["subscribeToResult"])(this, retries); }; - return WindowSubscriber; + return RepeatWhenSubscriber; }(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_2__["OuterSubscriber"])); -//# sourceMappingURL=window.js.map +//# sourceMappingURL=repeatWhen.js.map /***/ }), -/* 340 */ +/* 332 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "windowCount", function() { return windowCount; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "retry", function() { return retry; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); /* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); -/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(265); -/** PURE_IMPORTS_START tslib,_Subscriber,_Subject PURE_IMPORTS_END */ - +/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ -function windowCount(windowSize, startWindowEvery) { - if (startWindowEvery === void 0) { - startWindowEvery = 0; +function retry(count) { + if (count === void 0) { + count = -1; } - return function windowCountOperatorFunction(source) { - return source.lift(new WindowCountOperator(windowSize, startWindowEvery)); - }; + return function (source) { return source.lift(new RetryOperator(count, source)); }; } -var WindowCountOperator = /*@__PURE__*/ (function () { - function WindowCountOperator(windowSize, startWindowEvery) { - this.windowSize = windowSize; - this.startWindowEvery = startWindowEvery; +var RetryOperator = /*@__PURE__*/ (function () { + function RetryOperator(count, source) { + this.count = count; + this.source = source; } - WindowCountOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new WindowCountSubscriber(subscriber, this.windowSize, this.startWindowEvery)); + RetryOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new RetrySubscriber(subscriber, this.count, this.source)); }; - return WindowCountOperator; + return RetryOperator; }()); -var WindowCountSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](WindowCountSubscriber, _super); - function WindowCountSubscriber(destination, windowSize, startWindowEvery) { +var RetrySubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](RetrySubscriber, _super); + function RetrySubscriber(destination, count, source) { var _this = _super.call(this, destination) || this; - _this.destination = destination; - _this.windowSize = windowSize; - _this.startWindowEvery = startWindowEvery; - _this.windows = [new _Subject__WEBPACK_IMPORTED_MODULE_2__["Subject"]()]; - _this.count = 0; - destination.next(_this.windows[0]); + _this.count = count; + _this.source = source; return _this; } - WindowCountSubscriber.prototype._next = function (value) { - var startWindowEvery = (this.startWindowEvery > 0) ? this.startWindowEvery : this.windowSize; - var destination = this.destination; - var windowSize = this.windowSize; - var windows = this.windows; - var len = windows.length; - for (var i = 0; i < len && !this.closed; i++) { - windows[i].next(value); - } - var c = this.count - windowSize + 1; - if (c >= 0 && c % startWindowEvery === 0 && !this.closed) { - windows.shift().complete(); - } - if (++this.count % startWindowEvery === 0 && !this.closed) { - var window_1 = new _Subject__WEBPACK_IMPORTED_MODULE_2__["Subject"](); - windows.push(window_1); - destination.next(window_1); - } - }; - WindowCountSubscriber.prototype._error = function (err) { - var windows = this.windows; - if (windows) { - while (windows.length > 0 && !this.closed) { - windows.shift().error(err); + RetrySubscriber.prototype.error = function (err) { + if (!this.isStopped) { + var _a = this, source = _a.source, count = _a.count; + if (count === 0) { + return _super.prototype.error.call(this, err); } - } - this.destination.error(err); - }; - WindowCountSubscriber.prototype._complete = function () { - var windows = this.windows; - if (windows) { - while (windows.length > 0 && !this.closed) { - windows.shift().complete(); + else if (count > -1) { + this.count = count - 1; } + source.subscribe(this._unsubscribeAndRecycle()); } - this.destination.complete(); - }; - WindowCountSubscriber.prototype._unsubscribe = function () { - this.count = 0; - this.windows = null; }; - return WindowCountSubscriber; + return RetrySubscriber; }(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); -//# sourceMappingURL=windowCount.js.map +//# sourceMappingURL=retry.js.map /***/ }), -/* 341 */ +/* 333 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "windowTime", function() { return windowTime; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "retryWhen", function() { return retryWhen; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(265); -/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(199); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(172); -/* harmony import */ var _util_isNumeric__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(205); -/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(206); -/** PURE_IMPORTS_START tslib,_Subject,_scheduler_async,_Subscriber,_util_isNumeric,_util_isScheduler PURE_IMPORTS_END */ - - +/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(187); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(229); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(230); +/** PURE_IMPORTS_START tslib,_Subject,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ -function windowTime(windowTimeSpan) { - var scheduler = _scheduler_async__WEBPACK_IMPORTED_MODULE_2__["async"]; - var windowCreationInterval = null; - var maxWindowSize = Number.POSITIVE_INFINITY; - if (Object(_util_isScheduler__WEBPACK_IMPORTED_MODULE_5__["isScheduler"])(arguments[3])) { - scheduler = arguments[3]; - } - if (Object(_util_isScheduler__WEBPACK_IMPORTED_MODULE_5__["isScheduler"])(arguments[2])) { - scheduler = arguments[2]; - } - else if (Object(_util_isNumeric__WEBPACK_IMPORTED_MODULE_4__["isNumeric"])(arguments[2])) { - maxWindowSize = arguments[2]; - } - if (Object(_util_isScheduler__WEBPACK_IMPORTED_MODULE_5__["isScheduler"])(arguments[1])) { - scheduler = arguments[1]; - } - else if (Object(_util_isNumeric__WEBPACK_IMPORTED_MODULE_4__["isNumeric"])(arguments[1])) { - windowCreationInterval = arguments[1]; - } - return function windowTimeOperatorFunction(source) { - return source.lift(new WindowTimeOperator(windowTimeSpan, windowCreationInterval, maxWindowSize, scheduler)); - }; +function retryWhen(notifier) { + return function (source) { return source.lift(new RetryWhenOperator(notifier, source)); }; } -var WindowTimeOperator = /*@__PURE__*/ (function () { - function WindowTimeOperator(windowTimeSpan, windowCreationInterval, maxWindowSize, scheduler) { - this.windowTimeSpan = windowTimeSpan; - this.windowCreationInterval = windowCreationInterval; - this.maxWindowSize = maxWindowSize; - this.scheduler = scheduler; +var RetryWhenOperator = /*@__PURE__*/ (function () { + function RetryWhenOperator(notifier, source) { + this.notifier = notifier; + this.source = source; } - WindowTimeOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new WindowTimeSubscriber(subscriber, this.windowTimeSpan, this.windowCreationInterval, this.maxWindowSize, this.scheduler)); + RetryWhenOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new RetryWhenSubscriber(subscriber, this.notifier, this.source)); }; - return WindowTimeOperator; + return RetryWhenOperator; }()); -var CountedSubject = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](CountedSubject, _super); - function CountedSubject() { - var _this = _super !== null && _super.apply(this, arguments) || this; - _this._numberOfNextedValues = 0; - return _this; - } - CountedSubject.prototype.next = function (value) { - this._numberOfNextedValues++; - _super.prototype.next.call(this, value); - }; - Object.defineProperty(CountedSubject.prototype, "numberOfNextedValues", { - get: function () { - return this._numberOfNextedValues; - }, - enumerable: true, - configurable: true - }); - return CountedSubject; -}(_Subject__WEBPACK_IMPORTED_MODULE_1__["Subject"])); -var WindowTimeSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](WindowTimeSubscriber, _super); - function WindowTimeSubscriber(destination, windowTimeSpan, windowCreationInterval, maxWindowSize, scheduler) { +var RetryWhenSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](RetryWhenSubscriber, _super); + function RetryWhenSubscriber(destination, notifier, source) { var _this = _super.call(this, destination) || this; - _this.destination = destination; - _this.windowTimeSpan = windowTimeSpan; - _this.windowCreationInterval = windowCreationInterval; - _this.maxWindowSize = maxWindowSize; - _this.scheduler = scheduler; - _this.windows = []; - var window = _this.openWindow(); - if (windowCreationInterval !== null && windowCreationInterval >= 0) { - var closeState = { subscriber: _this, window: window, context: null }; - var creationState = { windowTimeSpan: windowTimeSpan, windowCreationInterval: windowCreationInterval, subscriber: _this, scheduler: scheduler }; - _this.add(scheduler.schedule(dispatchWindowClose, windowTimeSpan, closeState)); - _this.add(scheduler.schedule(dispatchWindowCreation, windowCreationInterval, creationState)); - } - else { - var timeSpanOnlyState = { subscriber: _this, window: window, windowTimeSpan: windowTimeSpan }; - _this.add(scheduler.schedule(dispatchWindowTimeSpanOnly, windowTimeSpan, timeSpanOnlyState)); - } + _this.notifier = notifier; + _this.source = source; return _this; } - WindowTimeSubscriber.prototype._next = function (value) { - var windows = this.windows; - var len = windows.length; - for (var i = 0; i < len; i++) { - var window_1 = windows[i]; - if (!window_1.closed) { - window_1.next(value); - if (window_1.numberOfNextedValues >= this.maxWindowSize) { - this.closeWindow(window_1); + RetryWhenSubscriber.prototype.error = function (err) { + if (!this.isStopped) { + var errors = this.errors; + var retries = this.retries; + var retriesSubscription = this.retriesSubscription; + if (!retries) { + errors = new _Subject__WEBPACK_IMPORTED_MODULE_1__["Subject"](); + try { + var notifier = this.notifier; + retries = notifier(errors); + } + catch (e) { + return _super.prototype.error.call(this, e); } + retriesSubscription = Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__["subscribeToResult"])(this, retries); } - } - }; - WindowTimeSubscriber.prototype._error = function (err) { - var windows = this.windows; - while (windows.length > 0) { - windows.shift().error(err); - } - this.destination.error(err); - }; - WindowTimeSubscriber.prototype._complete = function () { - var windows = this.windows; - while (windows.length > 0) { - var window_2 = windows.shift(); - if (!window_2.closed) { - window_2.complete(); + else { + this.errors = null; + this.retriesSubscription = null; } + this._unsubscribeAndRecycle(); + this.errors = errors; + this.retries = retries; + this.retriesSubscription = retriesSubscription; + errors.next(err); } - this.destination.complete(); }; - WindowTimeSubscriber.prototype.openWindow = function () { - var window = new CountedSubject(); - this.windows.push(window); - var destination = this.destination; - destination.next(window); - return window; + RetryWhenSubscriber.prototype._unsubscribe = function () { + var _a = this, errors = _a.errors, retriesSubscription = _a.retriesSubscription; + if (errors) { + errors.unsubscribe(); + this.errors = null; + } + if (retriesSubscription) { + retriesSubscription.unsubscribe(); + this.retriesSubscription = null; + } + this.retries = null; }; - WindowTimeSubscriber.prototype.closeWindow = function (window) { - window.complete(); - var windows = this.windows; - windows.splice(windows.indexOf(window), 1); + RetryWhenSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { + var _unsubscribe = this._unsubscribe; + this._unsubscribe = null; + this._unsubscribeAndRecycle(); + this._unsubscribe = _unsubscribe; + this.source.subscribe(this); }; - return WindowTimeSubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_3__["Subscriber"])); -function dispatchWindowTimeSpanOnly(state) { - var subscriber = state.subscriber, windowTimeSpan = state.windowTimeSpan, window = state.window; - if (window) { - subscriber.closeWindow(window); - } - state.window = subscriber.openWindow(); - this.schedule(state, windowTimeSpan); -} -function dispatchWindowCreation(state) { - var windowTimeSpan = state.windowTimeSpan, subscriber = state.subscriber, scheduler = state.scheduler, windowCreationInterval = state.windowCreationInterval; - var window = subscriber.openWindow(); - var action = this; - var context = { action: action, subscription: null }; - var timeSpanState = { subscriber: subscriber, window: window, context: context }; - context.subscription = scheduler.schedule(dispatchWindowClose, windowTimeSpan, timeSpanState); - action.add(context.subscription); - action.schedule(state, windowCreationInterval); -} -function dispatchWindowClose(state) { - var subscriber = state.subscriber, window = state.window, context = state.context; - if (context && context.action && context.subscription) { - context.action.remove(context.subscription); - } - subscriber.closeWindow(window); -} -//# sourceMappingURL=windowTime.js.map + return RetryWhenSubscriber; +}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_2__["OuterSubscriber"])); +//# sourceMappingURL=retryWhen.js.map /***/ }), -/* 342 */ +/* 334 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "windowToggle", function() { return windowToggle; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "sample", function() { return sample; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(265); -/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(177); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(171); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(182); -/** PURE_IMPORTS_START tslib,_Subject,_Subscription,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ - - +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(229); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(230); +/** PURE_IMPORTS_START tslib,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ -function windowToggle(openings, closingSelector) { - return function (source) { return source.lift(new WindowToggleOperator(openings, closingSelector)); }; +function sample(notifier) { + return function (source) { return source.lift(new SampleOperator(notifier)); }; } -var WindowToggleOperator = /*@__PURE__*/ (function () { - function WindowToggleOperator(openings, closingSelector) { - this.openings = openings; - this.closingSelector = closingSelector; +var SampleOperator = /*@__PURE__*/ (function () { + function SampleOperator(notifier) { + this.notifier = notifier; } - WindowToggleOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new WindowToggleSubscriber(subscriber, this.openings, this.closingSelector)); + SampleOperator.prototype.call = function (subscriber, source) { + var sampleSubscriber = new SampleSubscriber(subscriber); + var subscription = source.subscribe(sampleSubscriber); + subscription.add(Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__["subscribeToResult"])(sampleSubscriber, this.notifier)); + return subscription; }; - return WindowToggleOperator; + return SampleOperator; }()); -var WindowToggleSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](WindowToggleSubscriber, _super); - function WindowToggleSubscriber(destination, openings, closingSelector) { - var _this = _super.call(this, destination) || this; - _this.openings = openings; - _this.closingSelector = closingSelector; - _this.contexts = []; - _this.add(_this.openSubscription = Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__["subscribeToResult"])(_this, openings, openings)); +var SampleSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](SampleSubscriber, _super); + function SampleSubscriber() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this.hasValue = false; return _this; } - WindowToggleSubscriber.prototype._next = function (value) { - var contexts = this.contexts; - if (contexts) { - var len = contexts.length; - for (var i = 0; i < len; i++) { - contexts[i].window.next(value); - } - } - }; - WindowToggleSubscriber.prototype._error = function (err) { - var contexts = this.contexts; - this.contexts = null; - if (contexts) { - var len = contexts.length; - var index = -1; - while (++index < len) { - var context_1 = contexts[index]; - context_1.window.error(err); - context_1.subscription.unsubscribe(); - } - } - _super.prototype._error.call(this, err); - }; - WindowToggleSubscriber.prototype._complete = function () { - var contexts = this.contexts; - this.contexts = null; - if (contexts) { - var len = contexts.length; - var index = -1; - while (++index < len) { - var context_2 = contexts[index]; - context_2.window.complete(); - context_2.subscription.unsubscribe(); - } - } - _super.prototype._complete.call(this); - }; - WindowToggleSubscriber.prototype._unsubscribe = function () { - var contexts = this.contexts; - this.contexts = null; - if (contexts) { - var len = contexts.length; - var index = -1; - while (++index < len) { - var context_3 = contexts[index]; - context_3.window.unsubscribe(); - context_3.subscription.unsubscribe(); - } - } - }; - WindowToggleSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { - if (outerValue === this.openings) { - var closingNotifier = void 0; - try { - var closingSelector = this.closingSelector; - closingNotifier = closingSelector(innerValue); - } - catch (e) { - return this.error(e); - } - var window_1 = new _Subject__WEBPACK_IMPORTED_MODULE_1__["Subject"](); - var subscription = new _Subscription__WEBPACK_IMPORTED_MODULE_2__["Subscription"](); - var context_4 = { window: window_1, subscription: subscription }; - this.contexts.push(context_4); - var innerSubscription = Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__["subscribeToResult"])(this, closingNotifier, context_4); - if (innerSubscription.closed) { - this.closeWindow(this.contexts.length - 1); - } - else { - innerSubscription.context = context_4; - subscription.add(innerSubscription); - } - this.destination.next(window_1); - } - else { - this.closeWindow(this.contexts.indexOf(outerValue)); - } + SampleSubscriber.prototype._next = function (value) { + this.value = value; + this.hasValue = true; }; - WindowToggleSubscriber.prototype.notifyError = function (err) { - this.error(err); + SampleSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { + this.emitValue(); }; - WindowToggleSubscriber.prototype.notifyComplete = function (inner) { - if (inner !== this.openSubscription) { - this.closeWindow(this.contexts.indexOf(inner.context)); - } + SampleSubscriber.prototype.notifyComplete = function () { + this.emitValue(); }; - WindowToggleSubscriber.prototype.closeWindow = function (index) { - if (index === -1) { - return; + SampleSubscriber.prototype.emitValue = function () { + if (this.hasValue) { + this.hasValue = false; + this.destination.next(this.value); } - var contexts = this.contexts; - var context = contexts[index]; - var window = context.window, subscription = context.subscription; - contexts.splice(index, 1); - window.complete(); - subscription.unsubscribe(); }; - return WindowToggleSubscriber; -}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__["OuterSubscriber"])); -//# sourceMappingURL=windowToggle.js.map + return SampleSubscriber; +}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__["OuterSubscriber"])); +//# sourceMappingURL=sample.js.map /***/ }), -/* 343 */ +/* 335 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "windowWhen", function() { return windowWhen; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "sampleTime", function() { return sampleTime; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(265); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(171); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(182); -/** PURE_IMPORTS_START tslib,_Subject,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ - +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); +/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(215); +/** PURE_IMPORTS_START tslib,_Subscriber,_scheduler_async PURE_IMPORTS_END */ -function windowWhen(closingSelector) { - return function windowWhenOperatorFunction(source) { - return source.lift(new WindowOperator(closingSelector)); - }; +function sampleTime(period, scheduler) { + if (scheduler === void 0) { + scheduler = _scheduler_async__WEBPACK_IMPORTED_MODULE_2__["async"]; + } + return function (source) { return source.lift(new SampleTimeOperator(period, scheduler)); }; } -var WindowOperator = /*@__PURE__*/ (function () { - function WindowOperator(closingSelector) { - this.closingSelector = closingSelector; +var SampleTimeOperator = /*@__PURE__*/ (function () { + function SampleTimeOperator(period, scheduler) { + this.period = period; + this.scheduler = scheduler; } - WindowOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new WindowSubscriber(subscriber, this.closingSelector)); + SampleTimeOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new SampleTimeSubscriber(subscriber, this.period, this.scheduler)); }; - return WindowOperator; + return SampleTimeOperator; }()); -var WindowSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](WindowSubscriber, _super); - function WindowSubscriber(destination, closingSelector) { +var SampleTimeSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](SampleTimeSubscriber, _super); + function SampleTimeSubscriber(destination, period, scheduler) { var _this = _super.call(this, destination) || this; - _this.destination = destination; - _this.closingSelector = closingSelector; - _this.openWindow(); + _this.period = period; + _this.scheduler = scheduler; + _this.hasValue = false; + _this.add(scheduler.schedule(dispatchNotification, period, { subscriber: _this, period: period })); return _this; } - WindowSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { - this.openWindow(innerSub); - }; - WindowSubscriber.prototype.notifyError = function (error, innerSub) { - this._error(error); - }; - WindowSubscriber.prototype.notifyComplete = function (innerSub) { - this.openWindow(innerSub); - }; - WindowSubscriber.prototype._next = function (value) { - this.window.next(value); - }; - WindowSubscriber.prototype._error = function (err) { - this.window.error(err); - this.destination.error(err); - this.unsubscribeClosingNotification(); - }; - WindowSubscriber.prototype._complete = function () { - this.window.complete(); - this.destination.complete(); - this.unsubscribeClosingNotification(); - }; - WindowSubscriber.prototype.unsubscribeClosingNotification = function () { - if (this.closingNotification) { - this.closingNotification.unsubscribe(); - } + SampleTimeSubscriber.prototype._next = function (value) { + this.lastValue = value; + this.hasValue = true; }; - WindowSubscriber.prototype.openWindow = function (innerSub) { - if (innerSub === void 0) { - innerSub = null; - } - if (innerSub) { - this.remove(innerSub); - innerSub.unsubscribe(); - } - var prevWindow = this.window; - if (prevWindow) { - prevWindow.complete(); - } - var window = this.window = new _Subject__WEBPACK_IMPORTED_MODULE_1__["Subject"](); - this.destination.next(window); - var closingNotifier; - try { - var closingSelector = this.closingSelector; - closingNotifier = closingSelector(); - } - catch (e) { - this.destination.error(e); - this.window.error(e); - return; + SampleTimeSubscriber.prototype.notifyNext = function () { + if (this.hasValue) { + this.hasValue = false; + this.destination.next(this.lastValue); } - this.add(this.closingNotification = Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__["subscribeToResult"])(this, closingNotifier)); }; - return WindowSubscriber; -}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_2__["OuterSubscriber"])); -//# sourceMappingURL=windowWhen.js.map + return SampleTimeSubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); +function dispatchNotification(state) { + var subscriber = state.subscriber, period = state.period; + subscriber.notifyNext(); + this.schedule(state, period); +} +//# sourceMappingURL=sampleTime.js.map /***/ }), -/* 344 */ +/* 336 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "withLatestFrom", function() { return withLatestFrom; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "sequenceEqual", function() { return sequenceEqual; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SequenceEqualOperator", function() { return SequenceEqualOperator; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SequenceEqualSubscriber", function() { return SequenceEqualSubscriber; }); /* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(171); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(182); -/** PURE_IMPORTS_START tslib,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ - +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); +/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ -function withLatestFrom() { - var args = []; - for (var _i = 0; _i < arguments.length; _i++) { - args[_i] = arguments[_i]; - } - return function (source) { - var project; - if (typeof args[args.length - 1] === 'function') { - project = args.pop(); - } - var observables = args; - return source.lift(new WithLatestFromOperator(observables, project)); - }; +function sequenceEqual(compareTo, comparator) { + return function (source) { return source.lift(new SequenceEqualOperator(compareTo, comparator)); }; } -var WithLatestFromOperator = /*@__PURE__*/ (function () { - function WithLatestFromOperator(observables, project) { - this.observables = observables; - this.project = project; +var SequenceEqualOperator = /*@__PURE__*/ (function () { + function SequenceEqualOperator(compareTo, comparator) { + this.compareTo = compareTo; + this.comparator = comparator; } - WithLatestFromOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new WithLatestFromSubscriber(subscriber, this.observables, this.project)); + SequenceEqualOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new SequenceEqualSubscriber(subscriber, this.compareTo, this.comparator)); }; - return WithLatestFromOperator; + return SequenceEqualOperator; }()); -var WithLatestFromSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](WithLatestFromSubscriber, _super); - function WithLatestFromSubscriber(destination, observables, project) { + +var SequenceEqualSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](SequenceEqualSubscriber, _super); + function SequenceEqualSubscriber(destination, compareTo, comparator) { var _this = _super.call(this, destination) || this; - _this.observables = observables; - _this.project = project; - _this.toRespond = []; - var len = observables.length; - _this.values = new Array(len); - for (var i = 0; i < len; i++) { - _this.toRespond.push(i); - } - for (var i = 0; i < len; i++) { - var observable = observables[i]; - _this.add(Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__["subscribeToResult"])(_this, observable, observable, i)); - } + _this.compareTo = compareTo; + _this.comparator = comparator; + _this._a = []; + _this._b = []; + _this._oneComplete = false; + _this.destination.add(compareTo.subscribe(new SequenceEqualCompareToSubscriber(destination, _this))); return _this; } - WithLatestFromSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { - this.values[outerIndex] = innerValue; - var toRespond = this.toRespond; - if (toRespond.length > 0) { - var found = toRespond.indexOf(outerIndex); - if (found !== -1) { - toRespond.splice(found, 1); - } + SequenceEqualSubscriber.prototype._next = function (value) { + if (this._oneComplete && this._b.length === 0) { + this.emit(false); + } + else { + this._a.push(value); + this.checkValues(); } }; - WithLatestFromSubscriber.prototype.notifyComplete = function () { + SequenceEqualSubscriber.prototype._complete = function () { + if (this._oneComplete) { + this.emit(this._a.length === 0 && this._b.length === 0); + } + else { + this._oneComplete = true; + } + this.unsubscribe(); }; - WithLatestFromSubscriber.prototype._next = function (value) { - if (this.toRespond.length === 0) { - var args = [value].concat(this.values); - if (this.project) { - this._tryProject(args); + SequenceEqualSubscriber.prototype.checkValues = function () { + var _c = this, _a = _c._a, _b = _c._b, comparator = _c.comparator; + while (_a.length > 0 && _b.length > 0) { + var a = _a.shift(); + var b = _b.shift(); + var areEqual = false; + try { + areEqual = comparator ? comparator(a, b) : a === b; } - else { - this.destination.next(args); + catch (e) { + this.destination.error(e); + } + if (!areEqual) { + this.emit(false); } } }; - WithLatestFromSubscriber.prototype._tryProject = function (args) { - var result; - try { - result = this.project.apply(this, args); + SequenceEqualSubscriber.prototype.emit = function (value) { + var destination = this.destination; + destination.next(value); + destination.complete(); + }; + SequenceEqualSubscriber.prototype.nextB = function (value) { + if (this._oneComplete && this._a.length === 0) { + this.emit(false); } - catch (err) { - this.destination.error(err); - return; + else { + this._b.push(value); + this.checkValues(); } - this.destination.next(result); }; - return WithLatestFromSubscriber; -}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__["OuterSubscriber"])); -//# sourceMappingURL=withLatestFrom.js.map + SequenceEqualSubscriber.prototype.completeB = function () { + if (this._oneComplete) { + this.emit(this._a.length === 0 && this._b.length === 0); + } + else { + this._oneComplete = true; + } + }; + return SequenceEqualSubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); + +var SequenceEqualCompareToSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](SequenceEqualCompareToSubscriber, _super); + function SequenceEqualCompareToSubscriber(destination, parent) { + var _this = _super.call(this, destination) || this; + _this.parent = parent; + return _this; + } + SequenceEqualCompareToSubscriber.prototype._next = function (value) { + this.parent.nextB(value); + }; + SequenceEqualCompareToSubscriber.prototype._error = function (err) { + this.parent.error(err); + this.unsubscribe(); + }; + SequenceEqualCompareToSubscriber.prototype._complete = function () { + this.parent.completeB(); + this.unsubscribe(); + }; + return SequenceEqualCompareToSubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); +//# sourceMappingURL=sequenceEqual.js.map /***/ }), -/* 345 */ +/* 337 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "zip", function() { return zip; }); -/* harmony import */ var _observable_zip__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(346); -/** PURE_IMPORTS_START _observable_zip PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "share", function() { return share; }); +/* harmony import */ var _multicast__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(320); +/* harmony import */ var _refCount__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(190); +/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(187); +/** PURE_IMPORTS_START _multicast,_refCount,_Subject PURE_IMPORTS_END */ -function zip() { - var observables = []; - for (var _i = 0; _i < arguments.length; _i++) { - observables[_i] = arguments[_i]; - } - return function zipOperatorFunction(source) { - return source.lift.call(_observable_zip__WEBPACK_IMPORTED_MODULE_0__["zip"].apply(void 0, [source].concat(observables))); - }; + + +function shareSubjectFactory() { + return new _Subject__WEBPACK_IMPORTED_MODULE_2__["Subject"](); } -//# sourceMappingURL=zip.js.map +function share() { + return function (source) { return Object(_refCount__WEBPACK_IMPORTED_MODULE_1__["refCount"])()(Object(_multicast__WEBPACK_IMPORTED_MODULE_0__["multicast"])(shareSubjectFactory)(source)); }; +} +//# sourceMappingURL=share.js.map /***/ }), -/* 346 */ +/* 338 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "zip", function() { return zip; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ZipOperator", function() { return ZipOperator; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ZipSubscriber", function() { return ZipSubscriber; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _fromArray__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(215); -/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(178); -/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(172); -/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(171); -/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(182); -/* harmony import */ var _internal_symbol_iterator__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(188); -/** PURE_IMPORTS_START tslib,_fromArray,_util_isArray,_Subscriber,_OuterSubscriber,_util_subscribeToResult,_.._internal_symbol_iterator PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "shareReplay", function() { return shareReplay; }); +/* harmony import */ var _ReplaySubject__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(193); +/** PURE_IMPORTS_START _ReplaySubject PURE_IMPORTS_END */ +function shareReplay(configOrBufferSize, windowTime, scheduler) { + var config; + if (configOrBufferSize && typeof configOrBufferSize === 'object') { + config = configOrBufferSize; + } + else { + config = { + bufferSize: configOrBufferSize, + windowTime: windowTime, + refCount: false, + scheduler: scheduler + }; + } + return function (source) { return source.lift(shareReplayOperator(config)); }; +} +function shareReplayOperator(_a) { + var _b = _a.bufferSize, bufferSize = _b === void 0 ? Number.POSITIVE_INFINITY : _b, _c = _a.windowTime, windowTime = _c === void 0 ? Number.POSITIVE_INFINITY : _c, useRefCount = _a.refCount, scheduler = _a.scheduler; + var subject; + var refCount = 0; + var subscription; + var hasError = false; + var isComplete = false; + return function shareReplayOperation(source) { + refCount++; + if (!subject || hasError) { + hasError = false; + subject = new _ReplaySubject__WEBPACK_IMPORTED_MODULE_0__["ReplaySubject"](bufferSize, windowTime, scheduler); + subscription = source.subscribe({ + next: function (value) { subject.next(value); }, + error: function (err) { + hasError = true; + subject.error(err); + }, + complete: function () { + isComplete = true; + subject.complete(); + }, + }); + } + var innerSub = subject.subscribe(this); + this.add(function () { + refCount--; + innerSub.unsubscribe(); + if (subscription && !isComplete && useRefCount && refCount === 0) { + subscription.unsubscribe(); + subscription = undefined; + subject = undefined; + } + }); + }; +} +//# sourceMappingURL=shareReplay.js.map +/***/ }), +/* 339 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "single", function() { return single; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); +/* harmony import */ var _util_EmptyError__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(223); +/** PURE_IMPORTS_START tslib,_Subscriber,_util_EmptyError PURE_IMPORTS_END */ -function zip() { - var observables = []; - for (var _i = 0; _i < arguments.length; _i++) { - observables[_i] = arguments[_i]; - } - var resultSelector = observables[observables.length - 1]; - if (typeof resultSelector === 'function') { - observables.pop(); - } - return Object(_fromArray__WEBPACK_IMPORTED_MODULE_1__["fromArray"])(observables, undefined).lift(new ZipOperator(resultSelector)); +function single(predicate) { + return function (source) { return source.lift(new SingleOperator(predicate, source)); }; } -var ZipOperator = /*@__PURE__*/ (function () { - function ZipOperator(resultSelector) { - this.resultSelector = resultSelector; +var SingleOperator = /*@__PURE__*/ (function () { + function SingleOperator(predicate, source) { + this.predicate = predicate; + this.source = source; } - ZipOperator.prototype.call = function (subscriber, source) { - return source.subscribe(new ZipSubscriber(subscriber, this.resultSelector)); + SingleOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new SingleSubscriber(subscriber, this.predicate, this.source)); }; - return ZipOperator; + return SingleOperator; }()); - -var ZipSubscriber = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](ZipSubscriber, _super); - function ZipSubscriber(destination, resultSelector, values) { - if (values === void 0) { - values = Object.create(null); - } +var SingleSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](SingleSubscriber, _super); + function SingleSubscriber(destination, predicate, source) { var _this = _super.call(this, destination) || this; - _this.iterators = []; - _this.active = 0; - _this.resultSelector = (typeof resultSelector === 'function') ? resultSelector : null; - _this.values = values; + _this.predicate = predicate; + _this.source = source; + _this.seenValue = false; + _this.index = 0; return _this; } - ZipSubscriber.prototype._next = function (value) { - var iterators = this.iterators; - if (Object(_util_isArray__WEBPACK_IMPORTED_MODULE_2__["isArray"])(value)) { - iterators.push(new StaticArrayIterator(value)); - } - else if (typeof value[_internal_symbol_iterator__WEBPACK_IMPORTED_MODULE_6__["iterator"]] === 'function') { - iterators.push(new StaticIterator(value[_internal_symbol_iterator__WEBPACK_IMPORTED_MODULE_6__["iterator"]]())); + SingleSubscriber.prototype.applySingleValue = function (value) { + if (this.seenValue) { + this.destination.error('Sequence contains more than one element'); } else { - iterators.push(new ZipBufferIterator(this.destination, this, value)); + this.seenValue = true; + this.singleValue = value; } }; - ZipSubscriber.prototype._complete = function () { - var iterators = this.iterators; - var len = iterators.length; - this.unsubscribe(); - if (len === 0) { - this.destination.complete(); - return; + SingleSubscriber.prototype._next = function (value) { + var index = this.index++; + if (this.predicate) { + this.tryNext(value, index); } - this.active = len; - for (var i = 0; i < len; i++) { - var iterator = iterators[i]; - if (iterator.stillUnsubscribed) { - var destination = this.destination; - destination.add(iterator.subscribe(iterator, i)); - } - else { - this.active--; - } + else { + this.applySingleValue(value); } }; - ZipSubscriber.prototype.notifyInactive = function () { - this.active--; - if (this.active === 0) { - this.destination.complete(); + SingleSubscriber.prototype.tryNext = function (value, index) { + try { + if (this.predicate(value, index, this.source)) { + this.applySingleValue(value); + } + } + catch (err) { + this.destination.error(err); } }; - ZipSubscriber.prototype.checkIterators = function () { - var iterators = this.iterators; - var len = iterators.length; + SingleSubscriber.prototype._complete = function () { var destination = this.destination; - for (var i = 0; i < len; i++) { - var iterator = iterators[i]; - if (typeof iterator.hasValue === 'function' && !iterator.hasValue()) { - return; - } + if (this.index > 0) { + destination.next(this.seenValue ? this.singleValue : undefined); + destination.complete(); } - var shouldComplete = false; - var args = []; - for (var i = 0; i < len; i++) { - var iterator = iterators[i]; - var result = iterator.next(); - if (iterator.hasCompleted()) { - shouldComplete = true; - } - if (result.done) { - destination.complete(); - return; - } - args.push(result.value); + else { + destination.error(new _util_EmptyError__WEBPACK_IMPORTED_MODULE_2__["EmptyError"]); } - if (this.resultSelector) { - this._tryresultSelector(args); + }; + return SingleSubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); +//# sourceMappingURL=single.js.map + + +/***/ }), +/* 340 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "skip", function() { return skip; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); +/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ + + +function skip(count) { + return function (source) { return source.lift(new SkipOperator(count)); }; +} +var SkipOperator = /*@__PURE__*/ (function () { + function SkipOperator(total) { + this.total = total; + } + SkipOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new SkipSubscriber(subscriber, this.total)); + }; + return SkipOperator; +}()); +var SkipSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](SkipSubscriber, _super); + function SkipSubscriber(destination, total) { + var _this = _super.call(this, destination) || this; + _this.total = total; + _this.count = 0; + return _this; + } + SkipSubscriber.prototype._next = function (x) { + if (++this.count > this.total) { + this.destination.next(x); } - else { - destination.next(args); + }; + return SkipSubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); +//# sourceMappingURL=skip.js.map + + +/***/ }), +/* 341 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "skipLast", function() { return skipLast; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); +/* harmony import */ var _util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(222); +/** PURE_IMPORTS_START tslib,_Subscriber,_util_ArgumentOutOfRangeError PURE_IMPORTS_END */ + + + +function skipLast(count) { + return function (source) { return source.lift(new SkipLastOperator(count)); }; +} +var SkipLastOperator = /*@__PURE__*/ (function () { + function SkipLastOperator(_skipCount) { + this._skipCount = _skipCount; + if (this._skipCount < 0) { + throw new _util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_2__["ArgumentOutOfRangeError"]; } - if (shouldComplete) { - destination.complete(); + } + SkipLastOperator.prototype.call = function (subscriber, source) { + if (this._skipCount === 0) { + return source.subscribe(new _Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"](subscriber)); + } + else { + return source.subscribe(new SkipLastSubscriber(subscriber, this._skipCount)); } }; - ZipSubscriber.prototype._tryresultSelector = function (args) { - var result; - try { - result = this.resultSelector.apply(this, args); + return SkipLastOperator; +}()); +var SkipLastSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](SkipLastSubscriber, _super); + function SkipLastSubscriber(destination, _skipCount) { + var _this = _super.call(this, destination) || this; + _this._skipCount = _skipCount; + _this._count = 0; + _this._ring = new Array(_skipCount); + return _this; + } + SkipLastSubscriber.prototype._next = function (value) { + var skipCount = this._skipCount; + var count = this._count++; + if (count < skipCount) { + this._ring[count] = value; } - catch (err) { - this.destination.error(err); - return; + else { + var currentIndex = count % skipCount; + var ring = this._ring; + var oldValue = ring[currentIndex]; + ring[currentIndex] = value; + this.destination.next(oldValue); } - this.destination.next(result); }; - return ZipSubscriber; -}(_Subscriber__WEBPACK_IMPORTED_MODULE_3__["Subscriber"])); + return SkipLastSubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); +//# sourceMappingURL=skipLast.js.map -var StaticIterator = /*@__PURE__*/ (function () { - function StaticIterator(iterator) { - this.iterator = iterator; - this.nextResult = iterator.next(); + +/***/ }), +/* 342 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "skipUntil", function() { return skipUntil; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(229); +/* harmony import */ var _InnerSubscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(231); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(230); +/** PURE_IMPORTS_START tslib,_OuterSubscriber,_InnerSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ + + + + +function skipUntil(notifier) { + return function (source) { return source.lift(new SkipUntilOperator(notifier)); }; +} +var SkipUntilOperator = /*@__PURE__*/ (function () { + function SkipUntilOperator(notifier) { + this.notifier = notifier; } - StaticIterator.prototype.hasValue = function () { - return true; - }; - StaticIterator.prototype.next = function () { - var result = this.nextResult; - this.nextResult = this.iterator.next(); - return result; - }; - StaticIterator.prototype.hasCompleted = function () { - var nextResult = this.nextResult; - return nextResult && nextResult.done; + SkipUntilOperator.prototype.call = function (destination, source) { + return source.subscribe(new SkipUntilSubscriber(destination, this.notifier)); }; - return StaticIterator; + return SkipUntilOperator; }()); -var StaticArrayIterator = /*@__PURE__*/ (function () { - function StaticArrayIterator(array) { - this.array = array; - this.index = 0; - this.length = 0; - this.length = array.length; +var SkipUntilSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](SkipUntilSubscriber, _super); + function SkipUntilSubscriber(destination, notifier) { + var _this = _super.call(this, destination) || this; + _this.hasValue = false; + var innerSubscriber = new _InnerSubscriber__WEBPACK_IMPORTED_MODULE_2__["InnerSubscriber"](_this, undefined, undefined); + _this.add(innerSubscriber); + _this.innerSubscription = innerSubscriber; + Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__["subscribeToResult"])(_this, notifier, undefined, undefined, innerSubscriber); + return _this; } - StaticArrayIterator.prototype[_internal_symbol_iterator__WEBPACK_IMPORTED_MODULE_6__["iterator"]] = function () { - return this; + SkipUntilSubscriber.prototype._next = function (value) { + if (this.hasValue) { + _super.prototype._next.call(this, value); + } }; - StaticArrayIterator.prototype.next = function (value) { - var i = this.index++; - var array = this.array; - return i < this.length ? { value: array[i], done: false } : { value: null, done: true }; + SkipUntilSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { + this.hasValue = true; + if (this.innerSubscription) { + this.innerSubscription.unsubscribe(); + } }; - StaticArrayIterator.prototype.hasValue = function () { - return this.array.length > this.index; + SkipUntilSubscriber.prototype.notifyComplete = function () { }; - StaticArrayIterator.prototype.hasCompleted = function () { - return this.array.length === this.index; + return SkipUntilSubscriber; +}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__["OuterSubscriber"])); +//# sourceMappingURL=skipUntil.js.map + + +/***/ }), +/* 343 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { + +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "skipWhile", function() { return skipWhile; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); +/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ + + +function skipWhile(predicate) { + return function (source) { return source.lift(new SkipWhileOperator(predicate)); }; +} +var SkipWhileOperator = /*@__PURE__*/ (function () { + function SkipWhileOperator(predicate) { + this.predicate = predicate; + } + SkipWhileOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new SkipWhileSubscriber(subscriber, this.predicate)); }; - return StaticArrayIterator; + return SkipWhileOperator; }()); -var ZipBufferIterator = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](ZipBufferIterator, _super); - function ZipBufferIterator(destination, parent, observable) { +var SkipWhileSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](SkipWhileSubscriber, _super); + function SkipWhileSubscriber(destination, predicate) { var _this = _super.call(this, destination) || this; - _this.parent = parent; - _this.observable = observable; - _this.stillUnsubscribed = true; - _this.buffer = []; - _this.isComplete = false; + _this.predicate = predicate; + _this.skipping = true; + _this.index = 0; return _this; } - ZipBufferIterator.prototype[_internal_symbol_iterator__WEBPACK_IMPORTED_MODULE_6__["iterator"]] = function () { - return this; - }; - ZipBufferIterator.prototype.next = function () { - var buffer = this.buffer; - if (buffer.length === 0 && this.isComplete) { - return { value: null, done: true }; + SkipWhileSubscriber.prototype._next = function (value) { + var destination = this.destination; + if (this.skipping) { + this.tryCallPredicate(value); } - else { - return { value: buffer.shift(), done: false }; + if (!this.skipping) { + destination.next(value); } }; - ZipBufferIterator.prototype.hasValue = function () { - return this.buffer.length > 0; - }; - ZipBufferIterator.prototype.hasCompleted = function () { - return this.buffer.length === 0 && this.isComplete; - }; - ZipBufferIterator.prototype.notifyComplete = function () { - if (this.buffer.length > 0) { - this.isComplete = true; - this.parent.notifyInactive(); + SkipWhileSubscriber.prototype.tryCallPredicate = function (value) { + try { + var result = this.predicate(value, this.index++); + this.skipping = Boolean(result); } - else { - this.destination.complete(); + catch (err) { + this.destination.error(err); } }; - ZipBufferIterator.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { - this.buffer.push(innerValue); - this.parent.checkIterators(); - }; - ZipBufferIterator.prototype.subscribe = function (value, index) { - return Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_5__["subscribeToResult"])(this, this.observable, this, index); - }; - return ZipBufferIterator; -}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_4__["OuterSubscriber"])); -//# sourceMappingURL=zip.js.map + return SkipWhileSubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); +//# sourceMappingURL=skipWhile.js.map /***/ }), -/* 347 */ +/* 344 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "zipAll", function() { return zipAll; }); -/* harmony import */ var _observable_zip__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(346); -/** PURE_IMPORTS_START _observable_zip PURE_IMPORTS_END */ +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "startWith", function() { return startWith; }); +/* harmony import */ var _observable_concat__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(239); +/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(205); +/** PURE_IMPORTS_START _observable_concat,_util_isScheduler PURE_IMPORTS_END */ -function zipAll(project) { - return function (source) { return source.lift(new _observable_zip__WEBPACK_IMPORTED_MODULE_0__["ZipOperator"](project)); }; + +function startWith() { + var array = []; + for (var _i = 0; _i < arguments.length; _i++) { + array[_i] = arguments[_i]; + } + var scheduler = array[array.length - 1]; + if (Object(_util_isScheduler__WEBPACK_IMPORTED_MODULE_1__["isScheduler"])(scheduler)) { + array.pop(); + return function (source) { return Object(_observable_concat__WEBPACK_IMPORTED_MODULE_0__["concat"])(array, source, scheduler); }; + } + else { + return function (source) { return Object(_observable_concat__WEBPACK_IMPORTED_MODULE_0__["concat"])(array, source); }; + } } -//# sourceMappingURL=zipAll.js.map +//# sourceMappingURL=startWith.js.map /***/ }), -/* 348 */ -/***/ (function(module, exports, __webpack_require__) { +/* 345 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "subscribeOn", function() { return subscribeOn; }); +/* harmony import */ var _observable_SubscribeOnObservable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(346); +/** PURE_IMPORTS_START _observable_SubscribeOnObservable PURE_IMPORTS_END */ +function subscribeOn(scheduler, delay) { + if (delay === void 0) { + delay = 0; + } + return function subscribeOnOperatorFunction(source) { + return source.lift(new SubscribeOnOperator(scheduler, delay)); + }; +} +var SubscribeOnOperator = /*@__PURE__*/ (function () { + function SubscribeOnOperator(scheduler, delay) { + this.scheduler = scheduler; + this.delay = delay; + } + SubscribeOnOperator.prototype.call = function (subscriber, source) { + return new _observable_SubscribeOnObservable__WEBPACK_IMPORTED_MODULE_0__["SubscribeOnObservable"](source, this.delay, this.scheduler).subscribe(subscriber); + }; + return SubscribeOnOperator; +}()); +//# sourceMappingURL=subscribeOn.js.map -const callbacks = new Set(); -let called = false; - -function exit(exit, signal) { - if (called) { - return; - } - called = true; +/***/ }), +/* 346 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { - for (const callback of callbacks) { - callback(); - } +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SubscribeOnObservable", function() { return SubscribeOnObservable; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(170); +/* harmony import */ var _scheduler_asap__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(211); +/* harmony import */ var _util_isNumeric__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(257); +/** PURE_IMPORTS_START tslib,_Observable,_scheduler_asap,_util_isNumeric PURE_IMPORTS_END */ - if (exit === true) { - process.exit(128 + signal); // eslint-disable-line unicorn/no-process-exit - } -} -module.exports = callback => { - callbacks.add(callback); - if (callbacks.size === 1) { - process.once('exit', exit); - process.once('SIGINT', exit.bind(null, true, 2)); - process.once('SIGTERM', exit.bind(null, true, 15)); - // PM2 Cluster shutdown message. Caught to support async handlers with pm2, needed because - // explicitly calling process.exit() doesn't trigger the beforeExit event, and the exit - // event cannot support async handlers, since the event loop is never called after it. - process.on('message', message => { - if (message === 'shutdown') { - exit(true, -128); - } - }); - } +var SubscribeOnObservable = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](SubscribeOnObservable, _super); + function SubscribeOnObservable(source, delayTime, scheduler) { + if (delayTime === void 0) { + delayTime = 0; + } + if (scheduler === void 0) { + scheduler = _scheduler_asap__WEBPACK_IMPORTED_MODULE_2__["asap"]; + } + var _this = _super.call(this) || this; + _this.source = source; + _this.delayTime = delayTime; + _this.scheduler = scheduler; + if (!Object(_util_isNumeric__WEBPACK_IMPORTED_MODULE_3__["isNumeric"])(delayTime) || delayTime < 0) { + _this.delayTime = 0; + } + if (!scheduler || typeof scheduler.schedule !== 'function') { + _this.scheduler = _scheduler_asap__WEBPACK_IMPORTED_MODULE_2__["asap"]; + } + return _this; + } + SubscribeOnObservable.create = function (source, delay, scheduler) { + if (delay === void 0) { + delay = 0; + } + if (scheduler === void 0) { + scheduler = _scheduler_asap__WEBPACK_IMPORTED_MODULE_2__["asap"]; + } + return new SubscribeOnObservable(source, delay, scheduler); + }; + SubscribeOnObservable.dispatch = function (arg) { + var source = arg.source, subscriber = arg.subscriber; + return this.add(source.subscribe(subscriber)); + }; + SubscribeOnObservable.prototype._subscribe = function (subscriber) { + var delay = this.delayTime; + var source = this.source; + var scheduler = this.scheduler; + return scheduler.schedule(SubscribeOnObservable.dispatch, delay, { + source: source, subscriber: subscriber + }); + }; + return SubscribeOnObservable; +}(_Observable__WEBPACK_IMPORTED_MODULE_1__["Observable"])); - return () => { - callbacks.delete(callback); - }; -}; +//# sourceMappingURL=SubscribeOnObservable.js.map /***/ }), -/* 349 */ -/***/ (function(module, exports, __webpack_require__) { +/* 347 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "switchAll", function() { return switchAll; }); +/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(348); +/* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(220); +/** PURE_IMPORTS_START _switchMap,_util_identity PURE_IMPORTS_END */ -/* - * 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. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -const $isCliError = Symbol('isCliError'); -function createCliError(message) { - const error = new Error(message); - error[$isCliError] = true; - return error; -} -exports.createCliError = createCliError; -function isCliError(error) { - return error && !!error[$isCliError]; + +function switchAll() { + return Object(_switchMap__WEBPACK_IMPORTED_MODULE_0__["switchMap"])(_util_identity__WEBPACK_IMPORTED_MODULE_1__["identity"]); } -exports.isCliError = isCliError; +//# sourceMappingURL=switchAll.js.map /***/ }), -/* 350 */ -/***/ (function(module, exports, __webpack_require__) { +/* 348 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "switchMap", function() { return switchMap; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(229); +/* harmony import */ var _InnerSubscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(231); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(230); +/* harmony import */ var _map__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(226); +/* harmony import */ var _observable_from__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(243); +/** PURE_IMPORTS_START tslib,_OuterSubscriber,_InnerSubscriber,_util_subscribeToResult,_map,_observable_from PURE_IMPORTS_END */ -/* - * 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. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -const tslib_1 = __webpack_require__(36); -const execa_1 = tslib_1.__importDefault(__webpack_require__(351)); -const fs_1 = __webpack_require__(23); -const Rx = tslib_1.__importStar(__webpack_require__(391)); -const operators_1 = __webpack_require__(169); -const chalk_1 = tslib_1.__importDefault(__webpack_require__(2)); -const tree_kill_1 = tslib_1.__importDefault(__webpack_require__(411)); -const util_1 = __webpack_require__(29); -const treeKillAsync = util_1.promisify((...args) => tree_kill_1.default(...args)); -const observe_lines_1 = __webpack_require__(412); -const errors_1 = __webpack_require__(349); -const SECOND = 1000; -const STOP_TIMEOUT = 30 * SECOND; -async function withTimeout(attempt, ms, onTimeout) { - const TIMEOUT = Symbol('timeout'); - try { - await Promise.race([ - attempt(), - new Promise((_, reject) => setTimeout(() => reject(TIMEOUT), ms)), - ]); - } - catch (error) { - if (error === TIMEOUT) { - await onTimeout(); - } - else { - throw error; - } + + + + + +function switchMap(project, resultSelector) { + if (typeof resultSelector === 'function') { + return function (source) { return source.pipe(switchMap(function (a, i) { return Object(_observable_from__WEBPACK_IMPORTED_MODULE_5__["from"])(project(a, i)).pipe(Object(_map__WEBPACK_IMPORTED_MODULE_4__["map"])(function (b, ii) { return resultSelector(a, b, i, ii); })); })); }; } + return function (source) { return source.lift(new SwitchMapOperator(project)); }; } -function startProc(name, options, log) { - const { cmd, args, cwd, env, stdin } = options; - log.info('[%s] > %s', name, cmd, args.join(' ')); - // spawn fails with ENOENT when either the - // cmd or cwd don't exist, so we check for the cwd - // ahead of time so that the error is less ambiguous - try { - if (!fs_1.statSync(cwd).isDirectory()) { - throw new Error(`cwd "${cwd}" exists but is not a directory`); - } - } - catch (err) { - if (err.code === 'ENOENT') { - throw new Error(`cwd "${cwd}" does not exist`); - } - } - const childProcess = execa_1.default(cmd, args, { - cwd, - env, - stdio: ['pipe', 'pipe', 'pipe'], - preferLocal: true, - }); - if (stdin) { - childProcess.stdin.end(stdin, 'utf8'); +var SwitchMapOperator = /*@__PURE__*/ (function () { + function SwitchMapOperator(project) { + this.project = project; } - else { - childProcess.stdin.end(); + SwitchMapOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new SwitchMapSubscriber(subscriber, this.project)); + }; + return SwitchMapOperator; +}()); +var SwitchMapSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](SwitchMapSubscriber, _super); + function SwitchMapSubscriber(destination, project) { + var _this = _super.call(this, destination) || this; + _this.project = project; + _this.index = 0; + return _this; } - let stopCalled = false; - const outcome$ = Rx.race( - // observe first exit event - Rx.fromEvent(childProcess, 'exit').pipe(operators_1.take(1), operators_1.map(([code]) => { - if (stopCalled) { - return null; - } - // JVM exits with 143 on SIGTERM and 130 on SIGINT, dont' treat then as errors - if (code > 0 && !(code === 143 || code === 130)) { - throw errors_1.createCliError(`[${name}] exited with code ${code}`); + SwitchMapSubscriber.prototype._next = function (value) { + var result; + var index = this.index++; + try { + result = this.project(value, index); } - return code; - })), - // observe first error event - Rx.fromEvent(childProcess, 'error').pipe(operators_1.take(1), operators_1.mergeMap(err => Rx.throwError(err)))).pipe(operators_1.share()); - const lines$ = Rx.merge(observe_lines_1.observeLines(childProcess.stdout), observe_lines_1.observeLines(childProcess.stderr)).pipe(operators_1.tap(line => log.write(` ${chalk_1.default.gray('proc')} [${chalk_1.default.gray(name)}] ${line}`)), operators_1.share()); - const outcomePromise = Rx.merge(lines$.pipe(operators_1.ignoreElements()), outcome$).toPromise(); - async function stop(signal) { - if (stopCalled) { + catch (error) { + this.destination.error(error); return; } - stopCalled = true; - await withTimeout(async () => { - log.debug(`Sending "${signal}" to proc "${name}"`); - await treeKillAsync(childProcess.pid, signal); - await outcomePromise; - }, STOP_TIMEOUT, async () => { - log.warning(`Proc "${name}" was sent "${signal}" didn't emit the "exit" or "error" events after ${STOP_TIMEOUT} ms, sending SIGKILL`); - await treeKillAsync(childProcess.pid, 'SIGKILL'); - }); - await withTimeout(async () => { - try { - await outcomePromise; - } - catch (error) { - // ignore - } - }, STOP_TIMEOUT, async () => { - throw new Error(`Proc "${name}" was stopped but never emitted either the "exit" or "error" event after ${STOP_TIMEOUT} ms`); - }); - } - return { - name, - lines$, - outcome$, - outcomePromise, - stop, + this._innerSub(result, value, index); }; -} -exports.startProc = startProc; + SwitchMapSubscriber.prototype._innerSub = function (result, value, index) { + var innerSubscription = this.innerSubscription; + if (innerSubscription) { + innerSubscription.unsubscribe(); + } + var innerSubscriber = new _InnerSubscriber__WEBPACK_IMPORTED_MODULE_2__["InnerSubscriber"](this, undefined, undefined); + var destination = this.destination; + destination.add(innerSubscriber); + this.innerSubscription = Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__["subscribeToResult"])(this, result, value, index, innerSubscriber); + }; + SwitchMapSubscriber.prototype._complete = function () { + var innerSubscription = this.innerSubscription; + if (!innerSubscription || innerSubscription.closed) { + _super.prototype._complete.call(this); + } + this.unsubscribe(); + }; + SwitchMapSubscriber.prototype._unsubscribe = function () { + this.innerSubscription = null; + }; + SwitchMapSubscriber.prototype.notifyComplete = function (innerSub) { + var destination = this.destination; + destination.remove(innerSub); + this.innerSubscription = null; + if (this.isStopped) { + _super.prototype._complete.call(this); + } + }; + SwitchMapSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { + this.destination.next(innerValue); + }; + return SwitchMapSubscriber; +}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__["OuterSubscriber"])); +//# sourceMappingURL=switchMap.js.map /***/ }), -/* 351 */ -/***/ (function(module, exports, __webpack_require__) { +/* 349 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "switchMapTo", function() { return switchMapTo; }); +/* harmony import */ var _switchMap__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(348); +/** PURE_IMPORTS_START _switchMap PURE_IMPORTS_END */ -const path = __webpack_require__(16); -const childProcess = __webpack_require__(352); -const crossSpawn = __webpack_require__(353); -const stripFinalNewline = __webpack_require__(366); -const npmRunPath = __webpack_require__(367); -const onetime = __webpack_require__(368); -const makeError = __webpack_require__(370); -const normalizeStdio = __webpack_require__(375); -const {spawnedKill, spawnedCancel, setupTimeout, setExitHandler} = __webpack_require__(376); -const {handleInput, getSpawnedResult, makeAllStream, validateInputSync} = __webpack_require__(380); -const {mergePromise, getSpawnedPromise} = __webpack_require__(389); -const {joinCommand, parseCommand} = __webpack_require__(390); +function switchMapTo(innerObservable, resultSelector) { + return resultSelector ? Object(_switchMap__WEBPACK_IMPORTED_MODULE_0__["switchMap"])(function () { return innerObservable; }, resultSelector) : Object(_switchMap__WEBPACK_IMPORTED_MODULE_0__["switchMap"])(function () { return innerObservable; }); +} +//# sourceMappingURL=switchMapTo.js.map -const DEFAULT_MAX_BUFFER = 1000 * 1000 * 100; -const getEnv = ({env: envOption, extendEnv, preferLocal, localDir, execPath}) => { - const env = extendEnv ? {...process.env, ...envOption} : envOption; +/***/ }), +/* 350 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { - if (preferLocal) { - return npmRunPath.env({env, cwd: localDir, execPath}); - } +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "takeUntil", function() { return takeUntil; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(229); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(230); +/** PURE_IMPORTS_START tslib,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ - return env; -}; -const handleArgs = (file, args, options = {}) => { - const parsed = crossSpawn._parse(file, args, options); - file = parsed.command; - args = parsed.args; - options = parsed.options; - options = { - maxBuffer: DEFAULT_MAX_BUFFER, - buffer: true, - stripFinalNewline: true, - extendEnv: true, - preferLocal: false, - localDir: options.cwd || process.cwd(), - execPath: process.execPath, - encoding: 'utf8', - reject: true, - cleanup: true, - all: false, - windowsHide: true, - ...options - }; +function takeUntil(notifier) { + return function (source) { return source.lift(new TakeUntilOperator(notifier)); }; +} +var TakeUntilOperator = /*@__PURE__*/ (function () { + function TakeUntilOperator(notifier) { + this.notifier = notifier; + } + TakeUntilOperator.prototype.call = function (subscriber, source) { + var takeUntilSubscriber = new TakeUntilSubscriber(subscriber); + var notifierSubscription = Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__["subscribeToResult"])(takeUntilSubscriber, this.notifier); + if (notifierSubscription && !takeUntilSubscriber.seenValue) { + takeUntilSubscriber.add(notifierSubscription); + return source.subscribe(takeUntilSubscriber); + } + return takeUntilSubscriber; + }; + return TakeUntilOperator; +}()); +var TakeUntilSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](TakeUntilSubscriber, _super); + function TakeUntilSubscriber(destination) { + var _this = _super.call(this, destination) || this; + _this.seenValue = false; + return _this; + } + TakeUntilSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { + this.seenValue = true; + this.complete(); + }; + TakeUntilSubscriber.prototype.notifyComplete = function () { + }; + return TakeUntilSubscriber; +}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__["OuterSubscriber"])); +//# sourceMappingURL=takeUntil.js.map - options.env = getEnv(options); - options.stdio = normalizeStdio(options); +/***/ }), +/* 351 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { - if (process.platform === 'win32' && path.basename(file, '.exe') === 'cmd') { - // #116 - args.unshift('/q'); - } +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "takeWhile", function() { return takeWhile; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); +/** PURE_IMPORTS_START tslib,_Subscriber PURE_IMPORTS_END */ - return {file, args, options, parsed}; -}; -const handleOutput = (options, value, error) => { - if (typeof value !== 'string' && !Buffer.isBuffer(value)) { - // When `execa.sync()` errors, we normalize it to '' to mimic `execa()` - return error === undefined ? undefined : ''; - } +function takeWhile(predicate, inclusive) { + if (inclusive === void 0) { + inclusive = false; + } + return function (source) { + return source.lift(new TakeWhileOperator(predicate, inclusive)); + }; +} +var TakeWhileOperator = /*@__PURE__*/ (function () { + function TakeWhileOperator(predicate, inclusive) { + this.predicate = predicate; + this.inclusive = inclusive; + } + TakeWhileOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new TakeWhileSubscriber(subscriber, this.predicate, this.inclusive)); + }; + return TakeWhileOperator; +}()); +var TakeWhileSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](TakeWhileSubscriber, _super); + function TakeWhileSubscriber(destination, predicate, inclusive) { + var _this = _super.call(this, destination) || this; + _this.predicate = predicate; + _this.inclusive = inclusive; + _this.index = 0; + return _this; + } + TakeWhileSubscriber.prototype._next = function (value) { + var destination = this.destination; + var result; + try { + result = this.predicate(value, this.index++); + } + catch (err) { + destination.error(err); + return; + } + this.nextOrComplete(value, result); + }; + TakeWhileSubscriber.prototype.nextOrComplete = function (value, predicateResult) { + var destination = this.destination; + if (Boolean(predicateResult)) { + destination.next(value); + } + else { + if (this.inclusive) { + destination.next(value); + } + destination.complete(); + } + }; + return TakeWhileSubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); +//# sourceMappingURL=takeWhile.js.map - if (options.stripFinalNewline) { - return stripFinalNewline(value); - } - return value; -}; +/***/ }), +/* 352 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { -const execa = (file, args, options) => { - const parsed = handleArgs(file, args, options); - const command = joinCommand(file, args); +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "tap", function() { return tap; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); +/* harmony import */ var _util_noop__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(185); +/* harmony import */ var _util_isFunction__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(173); +/** PURE_IMPORTS_START tslib,_Subscriber,_util_noop,_util_isFunction PURE_IMPORTS_END */ - let spawned; - try { - spawned = childProcess.spawn(parsed.file, parsed.args, parsed.options); - } catch (error) { - // Ensure the returned error is always both a promise and a child process - const dummySpawned = new childProcess.ChildProcess(); - const errorPromise = Promise.reject(makeError({ - error, - stdout: '', - stderr: '', - all: '', - command, - parsed, - timedOut: false, - isCanceled: false, - killed: false - })); - return mergePromise(dummySpawned, errorPromise); - } - const spawnedPromise = getSpawnedPromise(spawned); - const timedPromise = setupTimeout(spawned, parsed.options, spawnedPromise); - const processDone = setExitHandler(spawned, parsed.options, timedPromise); - - const context = {isCanceled: false}; - - spawned.kill = spawnedKill.bind(null, spawned.kill.bind(spawned)); - spawned.cancel = spawnedCancel.bind(null, spawned, context); - - const handlePromise = async () => { - const [{error, exitCode, signal, timedOut}, stdoutResult, stderrResult, allResult] = await getSpawnedResult(spawned, parsed.options, processDone); - const stdout = handleOutput(parsed.options, stdoutResult); - const stderr = handleOutput(parsed.options, stderrResult); - const all = handleOutput(parsed.options, allResult); - - if (error || exitCode !== 0 || signal !== null) { - const returnedError = makeError({ - error, - exitCode, - signal, - stdout, - stderr, - all, - command, - parsed, - timedOut, - isCanceled: context.isCanceled, - killed: spawned.killed - }); - if (!parsed.options.reject) { - return returnedError; - } - throw returnedError; - } +function tap(nextOrObserver, error, complete) { + return function tapOperatorFunction(source) { + return source.lift(new DoOperator(nextOrObserver, error, complete)); + }; +} +var DoOperator = /*@__PURE__*/ (function () { + function DoOperator(nextOrObserver, error, complete) { + this.nextOrObserver = nextOrObserver; + this.error = error; + this.complete = complete; + } + DoOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new TapSubscriber(subscriber, this.nextOrObserver, this.error, this.complete)); + }; + return DoOperator; +}()); +var TapSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](TapSubscriber, _super); + function TapSubscriber(destination, observerOrNext, error, complete) { + var _this = _super.call(this, destination) || this; + _this._tapNext = _util_noop__WEBPACK_IMPORTED_MODULE_2__["noop"]; + _this._tapError = _util_noop__WEBPACK_IMPORTED_MODULE_2__["noop"]; + _this._tapComplete = _util_noop__WEBPACK_IMPORTED_MODULE_2__["noop"]; + _this._tapError = error || _util_noop__WEBPACK_IMPORTED_MODULE_2__["noop"]; + _this._tapComplete = complete || _util_noop__WEBPACK_IMPORTED_MODULE_2__["noop"]; + if (Object(_util_isFunction__WEBPACK_IMPORTED_MODULE_3__["isFunction"])(observerOrNext)) { + _this._context = _this; + _this._tapNext = observerOrNext; + } + else if (observerOrNext) { + _this._context = observerOrNext; + _this._tapNext = observerOrNext.next || _util_noop__WEBPACK_IMPORTED_MODULE_2__["noop"]; + _this._tapError = observerOrNext.error || _util_noop__WEBPACK_IMPORTED_MODULE_2__["noop"]; + _this._tapComplete = observerOrNext.complete || _util_noop__WEBPACK_IMPORTED_MODULE_2__["noop"]; + } + return _this; + } + TapSubscriber.prototype._next = function (value) { + try { + this._tapNext.call(this._context, value); + } + catch (err) { + this.destination.error(err); + return; + } + this.destination.next(value); + }; + TapSubscriber.prototype._error = function (err) { + try { + this._tapError.call(this._context, err); + } + catch (err) { + this.destination.error(err); + return; + } + this.destination.error(err); + }; + TapSubscriber.prototype._complete = function () { + try { + this._tapComplete.call(this._context); + } + catch (err) { + this.destination.error(err); + return; + } + return this.destination.complete(); + }; + return TapSubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); +//# sourceMappingURL=tap.js.map - return { - command, - exitCode: 0, - stdout, - stderr, - all, - failed: false, - timedOut: false, - isCanceled: false, - killed: false - }; - }; - const handlePromiseOnce = onetime(handlePromise); +/***/ }), +/* 353 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { - crossSpawn._enoent.hookChildProcess(spawned, parsed.parsed); +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "defaultThrottleConfig", function() { return defaultThrottleConfig; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "throttle", function() { return throttle; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(229); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(230); +/** PURE_IMPORTS_START tslib,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ - handleInput(spawned, parsed.options.input); - spawned.all = makeAllStream(spawned, parsed.options); - return mergePromise(spawned, handlePromiseOnce); +var defaultThrottleConfig = { + leading: true, + trailing: false }; +function throttle(durationSelector, config) { + if (config === void 0) { + config = defaultThrottleConfig; + } + return function (source) { return source.lift(new ThrottleOperator(durationSelector, config.leading, config.trailing)); }; +} +var ThrottleOperator = /*@__PURE__*/ (function () { + function ThrottleOperator(durationSelector, leading, trailing) { + this.durationSelector = durationSelector; + this.leading = leading; + this.trailing = trailing; + } + ThrottleOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new ThrottleSubscriber(subscriber, this.durationSelector, this.leading, this.trailing)); + }; + return ThrottleOperator; +}()); +var ThrottleSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](ThrottleSubscriber, _super); + function ThrottleSubscriber(destination, durationSelector, _leading, _trailing) { + var _this = _super.call(this, destination) || this; + _this.destination = destination; + _this.durationSelector = durationSelector; + _this._leading = _leading; + _this._trailing = _trailing; + _this._hasValue = false; + return _this; + } + ThrottleSubscriber.prototype._next = function (value) { + this._hasValue = true; + this._sendValue = value; + if (!this._throttled) { + if (this._leading) { + this.send(); + } + else { + this.throttle(value); + } + } + }; + ThrottleSubscriber.prototype.send = function () { + var _a = this, _hasValue = _a._hasValue, _sendValue = _a._sendValue; + if (_hasValue) { + this.destination.next(_sendValue); + this.throttle(_sendValue); + } + this._hasValue = false; + this._sendValue = null; + }; + ThrottleSubscriber.prototype.throttle = function (value) { + var duration = this.tryDurationSelector(value); + if (!!duration) { + this.add(this._throttled = Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__["subscribeToResult"])(this, duration)); + } + }; + ThrottleSubscriber.prototype.tryDurationSelector = function (value) { + try { + return this.durationSelector(value); + } + catch (err) { + this.destination.error(err); + return null; + } + }; + ThrottleSubscriber.prototype.throttlingDone = function () { + var _a = this, _throttled = _a._throttled, _trailing = _a._trailing; + if (_throttled) { + _throttled.unsubscribe(); + } + this._throttled = null; + if (_trailing) { + this.send(); + } + }; + ThrottleSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { + this.throttlingDone(); + }; + ThrottleSubscriber.prototype.notifyComplete = function () { + this.throttlingDone(); + }; + return ThrottleSubscriber; +}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__["OuterSubscriber"])); +//# sourceMappingURL=throttle.js.map -module.exports = execa; - -module.exports.sync = (file, args, options) => { - const parsed = handleArgs(file, args, options); - const command = joinCommand(file, args); - - validateInputSync(parsed.options); - let result; - try { - result = childProcess.spawnSync(parsed.file, parsed.args, parsed.options); - } catch (error) { - throw makeError({ - error, - stdout: '', - stderr: '', - all: '', - command, - parsed, - timedOut: false, - isCanceled: false, - killed: false - }); - } +/***/ }), +/* 354 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { - const stdout = handleOutput(parsed.options, result.stdout, result.error); - const stderr = handleOutput(parsed.options, result.stderr, result.error); +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "throttleTime", function() { return throttleTime; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); +/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(215); +/* harmony import */ var _throttle__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(353); +/** PURE_IMPORTS_START tslib,_Subscriber,_scheduler_async,_throttle PURE_IMPORTS_END */ - if (result.error || result.status !== 0 || result.signal !== null) { - const error = makeError({ - stdout, - stderr, - error: result.error, - signal: result.signal, - exitCode: result.status, - command, - parsed, - timedOut: result.error && result.error.code === 'ETIMEDOUT', - isCanceled: false, - killed: result.signal !== null - }); - if (!parsed.options.reject) { - return error; - } - throw error; - } - return { - command, - exitCode: 0, - stdout, - stderr, - failed: false, - timedOut: false, - isCanceled: false, - killed: false - }; -}; +function throttleTime(duration, scheduler, config) { + if (scheduler === void 0) { + scheduler = _scheduler_async__WEBPACK_IMPORTED_MODULE_2__["async"]; + } + if (config === void 0) { + config = _throttle__WEBPACK_IMPORTED_MODULE_3__["defaultThrottleConfig"]; + } + return function (source) { return source.lift(new ThrottleTimeOperator(duration, scheduler, config.leading, config.trailing)); }; +} +var ThrottleTimeOperator = /*@__PURE__*/ (function () { + function ThrottleTimeOperator(duration, scheduler, leading, trailing) { + this.duration = duration; + this.scheduler = scheduler; + this.leading = leading; + this.trailing = trailing; + } + ThrottleTimeOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new ThrottleTimeSubscriber(subscriber, this.duration, this.scheduler, this.leading, this.trailing)); + }; + return ThrottleTimeOperator; +}()); +var ThrottleTimeSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](ThrottleTimeSubscriber, _super); + function ThrottleTimeSubscriber(destination, duration, scheduler, leading, trailing) { + var _this = _super.call(this, destination) || this; + _this.duration = duration; + _this.scheduler = scheduler; + _this.leading = leading; + _this.trailing = trailing; + _this._hasTrailingValue = false; + _this._trailingValue = null; + return _this; + } + ThrottleTimeSubscriber.prototype._next = function (value) { + if (this.throttled) { + if (this.trailing) { + this._trailingValue = value; + this._hasTrailingValue = true; + } + } + else { + this.add(this.throttled = this.scheduler.schedule(dispatchNext, this.duration, { subscriber: this })); + if (this.leading) { + this.destination.next(value); + } + else if (this.trailing) { + this._trailingValue = value; + this._hasTrailingValue = true; + } + } + }; + ThrottleTimeSubscriber.prototype._complete = function () { + if (this._hasTrailingValue) { + this.destination.next(this._trailingValue); + this.destination.complete(); + } + else { + this.destination.complete(); + } + }; + ThrottleTimeSubscriber.prototype.clearThrottle = function () { + var throttled = this.throttled; + if (throttled) { + if (this.trailing && this._hasTrailingValue) { + this.destination.next(this._trailingValue); + this._trailingValue = null; + this._hasTrailingValue = false; + } + throttled.unsubscribe(); + this.remove(throttled); + this.throttled = null; + } + }; + return ThrottleTimeSubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); +function dispatchNext(arg) { + var subscriber = arg.subscriber; + subscriber.clearThrottle(); +} +//# sourceMappingURL=throttleTime.js.map -module.exports.command = (command, options) => { - const [file, ...args] = parseCommand(command); - return execa(file, args, options); -}; -module.exports.commandSync = (command, options) => { - const [file, ...args] = parseCommand(command); - return execa.sync(file, args, options); -}; +/***/ }), +/* 355 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { -module.exports.node = (scriptPath, args, options = {}) => { - if (args && !Array.isArray(args) && typeof args === 'object') { - options = args; - args = []; - } +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timeInterval", function() { return timeInterval; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "TimeInterval", function() { return TimeInterval; }); +/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(215); +/* harmony import */ var _scan__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(315); +/* harmony import */ var _observable_defer__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(250); +/* harmony import */ var _map__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(226); +/** PURE_IMPORTS_START _scheduler_async,_scan,_observable_defer,_map PURE_IMPORTS_END */ - const stdio = normalizeStdio.node(options); - const {nodePath = process.execPath, nodeOptions = process.execArgv} = options; - return execa( - nodePath, - [ - ...nodeOptions, - scriptPath, - ...(Array.isArray(args) ? args : []) - ], - { - ...options, - stdin: undefined, - stdout: undefined, - stderr: undefined, - stdio, - shell: false - } - ); -}; +function timeInterval(scheduler) { + if (scheduler === void 0) { + scheduler = _scheduler_async__WEBPACK_IMPORTED_MODULE_0__["async"]; + } + return function (source) { + return Object(_observable_defer__WEBPACK_IMPORTED_MODULE_2__["defer"])(function () { + return source.pipe(Object(_scan__WEBPACK_IMPORTED_MODULE_1__["scan"])(function (_a, value) { + var current = _a.current; + return ({ value: value, current: scheduler.now(), last: current }); + }, { current: scheduler.now(), value: undefined, last: undefined }), Object(_map__WEBPACK_IMPORTED_MODULE_3__["map"])(function (_a) { + var current = _a.current, last = _a.last, value = _a.value; + return new TimeInterval(value, current - last); + })); + }); + }; +} +var TimeInterval = /*@__PURE__*/ (function () { + function TimeInterval(value, interval) { + this.value = value; + this.interval = interval; + } + return TimeInterval; +}()); -/***/ }), -/* 352 */ -/***/ (function(module, exports) { +//# sourceMappingURL=timeInterval.js.map -module.exports = require("child_process"); /***/ }), -/* 353 */ -/***/ (function(module, exports, __webpack_require__) { +/* 356 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timeout", function() { return timeout; }); +/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(215); +/* harmony import */ var _util_TimeoutError__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(224); +/* harmony import */ var _timeoutWith__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(357); +/* harmony import */ var _observable_throwError__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(209); +/** PURE_IMPORTS_START _scheduler_async,_util_TimeoutError,_timeoutWith,_observable_throwError PURE_IMPORTS_END */ -const cp = __webpack_require__(352); -const parse = __webpack_require__(354); -const enoent = __webpack_require__(365); - -function spawn(command, args, options) { - // Parse the arguments - const parsed = parse(command, args, options); - - // Spawn the child process - const spawned = cp.spawn(parsed.command, parsed.args, parsed.options); - - // Hook into child process "exit" event to emit an error if the command - // does not exists, see: https://github.com/IndigoUnited/node-cross-spawn/issues/16 - enoent.hookChildProcess(spawned, parsed); - - return spawned; -} - -function spawnSync(command, args, options) { - // Parse the arguments - const parsed = parse(command, args, options); - - // Spawn the child process - const result = cp.spawnSync(parsed.command, parsed.args, parsed.options); - // Analyze if the command does not exist, see: https://github.com/IndigoUnited/node-cross-spawn/issues/16 - result.error = result.error || enoent.verifyENOENTSync(result.status, parsed); - return result; +function timeout(due, scheduler) { + if (scheduler === void 0) { + scheduler = _scheduler_async__WEBPACK_IMPORTED_MODULE_0__["async"]; + } + return Object(_timeoutWith__WEBPACK_IMPORTED_MODULE_2__["timeoutWith"])(due, Object(_observable_throwError__WEBPACK_IMPORTED_MODULE_3__["throwError"])(new _util_TimeoutError__WEBPACK_IMPORTED_MODULE_1__["TimeoutError"]()), scheduler); } - -module.exports = spawn; -module.exports.spawn = spawn; -module.exports.sync = spawnSync; - -module.exports._parse = parse; -module.exports._enoent = enoent; +//# sourceMappingURL=timeout.js.map /***/ }), -/* 354 */ -/***/ (function(module, exports, __webpack_require__) { +/* 357 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timeoutWith", function() { return timeoutWith; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(215); +/* harmony import */ var _util_isDate__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(289); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(229); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(230); +/** PURE_IMPORTS_START tslib,_scheduler_async,_util_isDate,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ -const path = __webpack_require__(16); -const resolveCommand = __webpack_require__(355); -const escape = __webpack_require__(361); -const readShebang = __webpack_require__(362); - -const isWin = process.platform === 'win32'; -const isExecutableRegExp = /\.(?:com|exe)$/i; -const isCmdShimRegExp = /node_modules[\\/].bin[\\/][^\\/]+\.cmd$/i; - -function detectShebang(parsed) { - parsed.file = resolveCommand(parsed); - const shebang = parsed.file && readShebang(parsed.file); - if (shebang) { - parsed.args.unshift(parsed.file); - parsed.command = shebang; - return resolveCommand(parsed); +function timeoutWith(due, withObservable, scheduler) { + if (scheduler === void 0) { + scheduler = _scheduler_async__WEBPACK_IMPORTED_MODULE_1__["async"]; } - - return parsed.file; + return function (source) { + var absoluteTimeout = Object(_util_isDate__WEBPACK_IMPORTED_MODULE_2__["isDate"])(due); + var waitFor = absoluteTimeout ? (+due - scheduler.now()) : Math.abs(due); + return source.lift(new TimeoutWithOperator(waitFor, absoluteTimeout, withObservable, scheduler)); + }; } - -function parseNonShell(parsed) { - if (!isWin) { - return parsed; +var TimeoutWithOperator = /*@__PURE__*/ (function () { + function TimeoutWithOperator(waitFor, absoluteTimeout, withObservable, scheduler) { + this.waitFor = waitFor; + this.absoluteTimeout = absoluteTimeout; + this.withObservable = withObservable; + this.scheduler = scheduler; } + TimeoutWithOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new TimeoutWithSubscriber(subscriber, this.absoluteTimeout, this.waitFor, this.withObservable, this.scheduler)); + }; + return TimeoutWithOperator; +}()); +var TimeoutWithSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](TimeoutWithSubscriber, _super); + function TimeoutWithSubscriber(destination, absoluteTimeout, waitFor, withObservable, scheduler) { + var _this = _super.call(this, destination) || this; + _this.absoluteTimeout = absoluteTimeout; + _this.waitFor = waitFor; + _this.withObservable = withObservable; + _this.scheduler = scheduler; + _this.action = null; + _this.scheduleTimeout(); + return _this; + } + TimeoutWithSubscriber.dispatchTimeout = function (subscriber) { + var withObservable = subscriber.withObservable; + subscriber._unsubscribeAndRecycle(); + subscriber.add(Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__["subscribeToResult"])(subscriber, withObservable)); + }; + TimeoutWithSubscriber.prototype.scheduleTimeout = function () { + var action = this.action; + if (action) { + this.action = action.schedule(this, this.waitFor); + } + else { + this.add(this.action = this.scheduler.schedule(TimeoutWithSubscriber.dispatchTimeout, this.waitFor, this)); + } + }; + TimeoutWithSubscriber.prototype._next = function (value) { + if (!this.absoluteTimeout) { + this.scheduleTimeout(); + } + _super.prototype._next.call(this, value); + }; + TimeoutWithSubscriber.prototype._unsubscribe = function () { + this.action = null; + this.scheduler = null; + this.withObservable = null; + }; + return TimeoutWithSubscriber; +}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__["OuterSubscriber"])); +//# sourceMappingURL=timeoutWith.js.map - // Detect & add support for shebangs - const commandFile = detectShebang(parsed); - - // We don't need a shell if the command filename is an executable - const needsShell = !isExecutableRegExp.test(commandFile); - - // If a shell is required, use cmd.exe and take care of escaping everything correctly - // Note that `forceShell` is an hidden option used only in tests - if (parsed.options.forceShell || needsShell) { - // Need to double escape meta chars if the command is a cmd-shim located in `node_modules/.bin/` - // The cmd-shim simply calls execute the package bin file with NodeJS, proxying any argument - // Because the escape of metachars with ^ gets interpreted when the cmd.exe is first called, - // we need to double escape them - const needsDoubleEscapeMetaChars = isCmdShimRegExp.test(commandFile); - // Normalize posix paths into OS compatible paths (e.g.: foo/bar -> foo\bar) - // This is necessary otherwise it will always fail with ENOENT in those cases - parsed.command = path.normalize(parsed.command); +/***/ }), +/* 358 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { - // Escape command & arguments - parsed.command = escape.command(parsed.command); - parsed.args = parsed.args.map((arg) => escape.argument(arg, needsDoubleEscapeMetaChars)); +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "timestamp", function() { return timestamp; }); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Timestamp", function() { return Timestamp; }); +/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(215); +/* harmony import */ var _map__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(226); +/** PURE_IMPORTS_START _scheduler_async,_map PURE_IMPORTS_END */ - const shellCommand = [parsed.command].concat(parsed.args).join(' '); - parsed.args = ['/d', '/s', '/c', `"${shellCommand}"`]; - parsed.command = process.env.comspec || 'cmd.exe'; - parsed.options.windowsVerbatimArguments = true; // Tell node's spawn that the arguments are already escaped +function timestamp(scheduler) { + if (scheduler === void 0) { + scheduler = _scheduler_async__WEBPACK_IMPORTED_MODULE_0__["async"]; } - - return parsed; + return Object(_map__WEBPACK_IMPORTED_MODULE_1__["map"])(function (value) { return new Timestamp(value, scheduler.now()); }); } - -function parse(command, args, options) { - // Normalize arguments, similar to nodejs - if (args && !Array.isArray(args)) { - options = args; - args = null; +var Timestamp = /*@__PURE__*/ (function () { + function Timestamp(value, timestamp) { + this.value = value; + this.timestamp = timestamp; } + return Timestamp; +}()); - args = args ? args.slice(0) : []; // Clone array to avoid changing the original - options = Object.assign({}, options); // Clone object to avoid changing the original - - // Build our parsed object - const parsed = { - command, - args, - options, - file: undefined, - original: { - command, - args, - }, - }; - - // Delegate further parsing to shell or non-shell - return options.shell ? parsed : parseNonShell(parsed); -} - -module.exports = parse; +//# sourceMappingURL=timestamp.js.map /***/ }), -/* 355 */ -/***/ (function(module, exports, __webpack_require__) { +/* 359 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "toArray", function() { return toArray; }); +/* harmony import */ var _reduce__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(314); +/** PURE_IMPORTS_START _reduce PURE_IMPORTS_END */ - -const path = __webpack_require__(16); -const which = __webpack_require__(356); -const pathKey = __webpack_require__(360)(); - -function resolveCommandAttempt(parsed, withoutPathExt) { - const cwd = process.cwd(); - const hasCustomCwd = parsed.options.cwd != null; - // Worker threads do not have process.chdir() - const shouldSwitchCwd = hasCustomCwd && process.chdir !== undefined; - - // If a custom `cwd` was specified, we need to change the process cwd - // because `which` will do stat calls but does not support a custom cwd - if (shouldSwitchCwd) { - try { - process.chdir(parsed.options.cwd); - } catch (err) { - /* Empty */ - } - } - - let resolved; - - try { - resolved = which.sync(parsed.command, { - path: (parsed.options.env || process.env)[pathKey], - pathExt: withoutPathExt ? path.delimiter : undefined, - }); - } catch (e) { - /* Empty */ - } finally { - if (shouldSwitchCwd) { - process.chdir(cwd); - } - } - - // If we successfully resolved, ensure that an absolute path is returned - // Note that when a custom `cwd` was used, we need to resolve to an absolute path based on it - if (resolved) { - resolved = path.resolve(hasCustomCwd ? parsed.options.cwd : '', resolved); +function toArrayReducer(arr, item, index) { + if (index === 0) { + return [item]; } - - return resolved; + arr.push(item); + return arr; } - -function resolveCommand(parsed) { - return resolveCommandAttempt(parsed) || resolveCommandAttempt(parsed, true); +function toArray() { + return Object(_reduce__WEBPACK_IMPORTED_MODULE_0__["reduce"])(toArrayReducer, []); } - -module.exports = resolveCommand; +//# sourceMappingURL=toArray.js.map /***/ }), -/* 356 */ -/***/ (function(module, exports, __webpack_require__) { - -const isWindows = process.platform === 'win32' || - process.env.OSTYPE === 'cygwin' || - process.env.OSTYPE === 'msys' - -const path = __webpack_require__(16) -const COLON = isWindows ? ';' : ':' -const isexe = __webpack_require__(357) +/* 360 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { -const getNotFoundError = (cmd) => - Object.assign(new Error(`not found: ${cmd}`), { code: 'ENOENT' }) +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "window", function() { return window; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(187); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(229); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(230); +/** PURE_IMPORTS_START tslib,_Subject,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ -const getPathInfo = (cmd, opt) => { - const colon = opt.colon || COLON - // If it has a slash, then we don't bother searching the pathenv. - // just check the file itself, and that's it. - const pathEnv = cmd.match(/\//) || isWindows && cmd.match(/\\/) ? [''] - : ( - [ - // windows always checks the cwd first - ...(isWindows ? [process.cwd()] : []), - ...(opt.path || process.env.PATH || - /* istanbul ignore next: very unusual */ '').split(colon), - ] - ) - const pathExtExe = isWindows - ? opt.pathExt || process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM' - : '' - const pathExt = isWindows ? pathExtExe.split(colon) : [''] - if (isWindows) { - if (cmd.indexOf('.') !== -1 && pathExt[0] !== '') - pathExt.unshift('') - } - return { - pathEnv, - pathExt, - pathExtExe, - } +function window(windowBoundaries) { + return function windowOperatorFunction(source) { + return source.lift(new WindowOperator(windowBoundaries)); + }; } +var WindowOperator = /*@__PURE__*/ (function () { + function WindowOperator(windowBoundaries) { + this.windowBoundaries = windowBoundaries; + } + WindowOperator.prototype.call = function (subscriber, source) { + var windowSubscriber = new WindowSubscriber(subscriber); + var sourceSubscription = source.subscribe(windowSubscriber); + if (!sourceSubscription.closed) { + windowSubscriber.add(Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__["subscribeToResult"])(windowSubscriber, this.windowBoundaries)); + } + return sourceSubscription; + }; + return WindowOperator; +}()); +var WindowSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](WindowSubscriber, _super); + function WindowSubscriber(destination) { + var _this = _super.call(this, destination) || this; + _this.window = new _Subject__WEBPACK_IMPORTED_MODULE_1__["Subject"](); + destination.next(_this.window); + return _this; + } + WindowSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { + this.openWindow(); + }; + WindowSubscriber.prototype.notifyError = function (error, innerSub) { + this._error(error); + }; + WindowSubscriber.prototype.notifyComplete = function (innerSub) { + this._complete(); + }; + WindowSubscriber.prototype._next = function (value) { + this.window.next(value); + }; + WindowSubscriber.prototype._error = function (err) { + this.window.error(err); + this.destination.error(err); + }; + WindowSubscriber.prototype._complete = function () { + this.window.complete(); + this.destination.complete(); + }; + WindowSubscriber.prototype._unsubscribe = function () { + this.window = null; + }; + WindowSubscriber.prototype.openWindow = function () { + var prevWindow = this.window; + if (prevWindow) { + prevWindow.complete(); + } + var destination = this.destination; + var newWindow = this.window = new _Subject__WEBPACK_IMPORTED_MODULE_1__["Subject"](); + destination.next(newWindow); + }; + return WindowSubscriber; +}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_2__["OuterSubscriber"])); +//# sourceMappingURL=window.js.map -const which = (cmd, opt, cb) => { - if (typeof opt === 'function') { - cb = opt - opt = {} - } - if (!opt) - opt = {} - - const { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt) - const found = [] - - const step = i => new Promise((resolve, reject) => { - if (i === pathEnv.length) - return opt.all && found.length ? resolve(found) - : reject(getNotFoundError(cmd)) - const ppRaw = pathEnv[i] - const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw +/***/ }), +/* 361 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { - const pCmd = path.join(pathPart, cmd) - const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd - : pCmd +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "windowCount", function() { return windowCount; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(172); +/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(187); +/** PURE_IMPORTS_START tslib,_Subscriber,_Subject PURE_IMPORTS_END */ - resolve(subStep(p, i, 0)) - }) - const subStep = (p, i, ii) => new Promise((resolve, reject) => { - if (ii === pathExt.length) - return resolve(step(i + 1)) - const ext = pathExt[ii] - isexe(p + ext, { pathExt: pathExtExe }, (er, is) => { - if (!er && is) { - if (opt.all) - found.push(p + ext) - else - return resolve(p + ext) - } - return resolve(subStep(p, i, ii + 1)) - }) - }) - return cb ? step(0).then(res => cb(null, res), cb) : step(0) +function windowCount(windowSize, startWindowEvery) { + if (startWindowEvery === void 0) { + startWindowEvery = 0; + } + return function windowCountOperatorFunction(source) { + return source.lift(new WindowCountOperator(windowSize, startWindowEvery)); + }; } - -const whichSync = (cmd, opt) => { - opt = opt || {} - - const { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt) - const found = [] - - for (let i = 0; i < pathEnv.length; i ++) { - const ppRaw = pathEnv[i] - const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw - - const pCmd = path.join(pathPart, cmd) - const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd - : pCmd - - for (let j = 0; j < pathExt.length; j ++) { - const cur = p + pathExt[j] - try { - const is = isexe.sync(cur, { pathExt: pathExtExe }) - if (is) { - if (opt.all) - found.push(cur) - else - return cur - } - } catch (ex) {} +var WindowCountOperator = /*@__PURE__*/ (function () { + function WindowCountOperator(windowSize, startWindowEvery) { + this.windowSize = windowSize; + this.startWindowEvery = startWindowEvery; } - } - - if (opt.all && found.length) - return found + WindowCountOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new WindowCountSubscriber(subscriber, this.windowSize, this.startWindowEvery)); + }; + return WindowCountOperator; +}()); +var WindowCountSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](WindowCountSubscriber, _super); + function WindowCountSubscriber(destination, windowSize, startWindowEvery) { + var _this = _super.call(this, destination) || this; + _this.destination = destination; + _this.windowSize = windowSize; + _this.startWindowEvery = startWindowEvery; + _this.windows = [new _Subject__WEBPACK_IMPORTED_MODULE_2__["Subject"]()]; + _this.count = 0; + destination.next(_this.windows[0]); + return _this; + } + WindowCountSubscriber.prototype._next = function (value) { + var startWindowEvery = (this.startWindowEvery > 0) ? this.startWindowEvery : this.windowSize; + var destination = this.destination; + var windowSize = this.windowSize; + var windows = this.windows; + var len = windows.length; + for (var i = 0; i < len && !this.closed; i++) { + windows[i].next(value); + } + var c = this.count - windowSize + 1; + if (c >= 0 && c % startWindowEvery === 0 && !this.closed) { + windows.shift().complete(); + } + if (++this.count % startWindowEvery === 0 && !this.closed) { + var window_1 = new _Subject__WEBPACK_IMPORTED_MODULE_2__["Subject"](); + windows.push(window_1); + destination.next(window_1); + } + }; + WindowCountSubscriber.prototype._error = function (err) { + var windows = this.windows; + if (windows) { + while (windows.length > 0 && !this.closed) { + windows.shift().error(err); + } + } + this.destination.error(err); + }; + WindowCountSubscriber.prototype._complete = function () { + var windows = this.windows; + if (windows) { + while (windows.length > 0 && !this.closed) { + windows.shift().complete(); + } + } + this.destination.complete(); + }; + WindowCountSubscriber.prototype._unsubscribe = function () { + this.count = 0; + this.windows = null; + }; + return WindowCountSubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_1__["Subscriber"])); +//# sourceMappingURL=windowCount.js.map - if (opt.nothrow) - return null - throw getNotFoundError(cmd) -} +/***/ }), +/* 362 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { -module.exports = which -which.sync = whichSync +"use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "windowTime", function() { return windowTime; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(187); +/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(215); +/* harmony import */ var _Subscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(172); +/* harmony import */ var _util_isNumeric__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(257); +/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(205); +/** PURE_IMPORTS_START tslib,_Subject,_scheduler_async,_Subscriber,_util_isNumeric,_util_isScheduler PURE_IMPORTS_END */ -/***/ }), -/* 357 */ -/***/ (function(module, exports, __webpack_require__) { -var fs = __webpack_require__(23) -var core -if (process.platform === 'win32' || global.TESTING_WINDOWS) { - core = __webpack_require__(358) -} else { - core = __webpack_require__(359) -} -module.exports = isexe -isexe.sync = sync -function isexe (path, options, cb) { - if (typeof options === 'function') { - cb = options - options = {} - } - if (!cb) { - if (typeof Promise !== 'function') { - throw new TypeError('callback not provided') +function windowTime(windowTimeSpan) { + var scheduler = _scheduler_async__WEBPACK_IMPORTED_MODULE_2__["async"]; + var windowCreationInterval = null; + var maxWindowSize = Number.POSITIVE_INFINITY; + if (Object(_util_isScheduler__WEBPACK_IMPORTED_MODULE_5__["isScheduler"])(arguments[3])) { + scheduler = arguments[3]; } - - return new Promise(function (resolve, reject) { - isexe(path, options || {}, function (er, is) { - if (er) { - reject(er) - } else { - resolve(is) - } - }) - }) - } - - core(path, options || {}, function (er, is) { - // ignore EACCES because that just means we aren't allowed to run it - if (er) { - if (er.code === 'EACCES' || options && options.ignoreErrors) { - er = null - is = false - } + if (Object(_util_isScheduler__WEBPACK_IMPORTED_MODULE_5__["isScheduler"])(arguments[2])) { + scheduler = arguments[2]; } - cb(er, is) - }) -} - -function sync (path, options) { - // my kingdom for a filtered catch - try { - return core.sync(path, options || {}) - } catch (er) { - if (options && options.ignoreErrors || er.code === 'EACCES') { - return false - } else { - throw er + else if (Object(_util_isNumeric__WEBPACK_IMPORTED_MODULE_4__["isNumeric"])(arguments[2])) { + maxWindowSize = arguments[2]; } - } -} - - -/***/ }), -/* 358 */ -/***/ (function(module, exports, __webpack_require__) { - -module.exports = isexe -isexe.sync = sync - -var fs = __webpack_require__(23) - -function checkPathExt (path, options) { - var pathext = options.pathExt !== undefined ? - options.pathExt : process.env.PATHEXT - - if (!pathext) { - return true - } - - pathext = pathext.split(';') - if (pathext.indexOf('') !== -1) { - return true - } - for (var i = 0; i < pathext.length; i++) { - var p = pathext[i].toLowerCase() - if (p && path.substr(-p.length).toLowerCase() === p) { - return true + if (Object(_util_isScheduler__WEBPACK_IMPORTED_MODULE_5__["isScheduler"])(arguments[1])) { + scheduler = arguments[1]; } - } - return false -} - -function checkStat (stat, path, options) { - if (!stat.isSymbolicLink() && !stat.isFile()) { - return false - } - return checkPathExt(path, options) -} - -function isexe (path, options, cb) { - fs.stat(path, function (er, stat) { - cb(er, er ? false : checkStat(stat, path, options)) - }) -} - -function sync (path, options) { - return checkStat(fs.statSync(path), path, options) -} - - -/***/ }), -/* 359 */ -/***/ (function(module, exports, __webpack_require__) { - -module.exports = isexe -isexe.sync = sync - -var fs = __webpack_require__(23) - -function isexe (path, options, cb) { - fs.stat(path, function (er, stat) { - cb(er, er ? false : checkStat(stat, options)) - }) + else if (Object(_util_isNumeric__WEBPACK_IMPORTED_MODULE_4__["isNumeric"])(arguments[1])) { + windowCreationInterval = arguments[1]; + } + return function windowTimeOperatorFunction(source) { + return source.lift(new WindowTimeOperator(windowTimeSpan, windowCreationInterval, maxWindowSize, scheduler)); + }; } - -function sync (path, options) { - return checkStat(fs.statSync(path), options) +var WindowTimeOperator = /*@__PURE__*/ (function () { + function WindowTimeOperator(windowTimeSpan, windowCreationInterval, maxWindowSize, scheduler) { + this.windowTimeSpan = windowTimeSpan; + this.windowCreationInterval = windowCreationInterval; + this.maxWindowSize = maxWindowSize; + this.scheduler = scheduler; + } + WindowTimeOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new WindowTimeSubscriber(subscriber, this.windowTimeSpan, this.windowCreationInterval, this.maxWindowSize, this.scheduler)); + }; + return WindowTimeOperator; +}()); +var CountedSubject = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](CountedSubject, _super); + function CountedSubject() { + var _this = _super !== null && _super.apply(this, arguments) || this; + _this._numberOfNextedValues = 0; + return _this; + } + CountedSubject.prototype.next = function (value) { + this._numberOfNextedValues++; + _super.prototype.next.call(this, value); + }; + Object.defineProperty(CountedSubject.prototype, "numberOfNextedValues", { + get: function () { + return this._numberOfNextedValues; + }, + enumerable: true, + configurable: true + }); + return CountedSubject; +}(_Subject__WEBPACK_IMPORTED_MODULE_1__["Subject"])); +var WindowTimeSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](WindowTimeSubscriber, _super); + function WindowTimeSubscriber(destination, windowTimeSpan, windowCreationInterval, maxWindowSize, scheduler) { + var _this = _super.call(this, destination) || this; + _this.destination = destination; + _this.windowTimeSpan = windowTimeSpan; + _this.windowCreationInterval = windowCreationInterval; + _this.maxWindowSize = maxWindowSize; + _this.scheduler = scheduler; + _this.windows = []; + var window = _this.openWindow(); + if (windowCreationInterval !== null && windowCreationInterval >= 0) { + var closeState = { subscriber: _this, window: window, context: null }; + var creationState = { windowTimeSpan: windowTimeSpan, windowCreationInterval: windowCreationInterval, subscriber: _this, scheduler: scheduler }; + _this.add(scheduler.schedule(dispatchWindowClose, windowTimeSpan, closeState)); + _this.add(scheduler.schedule(dispatchWindowCreation, windowCreationInterval, creationState)); + } + else { + var timeSpanOnlyState = { subscriber: _this, window: window, windowTimeSpan: windowTimeSpan }; + _this.add(scheduler.schedule(dispatchWindowTimeSpanOnly, windowTimeSpan, timeSpanOnlyState)); + } + return _this; + } + WindowTimeSubscriber.prototype._next = function (value) { + var windows = this.windows; + var len = windows.length; + for (var i = 0; i < len; i++) { + var window_1 = windows[i]; + if (!window_1.closed) { + window_1.next(value); + if (window_1.numberOfNextedValues >= this.maxWindowSize) { + this.closeWindow(window_1); + } + } + } + }; + WindowTimeSubscriber.prototype._error = function (err) { + var windows = this.windows; + while (windows.length > 0) { + windows.shift().error(err); + } + this.destination.error(err); + }; + WindowTimeSubscriber.prototype._complete = function () { + var windows = this.windows; + while (windows.length > 0) { + var window_2 = windows.shift(); + if (!window_2.closed) { + window_2.complete(); + } + } + this.destination.complete(); + }; + WindowTimeSubscriber.prototype.openWindow = function () { + var window = new CountedSubject(); + this.windows.push(window); + var destination = this.destination; + destination.next(window); + return window; + }; + WindowTimeSubscriber.prototype.closeWindow = function (window) { + window.complete(); + var windows = this.windows; + windows.splice(windows.indexOf(window), 1); + }; + return WindowTimeSubscriber; +}(_Subscriber__WEBPACK_IMPORTED_MODULE_3__["Subscriber"])); +function dispatchWindowTimeSpanOnly(state) { + var subscriber = state.subscriber, windowTimeSpan = state.windowTimeSpan, window = state.window; + if (window) { + subscriber.closeWindow(window); + } + state.window = subscriber.openWindow(); + this.schedule(state, windowTimeSpan); } - -function checkStat (stat, options) { - return stat.isFile() && checkMode(stat, options) +function dispatchWindowCreation(state) { + var windowTimeSpan = state.windowTimeSpan, subscriber = state.subscriber, scheduler = state.scheduler, windowCreationInterval = state.windowCreationInterval; + var window = subscriber.openWindow(); + var action = this; + var context = { action: action, subscription: null }; + var timeSpanState = { subscriber: subscriber, window: window, context: context }; + context.subscription = scheduler.schedule(dispatchWindowClose, windowTimeSpan, timeSpanState); + action.add(context.subscription); + action.schedule(state, windowCreationInterval); } - -function checkMode (stat, options) { - var mod = stat.mode - var uid = stat.uid - var gid = stat.gid - - var myUid = options.uid !== undefined ? - options.uid : process.getuid && process.getuid() - var myGid = options.gid !== undefined ? - options.gid : process.getgid && process.getgid() - - var u = parseInt('100', 8) - var g = parseInt('010', 8) - var o = parseInt('001', 8) - var ug = u | g - - var ret = (mod & o) || - (mod & g) && gid === myGid || - (mod & u) && uid === myUid || - (mod & ug) && myUid === 0 - - return ret +function dispatchWindowClose(state) { + var subscriber = state.subscriber, window = state.window, context = state.context; + if (context && context.action && context.subscription) { + context.action.remove(context.subscription); + } + subscriber.closeWindow(window); } +//# sourceMappingURL=windowTime.js.map /***/ }), -/* 360 */ -/***/ (function(module, exports, __webpack_require__) { +/* 363 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "windowToggle", function() { return windowToggle; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(187); +/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(177); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(229); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(230); +/** PURE_IMPORTS_START tslib,_Subject,_Subscription,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ -const pathKey = (options = {}) => { - const environment = options.env || process.env; - const platform = options.platform || process.platform; - if (platform !== 'win32') { - return 'PATH'; - } - return Object.keys(environment).find(key => key.toUpperCase() === 'PATH') || 'Path'; -}; -module.exports = pathKey; -// TODO: Remove this for the next major release -module.exports.default = pathKey; +function windowToggle(openings, closingSelector) { + return function (source) { return source.lift(new WindowToggleOperator(openings, closingSelector)); }; +} +var WindowToggleOperator = /*@__PURE__*/ (function () { + function WindowToggleOperator(openings, closingSelector) { + this.openings = openings; + this.closingSelector = closingSelector; + } + WindowToggleOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new WindowToggleSubscriber(subscriber, this.openings, this.closingSelector)); + }; + return WindowToggleOperator; +}()); +var WindowToggleSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](WindowToggleSubscriber, _super); + function WindowToggleSubscriber(destination, openings, closingSelector) { + var _this = _super.call(this, destination) || this; + _this.openings = openings; + _this.closingSelector = closingSelector; + _this.contexts = []; + _this.add(_this.openSubscription = Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__["subscribeToResult"])(_this, openings, openings)); + return _this; + } + WindowToggleSubscriber.prototype._next = function (value) { + var contexts = this.contexts; + if (contexts) { + var len = contexts.length; + for (var i = 0; i < len; i++) { + contexts[i].window.next(value); + } + } + }; + WindowToggleSubscriber.prototype._error = function (err) { + var contexts = this.contexts; + this.contexts = null; + if (contexts) { + var len = contexts.length; + var index = -1; + while (++index < len) { + var context_1 = contexts[index]; + context_1.window.error(err); + context_1.subscription.unsubscribe(); + } + } + _super.prototype._error.call(this, err); + }; + WindowToggleSubscriber.prototype._complete = function () { + var contexts = this.contexts; + this.contexts = null; + if (contexts) { + var len = contexts.length; + var index = -1; + while (++index < len) { + var context_2 = contexts[index]; + context_2.window.complete(); + context_2.subscription.unsubscribe(); + } + } + _super.prototype._complete.call(this); + }; + WindowToggleSubscriber.prototype._unsubscribe = function () { + var contexts = this.contexts; + this.contexts = null; + if (contexts) { + var len = contexts.length; + var index = -1; + while (++index < len) { + var context_3 = contexts[index]; + context_3.window.unsubscribe(); + context_3.subscription.unsubscribe(); + } + } + }; + WindowToggleSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { + if (outerValue === this.openings) { + var closingNotifier = void 0; + try { + var closingSelector = this.closingSelector; + closingNotifier = closingSelector(innerValue); + } + catch (e) { + return this.error(e); + } + var window_1 = new _Subject__WEBPACK_IMPORTED_MODULE_1__["Subject"](); + var subscription = new _Subscription__WEBPACK_IMPORTED_MODULE_2__["Subscription"](); + var context_4 = { window: window_1, subscription: subscription }; + this.contexts.push(context_4); + var innerSubscription = Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_4__["subscribeToResult"])(this, closingNotifier, context_4); + if (innerSubscription.closed) { + this.closeWindow(this.contexts.length - 1); + } + else { + innerSubscription.context = context_4; + subscription.add(innerSubscription); + } + this.destination.next(window_1); + } + else { + this.closeWindow(this.contexts.indexOf(outerValue)); + } + }; + WindowToggleSubscriber.prototype.notifyError = function (err) { + this.error(err); + }; + WindowToggleSubscriber.prototype.notifyComplete = function (inner) { + if (inner !== this.openSubscription) { + this.closeWindow(this.contexts.indexOf(inner.context)); + } + }; + WindowToggleSubscriber.prototype.closeWindow = function (index) { + if (index === -1) { + return; + } + var contexts = this.contexts; + var context = contexts[index]; + var window = context.window, subscription = context.subscription; + contexts.splice(index, 1); + window.complete(); + subscription.unsubscribe(); + }; + return WindowToggleSubscriber; +}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_3__["OuterSubscriber"])); +//# sourceMappingURL=windowToggle.js.map /***/ }), -/* 361 */ -/***/ (function(module, exports, __webpack_require__) { +/* 364 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "windowWhen", function() { return windowWhen; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _Subject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(187); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(229); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(230); +/** PURE_IMPORTS_START tslib,_Subject,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ -// See http://www.robvanderwoude.com/escapechars.php -const metaCharsRegExp = /([()\][%!^"`<>&|;, *?])/g; -function escapeCommand(arg) { - // Escape meta chars - arg = arg.replace(metaCharsRegExp, '^$1'); - return arg; +function windowWhen(closingSelector) { + return function windowWhenOperatorFunction(source) { + return source.lift(new WindowOperator(closingSelector)); + }; } - -function escapeArgument(arg, doubleEscapeMetaChars) { - // Convert to string - arg = `${arg}`; - - // Algorithm below is based on https://qntm.org/cmd - - // Sequence of backslashes followed by a double quote: - // double up all the backslashes and escape the double quote - arg = arg.replace(/(\\*)"/g, '$1$1\\"'); - - // Sequence of backslashes followed by the end of the string - // (which will become a double quote later): - // double up all the backslashes - arg = arg.replace(/(\\*)$/, '$1$1'); - - // All other backslashes occur literally - - // Quote the whole thing: - arg = `"${arg}"`; - - // Escape meta chars - arg = arg.replace(metaCharsRegExp, '^$1'); - - // Double escape meta chars if necessary - if (doubleEscapeMetaChars) { - arg = arg.replace(metaCharsRegExp, '^$1'); +var WindowOperator = /*@__PURE__*/ (function () { + function WindowOperator(closingSelector) { + this.closingSelector = closingSelector; } - - return arg; -} - -module.exports.command = escapeCommand; -module.exports.argument = escapeArgument; + WindowOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new WindowSubscriber(subscriber, this.closingSelector)); + }; + return WindowOperator; +}()); +var WindowSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](WindowSubscriber, _super); + function WindowSubscriber(destination, closingSelector) { + var _this = _super.call(this, destination) || this; + _this.destination = destination; + _this.closingSelector = closingSelector; + _this.openWindow(); + return _this; + } + WindowSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { + this.openWindow(innerSub); + }; + WindowSubscriber.prototype.notifyError = function (error, innerSub) { + this._error(error); + }; + WindowSubscriber.prototype.notifyComplete = function (innerSub) { + this.openWindow(innerSub); + }; + WindowSubscriber.prototype._next = function (value) { + this.window.next(value); + }; + WindowSubscriber.prototype._error = function (err) { + this.window.error(err); + this.destination.error(err); + this.unsubscribeClosingNotification(); + }; + WindowSubscriber.prototype._complete = function () { + this.window.complete(); + this.destination.complete(); + this.unsubscribeClosingNotification(); + }; + WindowSubscriber.prototype.unsubscribeClosingNotification = function () { + if (this.closingNotification) { + this.closingNotification.unsubscribe(); + } + }; + WindowSubscriber.prototype.openWindow = function (innerSub) { + if (innerSub === void 0) { + innerSub = null; + } + if (innerSub) { + this.remove(innerSub); + innerSub.unsubscribe(); + } + var prevWindow = this.window; + if (prevWindow) { + prevWindow.complete(); + } + var window = this.window = new _Subject__WEBPACK_IMPORTED_MODULE_1__["Subject"](); + this.destination.next(window); + var closingNotifier; + try { + var closingSelector = this.closingSelector; + closingNotifier = closingSelector(); + } + catch (e) { + this.destination.error(e); + this.window.error(e); + return; + } + this.add(this.closingNotification = Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_3__["subscribeToResult"])(this, closingNotifier)); + }; + return WindowSubscriber; +}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_2__["OuterSubscriber"])); +//# sourceMappingURL=windowWhen.js.map /***/ }), -/* 362 */ -/***/ (function(module, exports, __webpack_require__) { +/* 365 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "withLatestFrom", function() { return withLatestFrom; }); +/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); +/* harmony import */ var _OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(229); +/* harmony import */ var _util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(230); +/** PURE_IMPORTS_START tslib,_OuterSubscriber,_util_subscribeToResult PURE_IMPORTS_END */ -const fs = __webpack_require__(23); -const shebangCommand = __webpack_require__(363); - -function readShebang(command) { - // Read the first 150 bytes from the file - const size = 150; - const buffer = Buffer.alloc(size); - let fd; - - try { - fd = fs.openSync(command, 'r'); - fs.readSync(fd, buffer, 0, size, 0); - fs.closeSync(fd); - } catch (e) { /* Empty */ } - - // Attempt to extract shebang (null is returned if not a shebang) - return shebangCommand(buffer.toString()); +function withLatestFrom() { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + return function (source) { + var project; + if (typeof args[args.length - 1] === 'function') { + project = args.pop(); + } + var observables = args; + return source.lift(new WithLatestFromOperator(observables, project)); + }; } - -module.exports = readShebang; +var WithLatestFromOperator = /*@__PURE__*/ (function () { + function WithLatestFromOperator(observables, project) { + this.observables = observables; + this.project = project; + } + WithLatestFromOperator.prototype.call = function (subscriber, source) { + return source.subscribe(new WithLatestFromSubscriber(subscriber, this.observables, this.project)); + }; + return WithLatestFromOperator; +}()); +var WithLatestFromSubscriber = /*@__PURE__*/ (function (_super) { + tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](WithLatestFromSubscriber, _super); + function WithLatestFromSubscriber(destination, observables, project) { + var _this = _super.call(this, destination) || this; + _this.observables = observables; + _this.project = project; + _this.toRespond = []; + var len = observables.length; + _this.values = new Array(len); + for (var i = 0; i < len; i++) { + _this.toRespond.push(i); + } + for (var i = 0; i < len; i++) { + var observable = observables[i]; + _this.add(Object(_util_subscribeToResult__WEBPACK_IMPORTED_MODULE_2__["subscribeToResult"])(_this, observable, observable, i)); + } + return _this; + } + WithLatestFromSubscriber.prototype.notifyNext = function (outerValue, innerValue, outerIndex, innerIndex, innerSub) { + this.values[outerIndex] = innerValue; + var toRespond = this.toRespond; + if (toRespond.length > 0) { + var found = toRespond.indexOf(outerIndex); + if (found !== -1) { + toRespond.splice(found, 1); + } + } + }; + WithLatestFromSubscriber.prototype.notifyComplete = function () { + }; + WithLatestFromSubscriber.prototype._next = function (value) { + if (this.toRespond.length === 0) { + var args = [value].concat(this.values); + if (this.project) { + this._tryProject(args); + } + else { + this.destination.next(args); + } + } + }; + WithLatestFromSubscriber.prototype._tryProject = function (args) { + var result; + try { + result = this.project.apply(this, args); + } + catch (err) { + this.destination.error(err); + return; + } + this.destination.next(result); + }; + return WithLatestFromSubscriber; +}(_OuterSubscriber__WEBPACK_IMPORTED_MODULE_1__["OuterSubscriber"])); +//# sourceMappingURL=withLatestFrom.js.map /***/ }), -/* 363 */ -/***/ (function(module, exports, __webpack_require__) { +/* 366 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "zip", function() { return zip; }); +/* harmony import */ var _observable_zip__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(269); +/** PURE_IMPORTS_START _observable_zip PURE_IMPORTS_END */ -const shebangRegex = __webpack_require__(364); - -module.exports = (string = '') => { - const match = string.match(shebangRegex); - - if (!match) { - return null; - } - - const [path, argument] = match[0].replace(/#! ?/, '').split(' '); - const binary = path.split('/').pop(); - - if (binary === 'env') { - return argument; - } - - return argument ? `${binary} ${argument}` : binary; -}; +function zip() { + var observables = []; + for (var _i = 0; _i < arguments.length; _i++) { + observables[_i] = arguments[_i]; + } + return function zipOperatorFunction(source) { + return source.lift.call(_observable_zip__WEBPACK_IMPORTED_MODULE_0__["zip"].apply(void 0, [source].concat(observables))); + }; +} +//# sourceMappingURL=zip.js.map /***/ }), -/* 364 */ -/***/ (function(module, exports, __webpack_require__) { +/* 367 */ +/***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; +__webpack_require__.r(__webpack_exports__); +/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "zipAll", function() { return zipAll; }); +/* harmony import */ var _observable_zip__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(269); +/** PURE_IMPORTS_START _observable_zip PURE_IMPORTS_END */ -module.exports = /^#!(.*)/; +function zipAll(project) { + return function (source) { return source.lift(new _observable_zip__WEBPACK_IMPORTED_MODULE_0__["ZipOperator"](project)); }; +} +//# sourceMappingURL=zipAll.js.map /***/ }), -/* 365 */ +/* 368 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const isWin = process.platform === 'win32'; - -function notFoundError(original, syscall) { - return Object.assign(new Error(`${syscall} ${original.command} ENOENT`), { - code: 'ENOENT', - errno: 'ENOENT', - syscall: `${syscall} ${original.command}`, - path: original.command, - spawnargs: original.args, - }); -} - -function hookChildProcess(cp, parsed) { - if (!isWin) { - return; - } +const callbacks = new Set(); +let called = false; - const originalEmit = cp.emit; +function exit(exit, signal) { + if (called) { + return; + } - cp.emit = function (name, arg1) { - // If emitting "exit" event and exit code is 1, we need to check if - // the command exists and emit an "error" instead - // See https://github.com/IndigoUnited/node-cross-spawn/issues/16 - if (name === 'exit') { - const err = verifyENOENT(arg1, parsed, 'spawn'); + called = true; - if (err) { - return originalEmit.call(cp, 'error', err); - } - } + for (const callback of callbacks) { + callback(); + } - return originalEmit.apply(cp, arguments); // eslint-disable-line prefer-rest-params - }; + if (exit === true) { + process.exit(128 + signal); // eslint-disable-line unicorn/no-process-exit + } } -function verifyENOENT(status, parsed) { - if (isWin && status === 1 && !parsed.file) { - return notFoundError(parsed.original, 'spawn'); - } - - return null; -} +module.exports = callback => { + callbacks.add(callback); -function verifyENOENTSync(status, parsed) { - if (isWin && status === 1 && !parsed.file) { - return notFoundError(parsed.original, 'spawnSync'); - } + if (callbacks.size === 1) { + process.once('exit', exit); + process.once('SIGINT', exit.bind(null, true, 2)); + process.once('SIGTERM', exit.bind(null, true, 15)); - return null; -} + // PM2 Cluster shutdown message. Caught to support async handlers with pm2, needed because + // explicitly calling process.exit() doesn't trigger the beforeExit event, and the exit + // event cannot support async handlers, since the event loop is never called after it. + process.on('message', message => { + if (message === 'shutdown') { + exit(true, -128); + } + }); + } -module.exports = { - hookChildProcess, - verifyENOENT, - verifyENOENTSync, - notFoundError, + return () => { + callbacks.delete(callback); + }; }; /***/ }), -/* 366 */ +/* 369 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; +/* + * 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. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const $isCliError = Symbol('isCliError'); +function createCliError(message) { + const error = new Error(message); + error[$isCliError] = true; + return error; +} +exports.createCliError = createCliError; +function isCliError(error) { + return error && !!error[$isCliError]; +} +exports.isCliError = isCliError; -module.exports = input => { - const LF = typeof input === 'string' ? '\n' : '\n'.charCodeAt(); - const CR = typeof input === 'string' ? '\r' : '\r'.charCodeAt(); - if (input[input.length - 1] === LF) { - input = input.slice(0, input.length - 1); - } +/***/ }), +/* 370 */ +/***/ (function(module, exports, __webpack_require__) { - if (input[input.length - 1] === CR) { - input = input.slice(0, input.length - 1); - } +"use strict"; - return input; -}; +/* + * 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. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +const tslib_1 = __webpack_require__(36); +const execa_1 = tslib_1.__importDefault(__webpack_require__(371)); +const fs_1 = __webpack_require__(23); +const Rx = tslib_1.__importStar(__webpack_require__(169)); +const operators_1 = __webpack_require__(270); +const chalk_1 = tslib_1.__importDefault(__webpack_require__(2)); +const tree_kill_1 = tslib_1.__importDefault(__webpack_require__(411)); +const util_1 = __webpack_require__(29); +const treeKillAsync = util_1.promisify((...args) => tree_kill_1.default(...args)); +const observe_lines_1 = __webpack_require__(412); +const errors_1 = __webpack_require__(369); +const SECOND = 1000; +const STOP_TIMEOUT = 30 * SECOND; +async function withTimeout(attempt, ms, onTimeout) { + const TIMEOUT = Symbol('timeout'); + try { + await Promise.race([ + attempt(), + new Promise((_, reject) => setTimeout(() => reject(TIMEOUT), ms)), + ]); + } + catch (error) { + if (error === TIMEOUT) { + await onTimeout(); + } + else { + throw error; + } + } +} +function startProc(name, options, log) { + const { cmd, args, cwd, env, stdin } = options; + log.info('[%s] > %s', name, cmd, args.join(' ')); + // spawn fails with ENOENT when either the + // cmd or cwd don't exist, so we check for the cwd + // ahead of time so that the error is less ambiguous + try { + if (!fs_1.statSync(cwd).isDirectory()) { + throw new Error(`cwd "${cwd}" exists but is not a directory`); + } + } + catch (err) { + if (err.code === 'ENOENT') { + throw new Error(`cwd "${cwd}" does not exist`); + } + } + const childProcess = execa_1.default(cmd, args, { + cwd, + env, + stdio: ['pipe', 'pipe', 'pipe'], + preferLocal: true, + }); + if (stdin) { + childProcess.stdin.end(stdin, 'utf8'); + } + else { + childProcess.stdin.end(); + } + let stopCalled = false; + const outcome$ = Rx.race( + // observe first exit event + Rx.fromEvent(childProcess, 'exit').pipe(operators_1.take(1), operators_1.map(([code]) => { + if (stopCalled) { + return null; + } + // JVM exits with 143 on SIGTERM and 130 on SIGINT, dont' treat then as errors + if (code > 0 && !(code === 143 || code === 130)) { + throw errors_1.createCliError(`[${name}] exited with code ${code}`); + } + return code; + })), + // observe first error event + Rx.fromEvent(childProcess, 'error').pipe(operators_1.take(1), operators_1.mergeMap(err => Rx.throwError(err)))).pipe(operators_1.share()); + const lines$ = Rx.merge(observe_lines_1.observeLines(childProcess.stdout), observe_lines_1.observeLines(childProcess.stderr)).pipe(operators_1.tap(line => log.write(` ${chalk_1.default.gray('proc')} [${chalk_1.default.gray(name)}] ${line}`)), operators_1.share()); + const outcomePromise = Rx.merge(lines$.pipe(operators_1.ignoreElements()), outcome$).toPromise(); + async function stop(signal) { + if (stopCalled) { + return; + } + stopCalled = true; + await withTimeout(async () => { + log.debug(`Sending "${signal}" to proc "${name}"`); + await treeKillAsync(childProcess.pid, signal); + await outcomePromise; + }, STOP_TIMEOUT, async () => { + log.warning(`Proc "${name}" was sent "${signal}" didn't emit the "exit" or "error" events after ${STOP_TIMEOUT} ms, sending SIGKILL`); + await treeKillAsync(childProcess.pid, 'SIGKILL'); + }); + await withTimeout(async () => { + try { + await outcomePromise; + } + catch (error) { + // ignore + } + }, STOP_TIMEOUT, async () => { + throw new Error(`Proc "${name}" was stopped but never emitted either the "exit" or "error" event after ${STOP_TIMEOUT} ms`); + }); + } + return { + name, + lines$, + outcome$, + outcomePromise, + stop, + }; +} +exports.startProc = startProc; /***/ }), -/* 367 */ +/* 371 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(16); -const pathKey = __webpack_require__(360); +const childProcess = __webpack_require__(372); +const crossSpawn = __webpack_require__(373); +const stripFinalNewline = __webpack_require__(386); +const npmRunPath = __webpack_require__(387); +const onetime = __webpack_require__(388); +const makeError = __webpack_require__(390); +const normalizeStdio = __webpack_require__(395); +const {spawnedKill, spawnedCancel, setupTimeout, setExitHandler} = __webpack_require__(396); +const {handleInput, getSpawnedResult, makeAllStream, validateInputSync} = __webpack_require__(400); +const {mergePromise, getSpawnedPromise} = __webpack_require__(409); +const {joinCommand, parseCommand} = __webpack_require__(410); -const npmRunPath = options => { - options = { - cwd: process.cwd(), - path: process.env[pathKey()], - execPath: process.execPath, - ...options - }; +const DEFAULT_MAX_BUFFER = 1000 * 1000 * 100; - let previous; - let cwdPath = path.resolve(options.cwd); - const result = []; +const getEnv = ({env: envOption, extendEnv, preferLocal, localDir, execPath}) => { + const env = extendEnv ? {...process.env, ...envOption} : envOption; - while (previous !== cwdPath) { - result.push(path.join(cwdPath, 'node_modules/.bin')); - previous = cwdPath; - cwdPath = path.resolve(cwdPath, '..'); + if (preferLocal) { + return npmRunPath.env({env, cwd: localDir, execPath}); } - // Ensure the running `node` binary is used - const execPathDir = path.resolve(options.cwd, options.execPath, '..'); - result.unshift(execPathDir); - - return result.concat(options.path).join(path.delimiter); + return env; }; -module.exports = npmRunPath; -// TODO: Remove this for the next major release -module.exports.default = npmRunPath; +const handleArgs = (file, args, options = {}) => { + const parsed = crossSpawn._parse(file, args, options); + file = parsed.command; + args = parsed.args; + options = parsed.options; -module.exports.env = options => { options = { - env: process.env, + maxBuffer: DEFAULT_MAX_BUFFER, + buffer: true, + stripFinalNewline: true, + extendEnv: true, + preferLocal: false, + localDir: options.cwd || process.cwd(), + execPath: process.execPath, + encoding: 'utf8', + reject: true, + cleanup: true, + all: false, + windowsHide: true, ...options }; - const env = {...options.env}; - const path = pathKey({env}); + options.env = getEnv(options); - options.path = env[path]; - env[path] = module.exports(options); + options.stdio = normalizeStdio(options); - return env; -}; + if (process.platform === 'win32' && path.basename(file, '.exe') === 'cmd') { + // #116 + args.unshift('/q'); + } + return {file, args, options, parsed}; +}; -/***/ }), -/* 368 */ -/***/ (function(module, exports, __webpack_require__) { +const handleOutput = (options, value, error) => { + if (typeof value !== 'string' && !Buffer.isBuffer(value)) { + // When `execa.sync()` errors, we normalize it to '' to mimic `execa()` + return error === undefined ? undefined : ''; + } -"use strict"; + if (options.stripFinalNewline) { + return stripFinalNewline(value); + } -const mimicFn = __webpack_require__(369); + return value; +}; -const calledFunctions = new WeakMap(); +const execa = (file, args, options) => { + const parsed = handleArgs(file, args, options); + const command = joinCommand(file, args); -const oneTime = (fn, options = {}) => { - if (typeof fn !== 'function') { - throw new TypeError('Expected a function'); + let spawned; + try { + spawned = childProcess.spawn(parsed.file, parsed.args, parsed.options); + } catch (error) { + // Ensure the returned error is always both a promise and a child process + const dummySpawned = new childProcess.ChildProcess(); + const errorPromise = Promise.reject(makeError({ + error, + stdout: '', + stderr: '', + all: '', + command, + parsed, + timedOut: false, + isCanceled: false, + killed: false + })); + return mergePromise(dummySpawned, errorPromise); } - let ret; - let isCalled = false; - let callCount = 0; - const functionName = fn.displayName || fn.name || ''; + const spawnedPromise = getSpawnedPromise(spawned); + const timedPromise = setupTimeout(spawned, parsed.options, spawnedPromise); + const processDone = setExitHandler(spawned, parsed.options, timedPromise); - const onetime = function (...args) { - calledFunctions.set(onetime, ++callCount); + const context = {isCanceled: false}; - if (isCalled) { - if (options.throw === true) { - throw new Error(`Function \`${functionName}\` can only be called once`); - } + spawned.kill = spawnedKill.bind(null, spawned.kill.bind(spawned)); + spawned.cancel = spawnedCancel.bind(null, spawned, context); - return ret; - } + const handlePromise = async () => { + const [{error, exitCode, signal, timedOut}, stdoutResult, stderrResult, allResult] = await getSpawnedResult(spawned, parsed.options, processDone); + const stdout = handleOutput(parsed.options, stdoutResult); + const stderr = handleOutput(parsed.options, stderrResult); + const all = handleOutput(parsed.options, allResult); - isCalled = true; - ret = fn.apply(this, args); - fn = null; + if (error || exitCode !== 0 || signal !== null) { + const returnedError = makeError({ + error, + exitCode, + signal, + stdout, + stderr, + all, + command, + parsed, + timedOut, + isCanceled: context.isCanceled, + killed: spawned.killed + }); - return ret; + if (!parsed.options.reject) { + return returnedError; + } + + throw returnedError; + } + + return { + command, + exitCode: 0, + stdout, + stderr, + all, + failed: false, + timedOut: false, + isCanceled: false, + killed: false + }; }; - mimicFn(onetime, fn); - calledFunctions.set(onetime, callCount); + const handlePromiseOnce = onetime(handlePromise); - return onetime; -}; + crossSpawn._enoent.hookChildProcess(spawned, parsed.parsed); -module.exports = oneTime; -// TODO: Remove this for the next major release -module.exports.default = oneTime; + handleInput(spawned, parsed.options.input); -module.exports.callCount = fn => { - if (!calledFunctions.has(fn)) { - throw new Error(`The given function \`${fn.name}\` is not wrapped by the \`onetime\` package`); - } + spawned.all = makeAllStream(spawned, parsed.options); - return calledFunctions.get(fn); + return mergePromise(spawned, handlePromiseOnce); }; +module.exports = execa; -/***/ }), -/* 369 */ -/***/ (function(module, exports, __webpack_require__) { +module.exports.sync = (file, args, options) => { + const parsed = handleArgs(file, args, options); + const command = joinCommand(file, args); -"use strict"; + validateInputSync(parsed.options); + + let result; + try { + result = childProcess.spawnSync(parsed.file, parsed.args, parsed.options); + } catch (error) { + throw makeError({ + error, + stdout: '', + stderr: '', + all: '', + command, + parsed, + timedOut: false, + isCanceled: false, + killed: false + }); + } + const stdout = handleOutput(parsed.options, result.stdout, result.error); + const stderr = handleOutput(parsed.options, result.stderr, result.error); -const mimicFn = (to, from) => { - for (const prop of Reflect.ownKeys(from)) { - Object.defineProperty(to, prop, Object.getOwnPropertyDescriptor(from, prop)); + if (result.error || result.status !== 0 || result.signal !== null) { + const error = makeError({ + stdout, + stderr, + error: result.error, + signal: result.signal, + exitCode: result.status, + command, + parsed, + timedOut: result.error && result.error.code === 'ETIMEDOUT', + isCanceled: false, + killed: result.signal !== null + }); + + if (!parsed.options.reject) { + return error; + } + + throw error; } - return to; + return { + command, + exitCode: 0, + stdout, + stderr, + failed: false, + timedOut: false, + isCanceled: false, + killed: false + }; }; -module.exports = mimicFn; -// TODO: Remove this for the next major release -module.exports.default = mimicFn; +module.exports.command = (command, options) => { + const [file, ...args] = parseCommand(command); + return execa(file, args, options); +}; +module.exports.commandSync = (command, options) => { + const [file, ...args] = parseCommand(command); + return execa.sync(file, args, options); +}; -/***/ }), -/* 370 */ -/***/ (function(module, exports, __webpack_require__) { +module.exports.node = (scriptPath, args, options = {}) => { + if (args && !Array.isArray(args) && typeof args === 'object') { + options = args; + args = []; + } -"use strict"; + const stdio = normalizeStdio.node(options); -const {signalsByName} = __webpack_require__(371); + const {nodePath = process.execPath, nodeOptions = process.execArgv} = options; -const getErrorPrefix = ({timedOut, timeout, errorCode, signal, signalDescription, exitCode, isCanceled}) => { - if (timedOut) { - return `timed out after ${timeout} milliseconds`; - } + return execa( + nodePath, + [ + ...nodeOptions, + scriptPath, + ...(Array.isArray(args) ? args : []) + ], + { + ...options, + stdin: undefined, + stdout: undefined, + stderr: undefined, + stdio, + shell: false + } + ); +}; - if (isCanceled) { - return 'was canceled'; - } - if (errorCode !== undefined) { - return `failed with ${errorCode}`; - } +/***/ }), +/* 372 */ +/***/ (function(module, exports) { - if (signal !== undefined) { - return `was killed with ${signal} (${signalDescription})`; - } +module.exports = require("child_process"); - if (exitCode !== undefined) { - return `failed with exit code ${exitCode}`; - } +/***/ }), +/* 373 */ +/***/ (function(module, exports, __webpack_require__) { - return 'failed'; -}; +"use strict"; -const makeError = ({ - stdout, - stderr, - all, - error, - signal, - exitCode, - command, - timedOut, - isCanceled, - killed, - parsed: {options: {timeout}} -}) => { - // `signal` and `exitCode` emitted on `spawned.on('exit')` event can be `null`. - // We normalize them to `undefined` - exitCode = exitCode === null ? undefined : exitCode; - signal = signal === null ? undefined : signal; - const signalDescription = signal === undefined ? undefined : signalsByName[signal].description; - const errorCode = error && error.code; +const cp = __webpack_require__(372); +const parse = __webpack_require__(374); +const enoent = __webpack_require__(385); - const prefix = getErrorPrefix({timedOut, timeout, errorCode, signal, signalDescription, exitCode, isCanceled}); - const execaMessage = `Command ${prefix}: ${command}`; - const shortMessage = error instanceof Error ? `${execaMessage}\n${error.message}` : execaMessage; - const message = [shortMessage, stderr, stdout].filter(Boolean).join('\n'); +function spawn(command, args, options) { + // Parse the arguments + const parsed = parse(command, args, options); - if (error instanceof Error) { - error.originalMessage = error.message; - error.message = message; - } else { - error = new Error(message); - } + // Spawn the child process + const spawned = cp.spawn(parsed.command, parsed.args, parsed.options); - error.shortMessage = shortMessage; - error.command = command; - error.exitCode = exitCode; - error.signal = signal; - error.signalDescription = signalDescription; - error.stdout = stdout; - error.stderr = stderr; + // Hook into child process "exit" event to emit an error if the command + // does not exists, see: https://github.com/IndigoUnited/node-cross-spawn/issues/16 + enoent.hookChildProcess(spawned, parsed); - if (all !== undefined) { - error.all = all; - } + return spawned; +} - if ('bufferedData' in error) { - delete error.bufferedData; - } +function spawnSync(command, args, options) { + // Parse the arguments + const parsed = parse(command, args, options); - error.failed = true; - error.timedOut = Boolean(timedOut); - error.isCanceled = isCanceled; - error.killed = killed && !timedOut; + // Spawn the child process + const result = cp.spawnSync(parsed.command, parsed.args, parsed.options); - return error; -}; + // Analyze if the command does not exist, see: https://github.com/IndigoUnited/node-cross-spawn/issues/16 + result.error = result.error || enoent.verifyENOENTSync(result.status, parsed); -module.exports = makeError; + return result; +} + +module.exports = spawn; +module.exports.spawn = spawn; +module.exports.sync = spawnSync; + +module.exports._parse = parse; +module.exports._enoent = enoent; /***/ }), -/* 371 */ +/* 374 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -Object.defineProperty(exports,"__esModule",{value:true});exports.signalsByNumber=exports.signalsByName=void 0;var _os=__webpack_require__(11); -var _signals=__webpack_require__(372); -var _realtime=__webpack_require__(374); +const path = __webpack_require__(16); +const resolveCommand = __webpack_require__(375); +const escape = __webpack_require__(381); +const readShebang = __webpack_require__(382); +const isWin = process.platform === 'win32'; +const isExecutableRegExp = /\.(?:com|exe)$/i; +const isCmdShimRegExp = /node_modules[\\/].bin[\\/][^\\/]+\.cmd$/i; -const getSignalsByName=function(){ -const signals=(0,_signals.getSignals)(); -return signals.reduce(getSignalByName,{}); -}; +function detectShebang(parsed) { + parsed.file = resolveCommand(parsed); -const getSignalByName=function( -signalByNameMemo, -{name,number,description,supported,action,forced,standard}) -{ -return{ -...signalByNameMemo, -[name]:{name,number,description,supported,action,forced,standard}}; + const shebang = parsed.file && readShebang(parsed.file); -}; + if (shebang) { + parsed.args.unshift(parsed.file); + parsed.command = shebang; -const signalsByName=getSignalsByName();exports.signalsByName=signalsByName; + return resolveCommand(parsed); + } + return parsed.file; +} +function parseNonShell(parsed) { + if (!isWin) { + return parsed; + } + // Detect & add support for shebangs + const commandFile = detectShebang(parsed); -const getSignalsByNumber=function(){ -const signals=(0,_signals.getSignals)(); -const length=_realtime.SIGRTMAX+1; -const signalsA=Array.from({length},(value,number)=> -getSignalByNumber(number,signals)); + // We don't need a shell if the command filename is an executable + const needsShell = !isExecutableRegExp.test(commandFile); -return Object.assign({},...signalsA); -}; + // If a shell is required, use cmd.exe and take care of escaping everything correctly + // Note that `forceShell` is an hidden option used only in tests + if (parsed.options.forceShell || needsShell) { + // Need to double escape meta chars if the command is a cmd-shim located in `node_modules/.bin/` + // The cmd-shim simply calls execute the package bin file with NodeJS, proxying any argument + // Because the escape of metachars with ^ gets interpreted when the cmd.exe is first called, + // we need to double escape them + const needsDoubleEscapeMetaChars = isCmdShimRegExp.test(commandFile); -const getSignalByNumber=function(number,signals){ -const signal=findSignalByNumber(number,signals); + // Normalize posix paths into OS compatible paths (e.g.: foo/bar -> foo\bar) + // This is necessary otherwise it will always fail with ENOENT in those cases + parsed.command = path.normalize(parsed.command); -if(signal===undefined){ -return{}; -} + // Escape command & arguments + parsed.command = escape.command(parsed.command); + parsed.args = parsed.args.map((arg) => escape.argument(arg, needsDoubleEscapeMetaChars)); -const{name,description,supported,action,forced,standard}=signal; -return{ -[number]:{ -name, -number, -description, -supported, -action, -forced, -standard}}; + const shellCommand = [parsed.command].concat(parsed.args).join(' '); + parsed.args = ['/d', '/s', '/c', `"${shellCommand}"`]; + parsed.command = process.env.comspec || 'cmd.exe'; + parsed.options.windowsVerbatimArguments = true; // Tell node's spawn that the arguments are already escaped + } -}; + return parsed; +} +function parse(command, args, options) { + // Normalize arguments, similar to nodejs + if (args && !Array.isArray(args)) { + options = args; + args = null; + } + args = args ? args.slice(0) : []; // Clone array to avoid changing the original + options = Object.assign({}, options); // Clone object to avoid changing the original -const findSignalByNumber=function(number,signals){ -const signal=signals.find(({name})=>_os.constants.signals[name]===number); + // Build our parsed object + const parsed = { + command, + args, + options, + file: undefined, + original: { + command, + args, + }, + }; -if(signal!==undefined){ -return signal; + // Delegate further parsing to shell or non-shell + return options.shell ? parsed : parseNonShell(parsed); } -return signals.find(signalA=>signalA.number===number); -}; +module.exports = parse; -const signalsByNumber=getSignalsByNumber();exports.signalsByNumber=signalsByNumber; -//# sourceMappingURL=main.js.map /***/ }), -/* 372 */ +/* 375 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -Object.defineProperty(exports,"__esModule",{value:true});exports.getSignals=void 0;var _os=__webpack_require__(11); -var _core=__webpack_require__(373); -var _realtime=__webpack_require__(374); +const path = __webpack_require__(16); +const which = __webpack_require__(376); +const pathKey = __webpack_require__(380)(); +function resolveCommandAttempt(parsed, withoutPathExt) { + const cwd = process.cwd(); + const hasCustomCwd = parsed.options.cwd != null; + // Worker threads do not have process.chdir() + const shouldSwitchCwd = hasCustomCwd && process.chdir !== undefined; -const getSignals=function(){ -const realtimeSignals=(0,_realtime.getRealtimeSignals)(); -const signals=[..._core.SIGNALS,...realtimeSignals].map(normalizeSignal); -return signals; -};exports.getSignals=getSignals; + // If a custom `cwd` was specified, we need to change the process cwd + // because `which` will do stat calls but does not support a custom cwd + if (shouldSwitchCwd) { + try { + process.chdir(parsed.options.cwd); + } catch (err) { + /* Empty */ + } + } + let resolved; + try { + resolved = which.sync(parsed.command, { + path: (parsed.options.env || process.env)[pathKey], + pathExt: withoutPathExt ? path.delimiter : undefined, + }); + } catch (e) { + /* Empty */ + } finally { + if (shouldSwitchCwd) { + process.chdir(cwd); + } + } + // If we successfully resolved, ensure that an absolute path is returned + // Note that when a custom `cwd` was used, we need to resolve to an absolute path based on it + if (resolved) { + resolved = path.resolve(hasCustomCwd ? parsed.options.cwd : '', resolved); + } + return resolved; +} +function resolveCommand(parsed) { + return resolveCommandAttempt(parsed) || resolveCommandAttempt(parsed, true); +} +module.exports = resolveCommand; -const normalizeSignal=function({ -name, -number:defaultNumber, -description, -action, -forced=false, -standard}) -{ -const{ -signals:{[name]:constantSignal}}= -_os.constants; -const supported=constantSignal!==undefined; -const number=supported?constantSignal:defaultNumber; -return{name,number,description,supported,action,forced,standard}; -}; -//# sourceMappingURL=signals.js.map /***/ }), -/* 373 */ +/* 376 */ /***/ (function(module, exports, __webpack_require__) { -"use strict"; -Object.defineProperty(exports,"__esModule",{value:true});exports.SIGNALS=void 0; - -const SIGNALS=[ -{ -name:"SIGHUP", -number:1, -action:"terminate", -description:"Terminal closed", -standard:"posix"}, +const isWindows = process.platform === 'win32' || + process.env.OSTYPE === 'cygwin' || + process.env.OSTYPE === 'msys' -{ -name:"SIGINT", -number:2, -action:"terminate", -description:"User interruption with CTRL-C", -standard:"ansi"}, +const path = __webpack_require__(16) +const COLON = isWindows ? ';' : ':' +const isexe = __webpack_require__(377) -{ -name:"SIGQUIT", -number:3, -action:"core", -description:"User interruption with CTRL-\\", -standard:"posix"}, +const getNotFoundError = (cmd) => + Object.assign(new Error(`not found: ${cmd}`), { code: 'ENOENT' }) -{ -name:"SIGILL", -number:4, -action:"core", -description:"Invalid machine instruction", -standard:"ansi"}, +const getPathInfo = (cmd, opt) => { + const colon = opt.colon || COLON -{ -name:"SIGTRAP", -number:5, -action:"core", -description:"Debugger breakpoint", -standard:"posix"}, + // If it has a slash, then we don't bother searching the pathenv. + // just check the file itself, and that's it. + const pathEnv = cmd.match(/\//) || isWindows && cmd.match(/\\/) ? [''] + : ( + [ + // windows always checks the cwd first + ...(isWindows ? [process.cwd()] : []), + ...(opt.path || process.env.PATH || + /* istanbul ignore next: very unusual */ '').split(colon), + ] + ) + const pathExtExe = isWindows + ? opt.pathExt || process.env.PATHEXT || '.EXE;.CMD;.BAT;.COM' + : '' + const pathExt = isWindows ? pathExtExe.split(colon) : [''] -{ -name:"SIGABRT", -number:6, -action:"core", -description:"Aborted", -standard:"ansi"}, + if (isWindows) { + if (cmd.indexOf('.') !== -1 && pathExt[0] !== '') + pathExt.unshift('') + } -{ -name:"SIGIOT", -number:6, -action:"core", -description:"Aborted", -standard:"bsd"}, + return { + pathEnv, + pathExt, + pathExtExe, + } +} -{ -name:"SIGBUS", -number:7, -action:"core", -description: -"Bus error due to misaligned, non-existing address or paging error", -standard:"bsd"}, +const which = (cmd, opt, cb) => { + if (typeof opt === 'function') { + cb = opt + opt = {} + } + if (!opt) + opt = {} -{ -name:"SIGEMT", -number:7, -action:"terminate", -description:"Command should be emulated but is not implemented", -standard:"other"}, + const { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt) + const found = [] -{ -name:"SIGFPE", -number:8, -action:"core", -description:"Floating point arithmetic error", -standard:"ansi"}, + const step = i => new Promise((resolve, reject) => { + if (i === pathEnv.length) + return opt.all && found.length ? resolve(found) + : reject(getNotFoundError(cmd)) -{ -name:"SIGKILL", -number:9, -action:"terminate", -description:"Forced termination", -standard:"posix", -forced:true}, + const ppRaw = pathEnv[i] + const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw -{ -name:"SIGUSR1", -number:10, -action:"terminate", -description:"Application-specific signal", -standard:"posix"}, + const pCmd = path.join(pathPart, cmd) + const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd + : pCmd -{ -name:"SIGSEGV", -number:11, -action:"core", -description:"Segmentation fault", -standard:"ansi"}, + resolve(subStep(p, i, 0)) + }) -{ -name:"SIGUSR2", -number:12, -action:"terminate", -description:"Application-specific signal", -standard:"posix"}, + const subStep = (p, i, ii) => new Promise((resolve, reject) => { + if (ii === pathExt.length) + return resolve(step(i + 1)) + const ext = pathExt[ii] + isexe(p + ext, { pathExt: pathExtExe }, (er, is) => { + if (!er && is) { + if (opt.all) + found.push(p + ext) + else + return resolve(p + ext) + } + return resolve(subStep(p, i, ii + 1)) + }) + }) -{ -name:"SIGPIPE", -number:13, -action:"terminate", -description:"Broken pipe or socket", -standard:"posix"}, + return cb ? step(0).then(res => cb(null, res), cb) : step(0) +} -{ -name:"SIGALRM", -number:14, -action:"terminate", -description:"Timeout or timer", -standard:"posix"}, +const whichSync = (cmd, opt) => { + opt = opt || {} -{ -name:"SIGTERM", -number:15, -action:"terminate", -description:"Termination", -standard:"ansi"}, + const { pathEnv, pathExt, pathExtExe } = getPathInfo(cmd, opt) + const found = [] -{ -name:"SIGSTKFLT", -number:16, -action:"terminate", -description:"Stack is empty or overflowed", -standard:"other"}, + for (let i = 0; i < pathEnv.length; i ++) { + const ppRaw = pathEnv[i] + const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw -{ -name:"SIGCHLD", -number:17, -action:"ignore", -description:"Child process terminated, paused or unpaused", -standard:"posix"}, + const pCmd = path.join(pathPart, cmd) + const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd + : pCmd -{ -name:"SIGCLD", -number:17, -action:"ignore", -description:"Child process terminated, paused or unpaused", -standard:"other"}, + for (let j = 0; j < pathExt.length; j ++) { + const cur = p + pathExt[j] + try { + const is = isexe.sync(cur, { pathExt: pathExtExe }) + if (is) { + if (opt.all) + found.push(cur) + else + return cur + } + } catch (ex) {} + } + } -{ -name:"SIGCONT", -number:18, -action:"unpause", -description:"Unpaused", -standard:"posix", -forced:true}, + if (opt.all && found.length) + return found -{ -name:"SIGSTOP", -number:19, -action:"pause", -description:"Paused", -standard:"posix", -forced:true}, + if (opt.nothrow) + return null -{ -name:"SIGTSTP", -number:20, -action:"pause", -description:"Paused using CTRL-Z or \"suspend\"", -standard:"posix"}, + throw getNotFoundError(cmd) +} -{ -name:"SIGTTIN", -number:21, -action:"pause", -description:"Background process cannot read terminal input", -standard:"posix"}, +module.exports = which +which.sync = whichSync -{ -name:"SIGBREAK", -number:21, -action:"terminate", -description:"User interruption with CTRL-BREAK", -standard:"other"}, -{ -name:"SIGTTOU", -number:22, -action:"pause", -description:"Background process cannot write to terminal output", -standard:"posix"}, +/***/ }), +/* 377 */ +/***/ (function(module, exports, __webpack_require__) { -{ -name:"SIGURG", -number:23, -action:"ignore", -description:"Socket received out-of-band data", -standard:"bsd"}, +var fs = __webpack_require__(23) +var core +if (process.platform === 'win32' || global.TESTING_WINDOWS) { + core = __webpack_require__(378) +} else { + core = __webpack_require__(379) +} -{ -name:"SIGXCPU", -number:24, -action:"core", -description:"Process timed out", -standard:"bsd"}, +module.exports = isexe +isexe.sync = sync -{ -name:"SIGXFSZ", -number:25, -action:"core", -description:"File too big", -standard:"bsd"}, +function isexe (path, options, cb) { + if (typeof options === 'function') { + cb = options + options = {} + } -{ -name:"SIGVTALRM", -number:26, -action:"terminate", -description:"Timeout or timer", -standard:"bsd"}, + if (!cb) { + if (typeof Promise !== 'function') { + throw new TypeError('callback not provided') + } -{ -name:"SIGPROF", -number:27, -action:"terminate", -description:"Timeout or timer", -standard:"bsd"}, + return new Promise(function (resolve, reject) { + isexe(path, options || {}, function (er, is) { + if (er) { + reject(er) + } else { + resolve(is) + } + }) + }) + } -{ -name:"SIGWINCH", -number:28, -action:"ignore", -description:"Terminal window size changed", -standard:"bsd"}, + core(path, options || {}, function (er, is) { + // ignore EACCES because that just means we aren't allowed to run it + if (er) { + if (er.code === 'EACCES' || options && options.ignoreErrors) { + er = null + is = false + } + } + cb(er, is) + }) +} -{ -name:"SIGIO", -number:29, -action:"terminate", -description:"I/O is available", -standard:"other"}, +function sync (path, options) { + // my kingdom for a filtered catch + try { + return core.sync(path, options || {}) + } catch (er) { + if (options && options.ignoreErrors || er.code === 'EACCES') { + return false + } else { + throw er + } + } +} -{ -name:"SIGPOLL", -number:29, -action:"terminate", -description:"Watched event", -standard:"other"}, -{ -name:"SIGINFO", -number:29, -action:"ignore", -description:"Request for process information", -standard:"other"}, +/***/ }), +/* 378 */ +/***/ (function(module, exports, __webpack_require__) { -{ -name:"SIGPWR", -number:30, -action:"terminate", -description:"Device running out of power", -standard:"systemv"}, +module.exports = isexe +isexe.sync = sync -{ -name:"SIGSYS", -number:31, -action:"core", -description:"Invalid system call", -standard:"other"}, +var fs = __webpack_require__(23) -{ -name:"SIGUNUSED", -number:31, -action:"terminate", -description:"Invalid system call", -standard:"other"}];exports.SIGNALS=SIGNALS; -//# sourceMappingURL=core.js.map +function checkPathExt (path, options) { + var pathext = options.pathExt !== undefined ? + options.pathExt : process.env.PATHEXT -/***/ }), -/* 374 */ -/***/ (function(module, exports, __webpack_require__) { + if (!pathext) { + return true + } -"use strict"; -Object.defineProperty(exports,"__esModule",{value:true});exports.SIGRTMAX=exports.getRealtimeSignals=void 0; -const getRealtimeSignals=function(){ -const length=SIGRTMAX-SIGRTMIN+1; -return Array.from({length},getRealtimeSignal); -};exports.getRealtimeSignals=getRealtimeSignals; + pathext = pathext.split(';') + if (pathext.indexOf('') !== -1) { + return true + } + for (var i = 0; i < pathext.length; i++) { + var p = pathext[i].toLowerCase() + if (p && path.substr(-p.length).toLowerCase() === p) { + return true + } + } + return false +} -const getRealtimeSignal=function(value,index){ -return{ -name:`SIGRT${index+1}`, -number:SIGRTMIN+index, -action:"terminate", -description:"Application-specific signal (realtime)", -standard:"posix"}; +function checkStat (stat, path, options) { + if (!stat.isSymbolicLink() && !stat.isFile()) { + return false + } + return checkPathExt(path, options) +} -}; +function isexe (path, options, cb) { + fs.stat(path, function (er, stat) { + cb(er, er ? false : checkStat(stat, path, options)) + }) +} + +function sync (path, options) { + return checkStat(fs.statSync(path), path, options) +} -const SIGRTMIN=34; -const SIGRTMAX=64;exports.SIGRTMAX=SIGRTMAX; -//# sourceMappingURL=realtime.js.map /***/ }), -/* 375 */ +/* 379 */ /***/ (function(module, exports, __webpack_require__) { -"use strict"; +module.exports = isexe +isexe.sync = sync -const aliases = ['stdin', 'stdout', 'stderr']; +var fs = __webpack_require__(23) -const hasAlias = opts => aliases.some(alias => opts[alias] !== undefined); +function isexe (path, options, cb) { + fs.stat(path, function (er, stat) { + cb(er, er ? false : checkStat(stat, options)) + }) +} -const normalizeStdio = opts => { - if (!opts) { - return; - } +function sync (path, options) { + return checkStat(fs.statSync(path), options) +} - const {stdio} = opts; +function checkStat (stat, options) { + return stat.isFile() && checkMode(stat, options) +} - if (stdio === undefined) { - return aliases.map(alias => opts[alias]); - } +function checkMode (stat, options) { + var mod = stat.mode + var uid = stat.uid + var gid = stat.gid - if (hasAlias(opts)) { - throw new Error(`It's not possible to provide \`stdio\` in combination with one of ${aliases.map(alias => `\`${alias}\``).join(', ')}`); - } + var myUid = options.uid !== undefined ? + options.uid : process.getuid && process.getuid() + var myGid = options.gid !== undefined ? + options.gid : process.getgid && process.getgid() - if (typeof stdio === 'string') { - return stdio; - } + var u = parseInt('100', 8) + var g = parseInt('010', 8) + var o = parseInt('001', 8) + var ug = u | g - if (!Array.isArray(stdio)) { - throw new TypeError(`Expected \`stdio\` to be of type \`string\` or \`Array\`, got \`${typeof stdio}\``); - } + var ret = (mod & o) || + (mod & g) && gid === myGid || + (mod & u) && uid === myUid || + (mod & ug) && myUid === 0 - const length = Math.max(stdio.length, aliases.length); - return Array.from({length}, (value, index) => stdio[index]); -}; + return ret +} -module.exports = normalizeStdio; -// `ipc` is pushed unless it is already present -module.exports.node = opts => { - const stdio = normalizeStdio(opts); +/***/ }), +/* 380 */ +/***/ (function(module, exports, __webpack_require__) { - if (stdio === 'ipc') { - return 'ipc'; - } +"use strict"; - if (stdio === undefined || typeof stdio === 'string') { - return [stdio, stdio, stdio, 'ipc']; - } - if (stdio.includes('ipc')) { - return stdio; +const pathKey = (options = {}) => { + const environment = options.env || process.env; + const platform = options.platform || process.platform; + + if (platform !== 'win32') { + return 'PATH'; } - return [...stdio, 'ipc']; + return Object.keys(environment).find(key => key.toUpperCase() === 'PATH') || 'Path'; }; +module.exports = pathKey; +// TODO: Remove this for the next major release +module.exports.default = pathKey; + /***/ }), -/* 376 */ +/* 381 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const os = __webpack_require__(11); -const onExit = __webpack_require__(377); -const DEFAULT_FORCE_KILL_TIMEOUT = 1000 * 5; +// See http://www.robvanderwoude.com/escapechars.php +const metaCharsRegExp = /([()\][%!^"`<>&|;, *?])/g; -// Monkey-patches `childProcess.kill()` to add `forceKillAfterTimeout` behavior -const spawnedKill = (kill, signal = 'SIGTERM', options = {}) => { - const killResult = kill(signal); - setKillTimeout(kill, signal, options, killResult); - return killResult; -}; +function escapeCommand(arg) { + // Escape meta chars + arg = arg.replace(metaCharsRegExp, '^$1'); -const setKillTimeout = (kill, signal, options, killResult) => { - if (!shouldForceKill(signal, options, killResult)) { - return; - } + return arg; +} - const timeout = getForceKillAfterTimeout(options); - const t = setTimeout(() => { - kill('SIGKILL'); - }, timeout); +function escapeArgument(arg, doubleEscapeMetaChars) { + // Convert to string + arg = `${arg}`; - // Guarded because there's no `.unref()` when `execa` is used in the renderer - // process in Electron. This cannot be tested since we don't run tests in - // Electron. - // istanbul ignore else - if (t.unref) { - t.unref(); - } -}; + // Algorithm below is based on https://qntm.org/cmd -const shouldForceKill = (signal, {forceKillAfterTimeout}, killResult) => { - return isSigterm(signal) && forceKillAfterTimeout !== false && killResult; -}; + // Sequence of backslashes followed by a double quote: + // double up all the backslashes and escape the double quote + arg = arg.replace(/(\\*)"/g, '$1$1\\"'); -const isSigterm = signal => { - return signal === os.constants.signals.SIGTERM || - (typeof signal === 'string' && signal.toUpperCase() === 'SIGTERM'); -}; + // Sequence of backslashes followed by the end of the string + // (which will become a double quote later): + // double up all the backslashes + arg = arg.replace(/(\\*)$/, '$1$1'); -const getForceKillAfterTimeout = ({forceKillAfterTimeout = true}) => { - if (forceKillAfterTimeout === true) { - return DEFAULT_FORCE_KILL_TIMEOUT; - } + // All other backslashes occur literally - if (!Number.isInteger(forceKillAfterTimeout) || forceKillAfterTimeout < 0) { - throw new TypeError(`Expected the \`forceKillAfterTimeout\` option to be a non-negative integer, got \`${forceKillAfterTimeout}\` (${typeof forceKillAfterTimeout})`); - } + // Quote the whole thing: + arg = `"${arg}"`; - return forceKillAfterTimeout; -}; + // Escape meta chars + arg = arg.replace(metaCharsRegExp, '^$1'); -// `childProcess.cancel()` -const spawnedCancel = (spawned, context) => { - const killResult = spawned.kill(); + // Double escape meta chars if necessary + if (doubleEscapeMetaChars) { + arg = arg.replace(metaCharsRegExp, '^$1'); + } - if (killResult) { - context.isCanceled = true; - } -}; + return arg; +} -const timeoutKill = (spawned, signal, reject) => { - spawned.kill(signal); - reject(Object.assign(new Error('Timed out'), {timedOut: true, signal})); -}; +module.exports.command = escapeCommand; +module.exports.argument = escapeArgument; -// `timeout` option handling -const setupTimeout = (spawned, {timeout, killSignal = 'SIGTERM'}, spawnedPromise) => { - if (timeout === 0 || timeout === undefined) { - return spawnedPromise; - } - if (!Number.isInteger(timeout) || timeout < 0) { - throw new TypeError(`Expected the \`timeout\` option to be a non-negative integer, got \`${timeout}\` (${typeof timeout})`); - } +/***/ }), +/* 382 */ +/***/ (function(module, exports, __webpack_require__) { - let timeoutId; - const timeoutPromise = new Promise((resolve, reject) => { - timeoutId = setTimeout(() => { - timeoutKill(spawned, killSignal, reject); - }, timeout); - }); +"use strict"; - const safeSpawnedPromise = spawnedPromise.finally(() => { - clearTimeout(timeoutId); - }); - return Promise.race([timeoutPromise, safeSpawnedPromise]); -}; +const fs = __webpack_require__(23); +const shebangCommand = __webpack_require__(383); -// `cleanup` option handling -const setExitHandler = async (spawned, {cleanup, detached}, timedPromise) => { - if (!cleanup || detached) { - return timedPromise; - } +function readShebang(command) { + // Read the first 150 bytes from the file + const size = 150; + const buffer = Buffer.alloc(size); - const removeExitHandler = onExit(() => { - spawned.kill(); - }); + let fd; - return timedPromise.finally(() => { - removeExitHandler(); - }); -}; + try { + fd = fs.openSync(command, 'r'); + fs.readSync(fd, buffer, 0, size, 0); + fs.closeSync(fd); + } catch (e) { /* Empty */ } -module.exports = { - spawnedKill, - spawnedCancel, - setupTimeout, - setExitHandler -}; + // Attempt to extract shebang (null is returned if not a shebang) + return shebangCommand(buffer.toString()); +} + +module.exports = readShebang; /***/ }), -/* 377 */ +/* 383 */ /***/ (function(module, exports, __webpack_require__) { -// Note: since nyc uses this module to output coverage, any lines -// that are in the direct sync flow of nyc's outputCoverage are -// ignored, since we can never get coverage for them. -var assert = __webpack_require__(30) -var signals = __webpack_require__(378) - -var EE = __webpack_require__(379) -/* istanbul ignore if */ -if (typeof EE !== 'function') { - EE = EE.EventEmitter -} - -var emitter -if (process.__signal_exit_emitter__) { - emitter = process.__signal_exit_emitter__ -} else { - emitter = process.__signal_exit_emitter__ = new EE() - emitter.count = 0 - emitter.emitted = {} -} - -// Because this emitter is a global, we have to check to see if a -// previous version of this library failed to enable infinite listeners. -// I know what you're about to say. But literally everything about -// signal-exit is a compromise with evil. Get used to it. -if (!emitter.infinite) { - emitter.setMaxListeners(Infinity) - emitter.infinite = true -} - -module.exports = function (cb, opts) { - assert.equal(typeof cb, 'function', 'a callback must be provided for exit handler') +"use strict"; - if (loaded === false) { - load() - } +const shebangRegex = __webpack_require__(384); - var ev = 'exit' - if (opts && opts.alwaysLast) { - ev = 'afterexit' - } +module.exports = (string = '') => { + const match = string.match(shebangRegex); - var remove = function () { - emitter.removeListener(ev, cb) - if (emitter.listeners('exit').length === 0 && - emitter.listeners('afterexit').length === 0) { - unload() - } - } - emitter.on(ev, cb) + if (!match) { + return null; + } - return remove -} + const [path, argument] = match[0].replace(/#! ?/, '').split(' '); + const binary = path.split('/').pop(); -module.exports.unload = unload -function unload () { - if (!loaded) { - return - } - loaded = false + if (binary === 'env') { + return argument; + } - signals.forEach(function (sig) { - try { - process.removeListener(sig, sigListeners[sig]) - } catch (er) {} - }) - process.emit = originalProcessEmit - process.reallyExit = originalProcessReallyExit - emitter.count -= 1 -} + return argument ? `${binary} ${argument}` : binary; +}; -function emit (event, code, signal) { - if (emitter.emitted[event]) { - return - } - emitter.emitted[event] = true - emitter.emit(event, code, signal) -} -// { : , ... } -var sigListeners = {} -signals.forEach(function (sig) { - sigListeners[sig] = function listener () { - // If there are no other listeners, an exit is coming! - // Simplest way: remove us and then re-send the signal. - // We know that this will kill the process, so we can - // safely emit now. - var listeners = process.listeners(sig) - if (listeners.length === emitter.count) { - unload() - emit('exit', null, sig) - /* istanbul ignore next */ - emit('afterexit', null, sig) - /* istanbul ignore next */ - process.kill(process.pid, sig) - } - } -}) +/***/ }), +/* 384 */ +/***/ (function(module, exports, __webpack_require__) { -module.exports.signals = function () { - return signals -} +"use strict"; -module.exports.load = load +module.exports = /^#!(.*)/; -var loaded = false -function load () { - if (loaded) { - return - } - loaded = true +/***/ }), +/* 385 */ +/***/ (function(module, exports, __webpack_require__) { - // This is the number of onSignalExit's that are in play. - // It's important so that we can count the correct number of - // listeners on signals, and don't wait for the other one to - // handle it instead of us. - emitter.count += 1 +"use strict"; - signals = signals.filter(function (sig) { - try { - process.on(sig, sigListeners[sig]) - return true - } catch (er) { - return false - } - }) - process.emit = processEmit - process.reallyExit = processReallyExit -} +const isWin = process.platform === 'win32'; -var originalProcessReallyExit = process.reallyExit -function processReallyExit (code) { - process.exitCode = code || 0 - emit('exit', process.exitCode, null) - /* istanbul ignore next */ - emit('afterexit', process.exitCode, null) - /* istanbul ignore next */ - originalProcessReallyExit.call(process, process.exitCode) +function notFoundError(original, syscall) { + return Object.assign(new Error(`${syscall} ${original.command} ENOENT`), { + code: 'ENOENT', + errno: 'ENOENT', + syscall: `${syscall} ${original.command}`, + path: original.command, + spawnargs: original.args, + }); } -var originalProcessEmit = process.emit -function processEmit (ev, arg) { - if (ev === 'exit') { - if (arg !== undefined) { - process.exitCode = arg +function hookChildProcess(cp, parsed) { + if (!isWin) { + return; } - var ret = originalProcessEmit.apply(this, arguments) - emit('exit', process.exitCode, null) - /* istanbul ignore next */ - emit('afterexit', process.exitCode, null) - return ret - } else { - return originalProcessEmit.apply(this, arguments) - } -} + const originalEmit = cp.emit; -/***/ }), -/* 378 */ -/***/ (function(module, exports) { + cp.emit = function (name, arg1) { + // If emitting "exit" event and exit code is 1, we need to check if + // the command exists and emit an "error" instead + // See https://github.com/IndigoUnited/node-cross-spawn/issues/16 + if (name === 'exit') { + const err = verifyENOENT(arg1, parsed, 'spawn'); -// This is not the set of all possible signals. -// -// It IS, however, the set of all signals that trigger -// an exit on either Linux or BSD systems. Linux is a -// superset of the signal names supported on BSD, and -// the unknown signals just fail to register, so we can -// catch that easily enough. -// -// Don't bother with SIGKILL. It's uncatchable, which -// means that we can't fire any callbacks anyway. -// -// If a user does happen to register a handler on a non- -// fatal signal like SIGWINCH or something, and then -// exit, it'll end up firing `process.emit('exit')`, so -// the handler will be fired anyway. -// -// SIGBUS, SIGFPE, SIGSEGV and SIGILL, when not raised -// artificially, inherently leave the process in a -// state from which it is not safe to try and enter JS -// listeners. -module.exports = [ - 'SIGABRT', - 'SIGALRM', - 'SIGHUP', - 'SIGINT', - 'SIGTERM' -] + if (err) { + return originalEmit.call(cp, 'error', err); + } + } -if (process.platform !== 'win32') { - module.exports.push( - 'SIGVTALRM', - 'SIGXCPU', - 'SIGXFSZ', - 'SIGUSR2', - 'SIGTRAP', - 'SIGSYS', - 'SIGQUIT', - 'SIGIOT' - // should detect profiler and enable/disable accordingly. - // see #21 - // 'SIGPROF' - ) + return originalEmit.apply(cp, arguments); // eslint-disable-line prefer-rest-params + }; } -if (process.platform === 'linux') { - module.exports.push( - 'SIGIO', - 'SIGPOLL', - 'SIGPWR', - 'SIGSTKFLT', - 'SIGUNUSED' - ) +function verifyENOENT(status, parsed) { + if (isWin && status === 1 && !parsed.file) { + return notFoundError(parsed.original, 'spawn'); + } + + return null; } +function verifyENOENTSync(status, parsed) { + if (isWin && status === 1 && !parsed.file) { + return notFoundError(parsed.original, 'spawnSync'); + } -/***/ }), -/* 379 */ -/***/ (function(module, exports) { + return null; +} + +module.exports = { + hookChildProcess, + verifyENOENT, + verifyENOENTSync, + notFoundError, +}; -module.exports = require("events"); /***/ }), -/* 380 */ +/* 386 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const isStream = __webpack_require__(381); -const getStream = __webpack_require__(382); -const mergeStream = __webpack_require__(388); -// `input` option -const handleInput = (spawned, input) => { - // Checking for stdin is workaround for https://github.com/nodejs/node/issues/26852 - // TODO: Remove `|| spawned.stdin === undefined` once we drop support for Node.js <=12.2.0 - if (input === undefined || spawned.stdin === undefined) { - return; - } +module.exports = input => { + const LF = typeof input === 'string' ? '\n' : '\n'.charCodeAt(); + const CR = typeof input === 'string' ? '\r' : '\r'.charCodeAt(); - if (isStream(input)) { - input.pipe(spawned.stdin); - } else { - spawned.stdin.end(input); + if (input[input.length - 1] === LF) { + input = input.slice(0, input.length - 1); } -}; -// `all` interleaves `stdout` and `stderr` -const makeAllStream = (spawned, {all}) => { - if (!all || (!spawned.stdout && !spawned.stderr)) { - return; + if (input[input.length - 1] === CR) { + input = input.slice(0, input.length - 1); } - const mixed = mergeStream(); + return input; +}; - if (spawned.stdout) { - mixed.add(spawned.stdout); - } - if (spawned.stderr) { - mixed.add(spawned.stderr); - } +/***/ }), +/* 387 */ +/***/ (function(module, exports, __webpack_require__) { - return mixed; -}; +"use strict"; -// On failure, `result.stdout|stderr|all` should contain the currently buffered stream -const getBufferedData = async (stream, streamPromise) => { - if (!stream) { - return; - } +const path = __webpack_require__(16); +const pathKey = __webpack_require__(380); - stream.destroy(); +const npmRunPath = options => { + options = { + cwd: process.cwd(), + path: process.env[pathKey()], + execPath: process.execPath, + ...options + }; - try { - return await streamPromise; - } catch (error) { - return error.bufferedData; - } -}; + let previous; + let cwdPath = path.resolve(options.cwd); + const result = []; -const getStreamPromise = (stream, {encoding, buffer, maxBuffer}) => { - if (!stream || !buffer) { - return; + while (previous !== cwdPath) { + result.push(path.join(cwdPath, 'node_modules/.bin')); + previous = cwdPath; + cwdPath = path.resolve(cwdPath, '..'); } - if (encoding) { - return getStream(stream, {encoding, maxBuffer}); - } + // Ensure the running `node` binary is used + const execPathDir = path.resolve(options.cwd, options.execPath, '..'); + result.unshift(execPathDir); - return getStream.buffer(stream, {maxBuffer}); + return result.concat(options.path).join(path.delimiter); }; -// Retrieve result of child process: exit code, signal, error, streams (stdout/stderr/all) -const getSpawnedResult = async ({stdout, stderr, all}, {encoding, buffer, maxBuffer}, processDone) => { - const stdoutPromise = getStreamPromise(stdout, {encoding, buffer, maxBuffer}); - const stderrPromise = getStreamPromise(stderr, {encoding, buffer, maxBuffer}); - const allPromise = getStreamPromise(all, {encoding, buffer, maxBuffer: maxBuffer * 2}); +module.exports = npmRunPath; +// TODO: Remove this for the next major release +module.exports.default = npmRunPath; - try { - return await Promise.all([processDone, stdoutPromise, stderrPromise, allPromise]); - } catch (error) { - return Promise.all([ - {error, signal: error.signal, timedOut: error.timedOut}, - getBufferedData(stdout, stdoutPromise), - getBufferedData(stderr, stderrPromise), - getBufferedData(all, allPromise) - ]); - } -}; +module.exports.env = options => { + options = { + env: process.env, + ...options + }; -const validateInputSync = ({input}) => { - if (isStream(input)) { - throw new TypeError('The `input` option cannot be a stream in sync mode'); - } -}; + const env = {...options.env}; + const path = pathKey({env}); -module.exports = { - handleInput, - makeAllStream, - getSpawnedResult, - validateInputSync -}; + options.path = env[path]; + env[path] = module.exports(options); + return env; +}; /***/ }), -/* 381 */ +/* 388 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; +const mimicFn = __webpack_require__(389); -const isStream = stream => - stream !== null && - typeof stream === 'object' && - typeof stream.pipe === 'function'; - -isStream.writable = stream => - isStream(stream) && - stream.writable !== false && - typeof stream._write === 'function' && - typeof stream._writableState === 'object'; +const calledFunctions = new WeakMap(); -isStream.readable = stream => - isStream(stream) && - stream.readable !== false && - typeof stream._read === 'function' && - typeof stream._readableState === 'object'; +const oneTime = (fn, options = {}) => { + if (typeof fn !== 'function') { + throw new TypeError('Expected a function'); + } -isStream.duplex = stream => - isStream.writable(stream) && - isStream.readable(stream); + let ret; + let isCalled = false; + let callCount = 0; + const functionName = fn.displayName || fn.name || ''; -isStream.transform = stream => - isStream.duplex(stream) && - typeof stream._transform === 'function' && - typeof stream._transformState === 'object'; + const onetime = function (...args) { + calledFunctions.set(onetime, ++callCount); -module.exports = isStream; + if (isCalled) { + if (options.throw === true) { + throw new Error(`Function \`${functionName}\` can only be called once`); + } + return ret; + } -/***/ }), -/* 382 */ -/***/ (function(module, exports, __webpack_require__) { + isCalled = true; + ret = fn.apply(this, args); + fn = null; -"use strict"; + return ret; + }; -const pump = __webpack_require__(383); -const bufferStream = __webpack_require__(387); + mimicFn(onetime, fn); + calledFunctions.set(onetime, callCount); -class MaxBufferError extends Error { - constructor() { - super('maxBuffer exceeded'); - this.name = 'MaxBufferError'; - } -} + return onetime; +}; -async function getStream(inputStream, options) { - if (!inputStream) { - return Promise.reject(new Error('Expected a stream')); - } +module.exports = oneTime; +// TODO: Remove this for the next major release +module.exports.default = oneTime; - options = { - maxBuffer: Infinity, - ...options - }; +module.exports.callCount = fn => { + if (!calledFunctions.has(fn)) { + throw new Error(`The given function \`${fn.name}\` is not wrapped by the \`onetime\` package`); + } - const {maxBuffer} = options; + return calledFunctions.get(fn); +}; - let stream; - await new Promise((resolve, reject) => { - const rejectPromise = error => { - if (error) { // A null check - error.bufferedData = stream.getBufferedValue(); - } - reject(error); - }; +/***/ }), +/* 389 */ +/***/ (function(module, exports, __webpack_require__) { - stream = pump(inputStream, bufferStream(options), error => { - if (error) { - rejectPromise(error); - return; - } +"use strict"; - resolve(); - }); - stream.on('data', () => { - if (stream.getBufferedLength() > maxBuffer) { - rejectPromise(new MaxBufferError()); - } - }); - }); +const mimicFn = (to, from) => { + for (const prop of Reflect.ownKeys(from)) { + Object.defineProperty(to, prop, Object.getOwnPropertyDescriptor(from, prop)); + } - return stream.getBufferedValue(); -} + return to; +}; -module.exports = getStream; +module.exports = mimicFn; // TODO: Remove this for the next major release -module.exports.default = getStream; -module.exports.buffer = (stream, options) => getStream(stream, {...options, encoding: 'buffer'}); -module.exports.array = (stream, options) => getStream(stream, {...options, array: true}); -module.exports.MaxBufferError = MaxBufferError; +module.exports.default = mimicFn; /***/ }), -/* 383 */ +/* 390 */ /***/ (function(module, exports, __webpack_require__) { -var once = __webpack_require__(384) -var eos = __webpack_require__(386) -var fs = __webpack_require__(23) // we only need fs to get the ReadStream and WriteStream prototypes - -var noop = function () {} -var ancient = /^v?\.0/.test(process.version) +"use strict"; -var isFn = function (fn) { - return typeof fn === 'function' -} +const {signalsByName} = __webpack_require__(391); -var isFS = function (stream) { - if (!ancient) return false // newer node version do not need to care about fs is a special way - if (!fs) return false // browser - return (stream instanceof (fs.ReadStream || noop) || stream instanceof (fs.WriteStream || noop)) && isFn(stream.close) -} +const getErrorPrefix = ({timedOut, timeout, errorCode, signal, signalDescription, exitCode, isCanceled}) => { + if (timedOut) { + return `timed out after ${timeout} milliseconds`; + } -var isRequest = function (stream) { - return stream.setHeader && isFn(stream.abort) -} + if (isCanceled) { + return 'was canceled'; + } -var destroyer = function (stream, reading, writing, callback) { - callback = once(callback) + if (errorCode !== undefined) { + return `failed with ${errorCode}`; + } - var closed = false - stream.on('close', function () { - closed = true - }) + if (signal !== undefined) { + return `was killed with ${signal} (${signalDescription})`; + } - eos(stream, {readable: reading, writable: writing}, function (err) { - if (err) return callback(err) - closed = true - callback() - }) + if (exitCode !== undefined) { + return `failed with exit code ${exitCode}`; + } - var destroyed = false - return function (err) { - if (closed) return - if (destroyed) return - destroyed = true + return 'failed'; +}; - if (isFS(stream)) return stream.close(noop) // use close for fs streams to avoid fd leaks - if (isRequest(stream)) return stream.abort() // request.destroy just do .end - .abort is what we want +const makeError = ({ + stdout, + stderr, + all, + error, + signal, + exitCode, + command, + timedOut, + isCanceled, + killed, + parsed: {options: {timeout}} +}) => { + // `signal` and `exitCode` emitted on `spawned.on('exit')` event can be `null`. + // We normalize them to `undefined` + exitCode = exitCode === null ? undefined : exitCode; + signal = signal === null ? undefined : signal; + const signalDescription = signal === undefined ? undefined : signalsByName[signal].description; - if (isFn(stream.destroy)) return stream.destroy() + const errorCode = error && error.code; - callback(err || new Error('stream was destroyed')) - } -} + const prefix = getErrorPrefix({timedOut, timeout, errorCode, signal, signalDescription, exitCode, isCanceled}); + const execaMessage = `Command ${prefix}: ${command}`; + const shortMessage = error instanceof Error ? `${execaMessage}\n${error.message}` : execaMessage; + const message = [shortMessage, stderr, stdout].filter(Boolean).join('\n'); -var call = function (fn) { - fn() -} + if (error instanceof Error) { + error.originalMessage = error.message; + error.message = message; + } else { + error = new Error(message); + } -var pipe = function (from, to) { - return from.pipe(to) -} + error.shortMessage = shortMessage; + error.command = command; + error.exitCode = exitCode; + error.signal = signal; + error.signalDescription = signalDescription; + error.stdout = stdout; + error.stderr = stderr; -var pump = function () { - var streams = Array.prototype.slice.call(arguments) - var callback = isFn(streams[streams.length - 1] || noop) && streams.pop() || noop + if (all !== undefined) { + error.all = all; + } - if (Array.isArray(streams[0])) streams = streams[0] - if (streams.length < 2) throw new Error('pump requires two streams per minimum') + if ('bufferedData' in error) { + delete error.bufferedData; + } - var error - var destroys = streams.map(function (stream, i) { - var reading = i < streams.length - 1 - var writing = i > 0 - return destroyer(stream, reading, writing, function (err) { - if (!error) error = err - if (err) destroys.forEach(call) - if (reading) return - destroys.forEach(call) - callback(error) - }) - }) + error.failed = true; + error.timedOut = Boolean(timedOut); + error.isCanceled = isCanceled; + error.killed = killed && !timedOut; - return streams.reduce(pipe) -} + return error; +}; -module.exports = pump +module.exports = makeError; /***/ }), -/* 384 */ +/* 391 */ /***/ (function(module, exports, __webpack_require__) { -var wrappy = __webpack_require__(385) -module.exports = wrappy(once) -module.exports.strict = wrappy(onceStrict) - -once.proto = once(function () { - Object.defineProperty(Function.prototype, 'once', { - value: function () { - return once(this) - }, - configurable: true - }) - - Object.defineProperty(Function.prototype, 'onceStrict', { - value: function () { - return onceStrict(this) - }, - configurable: true - }) -}) - -function once (fn) { - var f = function () { - if (f.called) return f.value - f.called = true - return f.value = fn.apply(this, arguments) - } - f.called = false - return f -} - -function onceStrict (fn) { - var f = function () { - if (f.called) - throw new Error(f.onceError) - f.called = true - return f.value = fn.apply(this, arguments) - } - var name = fn.name || 'Function wrapped with `once`' - f.onceError = name + " shouldn't be called more than once" - f.called = false - return f -} - +"use strict"; +Object.defineProperty(exports,"__esModule",{value:true});exports.signalsByNumber=exports.signalsByName=void 0;var _os=__webpack_require__(11); -/***/ }), -/* 385 */ -/***/ (function(module, exports) { +var _signals=__webpack_require__(392); +var _realtime=__webpack_require__(394); -// Returns a wrapper function that returns a wrapped callback -// The wrapper function should do some stuff, and return a -// presumably different callback function. -// This makes sure that own properties are retained, so that -// decorations and such are not lost along the way. -module.exports = wrappy -function wrappy (fn, cb) { - if (fn && cb) return wrappy(fn)(cb) - if (typeof fn !== 'function') - throw new TypeError('need wrapper function') - Object.keys(fn).forEach(function (k) { - wrapper[k] = fn[k] - }) +const getSignalsByName=function(){ +const signals=(0,_signals.getSignals)(); +return signals.reduce(getSignalByName,{}); +}; - return wrapper +const getSignalByName=function( +signalByNameMemo, +{name,number,description,supported,action,forced,standard}) +{ +return{ +...signalByNameMemo, +[name]:{name,number,description,supported,action,forced,standard}}; - function wrapper() { - var args = new Array(arguments.length) - for (var i = 0; i < args.length; i++) { - args[i] = arguments[i] - } - var ret = fn.apply(this, args) - var cb = args[args.length-1] - if (typeof ret === 'function' && ret !== cb) { - Object.keys(cb).forEach(function (k) { - ret[k] = cb[k] - }) - } - return ret - } -} +}; +const signalsByName=getSignalsByName();exports.signalsByName=signalsByName; -/***/ }), -/* 386 */ -/***/ (function(module, exports, __webpack_require__) { -var once = __webpack_require__(384); -var noop = function() {}; -var isRequest = function(stream) { - return stream.setHeader && typeof stream.abort === 'function'; -}; +const getSignalsByNumber=function(){ +const signals=(0,_signals.getSignals)(); +const length=_realtime.SIGRTMAX+1; +const signalsA=Array.from({length},(value,number)=> +getSignalByNumber(number,signals)); -var isChildProcess = function(stream) { - return stream.stdio && Array.isArray(stream.stdio) && stream.stdio.length === 3 +return Object.assign({},...signalsA); }; -var eos = function(stream, opts, callback) { - if (typeof opts === 'function') return eos(stream, null, opts); - if (!opts) opts = {}; - - callback = once(callback || noop); - - var ws = stream._writableState; - var rs = stream._readableState; - var readable = opts.readable || (opts.readable !== false && stream.readable); - var writable = opts.writable || (opts.writable !== false && stream.writable); - - var onlegacyfinish = function() { - if (!stream.writable) onfinish(); - }; - - var onfinish = function() { - writable = false; - if (!readable) callback.call(stream); - }; +const getSignalByNumber=function(number,signals){ +const signal=findSignalByNumber(number,signals); - var onend = function() { - readable = false; - if (!writable) callback.call(stream); - }; +if(signal===undefined){ +return{}; +} - var onexit = function(exitCode) { - callback.call(stream, exitCode ? new Error('exited with error code: ' + exitCode) : null); - }; +const{name,description,supported,action,forced,standard}=signal; +return{ +[number]:{ +name, +number, +description, +supported, +action, +forced, +standard}}; - var onerror = function(err) { - callback.call(stream, err); - }; - var onclose = function() { - if (readable && !(rs && rs.ended)) return callback.call(stream, new Error('premature close')); - if (writable && !(ws && ws.ended)) return callback.call(stream, new Error('premature close')); - }; +}; - var onrequest = function() { - stream.req.on('finish', onfinish); - }; - if (isRequest(stream)) { - stream.on('complete', onfinish); - stream.on('abort', onclose); - if (stream.req) onrequest(); - else stream.on('request', onrequest); - } else if (writable && !ws) { // legacy streams - stream.on('end', onlegacyfinish); - stream.on('close', onlegacyfinish); - } - if (isChildProcess(stream)) stream.on('exit', onexit); +const findSignalByNumber=function(number,signals){ +const signal=signals.find(({name})=>_os.constants.signals[name]===number); - stream.on('end', onend); - stream.on('finish', onfinish); - if (opts.error !== false) stream.on('error', onerror); - stream.on('close', onclose); +if(signal!==undefined){ +return signal; +} - return function() { - stream.removeListener('complete', onfinish); - stream.removeListener('abort', onclose); - stream.removeListener('request', onrequest); - if (stream.req) stream.req.removeListener('finish', onfinish); - stream.removeListener('end', onlegacyfinish); - stream.removeListener('close', onlegacyfinish); - stream.removeListener('finish', onfinish); - stream.removeListener('exit', onexit); - stream.removeListener('end', onend); - stream.removeListener('error', onerror); - stream.removeListener('close', onclose); - }; +return signals.find(signalA=>signalA.number===number); }; -module.exports = eos; - +const signalsByNumber=getSignalsByNumber();exports.signalsByNumber=signalsByNumber; +//# sourceMappingURL=main.js.map /***/ }), -/* 387 */ +/* 392 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; +Object.defineProperty(exports,"__esModule",{value:true});exports.getSignals=void 0;var _os=__webpack_require__(11); -const {PassThrough: PassThroughStream} = __webpack_require__(27); - -module.exports = options => { - options = {...options}; - - const {array} = options; - let {encoding} = options; - const isBuffer = encoding === 'buffer'; - let objectMode = false; - - if (array) { - objectMode = !(encoding || isBuffer); - } else { - encoding = encoding || 'utf8'; - } +var _core=__webpack_require__(393); +var _realtime=__webpack_require__(394); - if (isBuffer) { - encoding = null; - } - const stream = new PassThroughStream({objectMode}); - if (encoding) { - stream.setEncoding(encoding); - } +const getSignals=function(){ +const realtimeSignals=(0,_realtime.getRealtimeSignals)(); +const signals=[..._core.SIGNALS,...realtimeSignals].map(normalizeSignal); +return signals; +};exports.getSignals=getSignals; - let length = 0; - const chunks = []; - stream.on('data', chunk => { - chunks.push(chunk); - if (objectMode) { - length = chunks.length; - } else { - length += chunk.length; - } - }); - stream.getBufferedValue = () => { - if (array) { - return chunks; - } - return isBuffer ? Buffer.concat(chunks, length) : chunks.join(''); - }; - stream.getBufferedLength = () => length; - return stream; +const normalizeSignal=function({ +name, +number:defaultNumber, +description, +action, +forced=false, +standard}) +{ +const{ +signals:{[name]:constantSignal}}= +_os.constants; +const supported=constantSignal!==undefined; +const number=supported?constantSignal:defaultNumber; +return{name,number,description,supported,action,forced,standard}; }; - +//# sourceMappingURL=signals.js.map /***/ }), -/* 388 */ +/* 393 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; +Object.defineProperty(exports,"__esModule",{value:true});exports.SIGNALS=void 0; +const SIGNALS=[ +{ +name:"SIGHUP", +number:1, +action:"terminate", +description:"Terminal closed", +standard:"posix"}, -const { PassThrough } = __webpack_require__(27); - -module.exports = function (/*streams...*/) { - var sources = [] - var output = new PassThrough({objectMode: true}) - - output.setMaxListeners(0) +{ +name:"SIGINT", +number:2, +action:"terminate", +description:"User interruption with CTRL-C", +standard:"ansi"}, - output.add = add - output.isEmpty = isEmpty +{ +name:"SIGQUIT", +number:3, +action:"core", +description:"User interruption with CTRL-\\", +standard:"posix"}, - output.on('unpipe', remove) +{ +name:"SIGILL", +number:4, +action:"core", +description:"Invalid machine instruction", +standard:"ansi"}, - Array.prototype.slice.call(arguments).forEach(add) +{ +name:"SIGTRAP", +number:5, +action:"core", +description:"Debugger breakpoint", +standard:"posix"}, - return output +{ +name:"SIGABRT", +number:6, +action:"core", +description:"Aborted", +standard:"ansi"}, - function add (source) { - if (Array.isArray(source)) { - source.forEach(add) - return this - } +{ +name:"SIGIOT", +number:6, +action:"core", +description:"Aborted", +standard:"bsd"}, - sources.push(source); - source.once('end', remove.bind(null, source)) - source.once('error', output.emit.bind(output, 'error')) - source.pipe(output, {end: false}) - return this - } +{ +name:"SIGBUS", +number:7, +action:"core", +description: +"Bus error due to misaligned, non-existing address or paging error", +standard:"bsd"}, - function isEmpty () { - return sources.length == 0; - } +{ +name:"SIGEMT", +number:7, +action:"terminate", +description:"Command should be emulated but is not implemented", +standard:"other"}, - function remove (source) { - sources = sources.filter(function (it) { return it !== source }) - if (!sources.length && output.readable) { output.end() } - } -} +{ +name:"SIGFPE", +number:8, +action:"core", +description:"Floating point arithmetic error", +standard:"ansi"}, +{ +name:"SIGKILL", +number:9, +action:"terminate", +description:"Forced termination", +standard:"posix", +forced:true}, -/***/ }), -/* 389 */ -/***/ (function(module, exports, __webpack_require__) { +{ +name:"SIGUSR1", +number:10, +action:"terminate", +description:"Application-specific signal", +standard:"posix"}, -"use strict"; +{ +name:"SIGSEGV", +number:11, +action:"core", +description:"Segmentation fault", +standard:"ansi"}, -const mergePromiseProperty = (spawned, promise, property) => { - // Starting the main `promise` is deferred to avoid consuming streams - const value = typeof promise === 'function' ? - (...args) => promise()[property](...args) : - promise[property].bind(promise); +{ +name:"SIGUSR2", +number:12, +action:"terminate", +description:"Application-specific signal", +standard:"posix"}, - Object.defineProperty(spawned, property, { - value, - writable: true, - enumerable: false, - configurable: true - }); -}; +{ +name:"SIGPIPE", +number:13, +action:"terminate", +description:"Broken pipe or socket", +standard:"posix"}, -// The return value is a mixin of `childProcess` and `Promise` -const mergePromise = (spawned, promise) => { - mergePromiseProperty(spawned, promise, 'then'); - mergePromiseProperty(spawned, promise, 'catch'); - mergePromiseProperty(spawned, promise, 'finally'); - return spawned; -}; +{ +name:"SIGALRM", +number:14, +action:"terminate", +description:"Timeout or timer", +standard:"posix"}, -// Use promises instead of `child_process` events -const getSpawnedPromise = spawned => { - return new Promise((resolve, reject) => { - spawned.on('exit', (exitCode, signal) => { - resolve({exitCode, signal}); - }); +{ +name:"SIGTERM", +number:15, +action:"terminate", +description:"Termination", +standard:"ansi"}, - spawned.on('error', error => { - reject(error); - }); +{ +name:"SIGSTKFLT", +number:16, +action:"terminate", +description:"Stack is empty or overflowed", +standard:"other"}, - if (spawned.stdin) { - spawned.stdin.on('error', error => { - reject(error); - }); - } - }); -}; +{ +name:"SIGCHLD", +number:17, +action:"ignore", +description:"Child process terminated, paused or unpaused", +standard:"posix"}, -module.exports = { - mergePromise, - getSpawnedPromise -}; +{ +name:"SIGCLD", +number:17, +action:"ignore", +description:"Child process terminated, paused or unpaused", +standard:"other"}, +{ +name:"SIGCONT", +number:18, +action:"unpause", +description:"Unpaused", +standard:"posix", +forced:true}, +{ +name:"SIGSTOP", +number:19, +action:"pause", +description:"Paused", +standard:"posix", +forced:true}, -/***/ }), -/* 390 */ -/***/ (function(module, exports, __webpack_require__) { +{ +name:"SIGTSTP", +number:20, +action:"pause", +description:"Paused using CTRL-Z or \"suspend\"", +standard:"posix"}, -"use strict"; +{ +name:"SIGTTIN", +number:21, +action:"pause", +description:"Background process cannot read terminal input", +standard:"posix"}, -const SPACES_REGEXP = / +/g; +{ +name:"SIGBREAK", +number:21, +action:"terminate", +description:"User interruption with CTRL-BREAK", +standard:"other"}, -const joinCommand = (file, args = []) => { - if (!Array.isArray(args)) { - return file; - } +{ +name:"SIGTTOU", +number:22, +action:"pause", +description:"Background process cannot write to terminal output", +standard:"posix"}, - return [file, ...args].join(' '); -}; +{ +name:"SIGURG", +number:23, +action:"ignore", +description:"Socket received out-of-band data", +standard:"bsd"}, -// Allow spaces to be escaped by a backslash if not meant as a delimiter -const handleEscaping = (tokens, token, index) => { - if (index === 0) { - return [token]; - } +{ +name:"SIGXCPU", +number:24, +action:"core", +description:"Process timed out", +standard:"bsd"}, - const previousToken = tokens[tokens.length - 1]; +{ +name:"SIGXFSZ", +number:25, +action:"core", +description:"File too big", +standard:"bsd"}, - if (previousToken.endsWith('\\')) { - return [...tokens.slice(0, -1), `${previousToken.slice(0, -1)} ${token}`]; - } +{ +name:"SIGVTALRM", +number:26, +action:"terminate", +description:"Timeout or timer", +standard:"bsd"}, - return [...tokens, token]; -}; +{ +name:"SIGPROF", +number:27, +action:"terminate", +description:"Timeout or timer", +standard:"bsd"}, -// Handle `execa.command()` -const parseCommand = command => { - return command - .trim() - .split(SPACES_REGEXP) - .reduce(handleEscaping, []); -}; +{ +name:"SIGWINCH", +number:28, +action:"ignore", +description:"Terminal window size changed", +standard:"bsd"}, -module.exports = { - joinCommand, - parseCommand -}; +{ +name:"SIGIO", +number:29, +action:"terminate", +description:"I/O is available", +standard:"other"}, +{ +name:"SIGPOLL", +number:29, +action:"terminate", +description:"Watched event", +standard:"other"}, -/***/ }), -/* 391 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { +{ +name:"SIGINFO", +number:29, +action:"ignore", +description:"Request for process information", +standard:"other"}, -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony import */ var _internal_Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(193); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "Observable", function() { return _internal_Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"]; }); +{ +name:"SIGPWR", +number:30, +action:"terminate", +description:"Device running out of power", +standard:"systemv"}, -/* harmony import */ var _internal_observable_ConnectableObservable__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(283); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "ConnectableObservable", function() { return _internal_observable_ConnectableObservable__WEBPACK_IMPORTED_MODULE_1__["ConnectableObservable"]; }); +{ +name:"SIGSYS", +number:31, +action:"core", +description:"Invalid system call", +standard:"other"}, -/* harmony import */ var _internal_operators_groupBy__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(264); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "GroupedObservable", function() { return _internal_operators_groupBy__WEBPACK_IMPORTED_MODULE_2__["GroupedObservable"]; }); +{ +name:"SIGUNUSED", +number:31, +action:"terminate", +description:"Invalid system call", +standard:"other"}];exports.SIGNALS=SIGNALS; +//# sourceMappingURL=core.js.map -/* harmony import */ var _internal_symbol_observable__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(190); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "observable", function() { return _internal_symbol_observable__WEBPACK_IMPORTED_MODULE_3__["observable"]; }); +/***/ }), +/* 394 */ +/***/ (function(module, exports, __webpack_require__) { -/* harmony import */ var _internal_Subject__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(265); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "Subject", function() { return _internal_Subject__WEBPACK_IMPORTED_MODULE_4__["Subject"]; }); +"use strict"; +Object.defineProperty(exports,"__esModule",{value:true});exports.SIGRTMAX=exports.getRealtimeSignals=void 0; +const getRealtimeSignals=function(){ +const length=SIGRTMAX-SIGRTMIN+1; +return Array.from({length},getRealtimeSignal); +};exports.getRealtimeSignals=getRealtimeSignals; -/* harmony import */ var _internal_BehaviorSubject__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(293); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "BehaviorSubject", function() { return _internal_BehaviorSubject__WEBPACK_IMPORTED_MODULE_5__["BehaviorSubject"]; }); +const getRealtimeSignal=function(value,index){ +return{ +name:`SIGRT${index+1}`, +number:SIGRTMIN+index, +action:"terminate", +description:"Application-specific signal (realtime)", +standard:"posix"}; -/* harmony import */ var _internal_ReplaySubject__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(297); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "ReplaySubject", function() { return _internal_ReplaySubject__WEBPACK_IMPORTED_MODULE_6__["ReplaySubject"]; }); - -/* harmony import */ var _internal_AsyncSubject__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(295); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "AsyncSubject", function() { return _internal_AsyncSubject__WEBPACK_IMPORTED_MODULE_7__["AsyncSubject"]; }); - -/* harmony import */ var _internal_scheduler_asap__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(320); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "asapScheduler", function() { return _internal_scheduler_asap__WEBPACK_IMPORTED_MODULE_8__["asap"]; }); - -/* harmony import */ var _internal_scheduler_async__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(199); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "asyncScheduler", function() { return _internal_scheduler_async__WEBPACK_IMPORTED_MODULE_9__["async"]; }); - -/* harmony import */ var _internal_scheduler_queue__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(298); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "queueScheduler", function() { return _internal_scheduler_queue__WEBPACK_IMPORTED_MODULE_10__["queue"]; }); - -/* harmony import */ var _internal_scheduler_animationFrame__WEBPACK_IMPORTED_MODULE_11__ = __webpack_require__(392); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "animationFrameScheduler", function() { return _internal_scheduler_animationFrame__WEBPACK_IMPORTED_MODULE_11__["animationFrame"]; }); - -/* harmony import */ var _internal_scheduler_VirtualTimeScheduler__WEBPACK_IMPORTED_MODULE_12__ = __webpack_require__(395); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "VirtualTimeScheduler", function() { return _internal_scheduler_VirtualTimeScheduler__WEBPACK_IMPORTED_MODULE_12__["VirtualTimeScheduler"]; }); - -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "VirtualAction", function() { return _internal_scheduler_VirtualTimeScheduler__WEBPACK_IMPORTED_MODULE_12__["VirtualAction"]; }); - -/* harmony import */ var _internal_Scheduler__WEBPACK_IMPORTED_MODULE_13__ = __webpack_require__(203); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "Scheduler", function() { return _internal_Scheduler__WEBPACK_IMPORTED_MODULE_13__["Scheduler"]; }); - -/* harmony import */ var _internal_Subscription__WEBPACK_IMPORTED_MODULE_14__ = __webpack_require__(177); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "Subscription", function() { return _internal_Subscription__WEBPACK_IMPORTED_MODULE_14__["Subscription"]; }); - -/* harmony import */ var _internal_Subscriber__WEBPACK_IMPORTED_MODULE_15__ = __webpack_require__(172); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "Subscriber", function() { return _internal_Subscriber__WEBPACK_IMPORTED_MODULE_15__["Subscriber"]; }); - -/* harmony import */ var _internal_Notification__WEBPACK_IMPORTED_MODULE_16__ = __webpack_require__(241); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "Notification", function() { return _internal_Notification__WEBPACK_IMPORTED_MODULE_16__["Notification"]; }); - -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "NotificationKind", function() { return _internal_Notification__WEBPACK_IMPORTED_MODULE_16__["NotificationKind"]; }); - -/* harmony import */ var _internal_util_pipe__WEBPACK_IMPORTED_MODULE_17__ = __webpack_require__(196); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "pipe", function() { return _internal_util_pipe__WEBPACK_IMPORTED_MODULE_17__["pipe"]; }); - -/* harmony import */ var _internal_util_noop__WEBPACK_IMPORTED_MODULE_18__ = __webpack_require__(197); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "noop", function() { return _internal_util_noop__WEBPACK_IMPORTED_MODULE_18__["noop"]; }); - -/* harmony import */ var _internal_util_identity__WEBPACK_IMPORTED_MODULE_19__ = __webpack_require__(232); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "identity", function() { return _internal_util_identity__WEBPACK_IMPORTED_MODULE_19__["identity"]; }); - -/* harmony import */ var _internal_util_isObservable__WEBPACK_IMPORTED_MODULE_20__ = __webpack_require__(396); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "isObservable", function() { return _internal_util_isObservable__WEBPACK_IMPORTED_MODULE_20__["isObservable"]; }); - -/* harmony import */ var _internal_util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_21__ = __webpack_require__(250); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "ArgumentOutOfRangeError", function() { return _internal_util_ArgumentOutOfRangeError__WEBPACK_IMPORTED_MODULE_21__["ArgumentOutOfRangeError"]; }); - -/* harmony import */ var _internal_util_EmptyError__WEBPACK_IMPORTED_MODULE_22__ = __webpack_require__(253); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "EmptyError", function() { return _internal_util_EmptyError__WEBPACK_IMPORTED_MODULE_22__["EmptyError"]; }); - -/* harmony import */ var _internal_util_ObjectUnsubscribedError__WEBPACK_IMPORTED_MODULE_23__ = __webpack_require__(266); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "ObjectUnsubscribedError", function() { return _internal_util_ObjectUnsubscribedError__WEBPACK_IMPORTED_MODULE_23__["ObjectUnsubscribedError"]; }); - -/* harmony import */ var _internal_util_UnsubscriptionError__WEBPACK_IMPORTED_MODULE_24__ = __webpack_require__(180); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "UnsubscriptionError", function() { return _internal_util_UnsubscriptionError__WEBPACK_IMPORTED_MODULE_24__["UnsubscriptionError"]; }); - -/* harmony import */ var _internal_util_TimeoutError__WEBPACK_IMPORTED_MODULE_25__ = __webpack_require__(335); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "TimeoutError", function() { return _internal_util_TimeoutError__WEBPACK_IMPORTED_MODULE_25__["TimeoutError"]; }); - -/* harmony import */ var _internal_observable_bindCallback__WEBPACK_IMPORTED_MODULE_26__ = __webpack_require__(397); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bindCallback", function() { return _internal_observable_bindCallback__WEBPACK_IMPORTED_MODULE_26__["bindCallback"]; }); - -/* harmony import */ var _internal_observable_bindNodeCallback__WEBPACK_IMPORTED_MODULE_27__ = __webpack_require__(398); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "bindNodeCallback", function() { return _internal_observable_bindNodeCallback__WEBPACK_IMPORTED_MODULE_27__["bindNodeCallback"]; }); - -/* harmony import */ var _internal_observable_combineLatest__WEBPACK_IMPORTED_MODULE_28__ = __webpack_require__(214); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "combineLatest", function() { return _internal_observable_combineLatest__WEBPACK_IMPORTED_MODULE_28__["combineLatest"]; }); - -/* harmony import */ var _internal_observable_concat__WEBPACK_IMPORTED_MODULE_29__ = __webpack_require__(226); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "concat", function() { return _internal_observable_concat__WEBPACK_IMPORTED_MODULE_29__["concat"]; }); - -/* harmony import */ var _internal_observable_defer__WEBPACK_IMPORTED_MODULE_30__ = __webpack_require__(333); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "defer", function() { return _internal_observable_defer__WEBPACK_IMPORTED_MODULE_30__["defer"]; }); - -/* harmony import */ var _internal_observable_empty__WEBPACK_IMPORTED_MODULE_31__ = __webpack_require__(242); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "empty", function() { return _internal_observable_empty__WEBPACK_IMPORTED_MODULE_31__["empty"]; }); - -/* harmony import */ var _internal_observable_forkJoin__WEBPACK_IMPORTED_MODULE_32__ = __webpack_require__(399); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "forkJoin", function() { return _internal_observable_forkJoin__WEBPACK_IMPORTED_MODULE_32__["forkJoin"]; }); - -/* harmony import */ var _internal_observable_from__WEBPACK_IMPORTED_MODULE_33__ = __webpack_require__(218); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "from", function() { return _internal_observable_from__WEBPACK_IMPORTED_MODULE_33__["from"]; }); - -/* harmony import */ var _internal_observable_fromEvent__WEBPACK_IMPORTED_MODULE_34__ = __webpack_require__(400); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "fromEvent", function() { return _internal_observable_fromEvent__WEBPACK_IMPORTED_MODULE_34__["fromEvent"]; }); - -/* harmony import */ var _internal_observable_fromEventPattern__WEBPACK_IMPORTED_MODULE_35__ = __webpack_require__(401); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "fromEventPattern", function() { return _internal_observable_fromEventPattern__WEBPACK_IMPORTED_MODULE_35__["fromEventPattern"]; }); - -/* harmony import */ var _internal_observable_generate__WEBPACK_IMPORTED_MODULE_36__ = __webpack_require__(402); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "generate", function() { return _internal_observable_generate__WEBPACK_IMPORTED_MODULE_36__["generate"]; }); - -/* harmony import */ var _internal_observable_iif__WEBPACK_IMPORTED_MODULE_37__ = __webpack_require__(403); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "iif", function() { return _internal_observable_iif__WEBPACK_IMPORTED_MODULE_37__["iif"]; }); - -/* harmony import */ var _internal_observable_interval__WEBPACK_IMPORTED_MODULE_38__ = __webpack_require__(404); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "interval", function() { return _internal_observable_interval__WEBPACK_IMPORTED_MODULE_38__["interval"]; }); - -/* harmony import */ var _internal_observable_merge__WEBPACK_IMPORTED_MODULE_39__ = __webpack_require__(278); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "merge", function() { return _internal_observable_merge__WEBPACK_IMPORTED_MODULE_39__["merge"]; }); - -/* harmony import */ var _internal_observable_never__WEBPACK_IMPORTED_MODULE_40__ = __webpack_require__(405); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "never", function() { return _internal_observable_never__WEBPACK_IMPORTED_MODULE_40__["never"]; }); - -/* harmony import */ var _internal_observable_of__WEBPACK_IMPORTED_MODULE_41__ = __webpack_require__(227); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "of", function() { return _internal_observable_of__WEBPACK_IMPORTED_MODULE_41__["of"]; }); - -/* harmony import */ var _internal_observable_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_42__ = __webpack_require__(406); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "onErrorResumeNext", function() { return _internal_observable_onErrorResumeNext__WEBPACK_IMPORTED_MODULE_42__["onErrorResumeNext"]; }); - -/* harmony import */ var _internal_observable_pairs__WEBPACK_IMPORTED_MODULE_43__ = __webpack_require__(407); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "pairs", function() { return _internal_observable_pairs__WEBPACK_IMPORTED_MODULE_43__["pairs"]; }); - -/* harmony import */ var _internal_observable_partition__WEBPACK_IMPORTED_MODULE_44__ = __webpack_require__(408); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "partition", function() { return _internal_observable_partition__WEBPACK_IMPORTED_MODULE_44__["partition"]; }); - -/* harmony import */ var _internal_observable_race__WEBPACK_IMPORTED_MODULE_45__ = __webpack_require__(302); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "race", function() { return _internal_observable_race__WEBPACK_IMPORTED_MODULE_45__["race"]; }); - -/* harmony import */ var _internal_observable_range__WEBPACK_IMPORTED_MODULE_46__ = __webpack_require__(409); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "range", function() { return _internal_observable_range__WEBPACK_IMPORTED_MODULE_46__["range"]; }); - -/* harmony import */ var _internal_observable_throwError__WEBPACK_IMPORTED_MODULE_47__ = __webpack_require__(243); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "throwError", function() { return _internal_observable_throwError__WEBPACK_IMPORTED_MODULE_47__["throwError"]; }); - -/* harmony import */ var _internal_observable_timer__WEBPACK_IMPORTED_MODULE_48__ = __webpack_require__(204); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "timer", function() { return _internal_observable_timer__WEBPACK_IMPORTED_MODULE_48__["timer"]; }); - -/* harmony import */ var _internal_observable_using__WEBPACK_IMPORTED_MODULE_49__ = __webpack_require__(410); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "using", function() { return _internal_observable_using__WEBPACK_IMPORTED_MODULE_49__["using"]; }); - -/* harmony import */ var _internal_observable_zip__WEBPACK_IMPORTED_MODULE_50__ = __webpack_require__(346); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "zip", function() { return _internal_observable_zip__WEBPACK_IMPORTED_MODULE_50__["zip"]; }); - -/* harmony import */ var _internal_scheduled_scheduled__WEBPACK_IMPORTED_MODULE_51__ = __webpack_require__(219); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "scheduled", function() { return _internal_scheduled_scheduled__WEBPACK_IMPORTED_MODULE_51__["scheduled"]; }); - -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "EMPTY", function() { return _internal_observable_empty__WEBPACK_IMPORTED_MODULE_31__["EMPTY"]; }); - -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "NEVER", function() { return _internal_observable_never__WEBPACK_IMPORTED_MODULE_40__["NEVER"]; }); - -/* harmony import */ var _internal_config__WEBPACK_IMPORTED_MODULE_52__ = __webpack_require__(175); -/* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "config", function() { return _internal_config__WEBPACK_IMPORTED_MODULE_52__["config"]; }); - -/** PURE_IMPORTS_START PURE_IMPORTS_END */ +}; +const SIGRTMIN=34; +const SIGRTMAX=64;exports.SIGRTMAX=SIGRTMAX; +//# sourceMappingURL=realtime.js.map +/***/ }), +/* 395 */ +/***/ (function(module, exports, __webpack_require__) { +"use strict"; +const aliases = ['stdin', 'stdout', 'stderr']; +const hasAlias = opts => aliases.some(alias => opts[alias] !== undefined); +const normalizeStdio = opts => { + if (!opts) { + return; + } + const {stdio} = opts; + if (stdio === undefined) { + return aliases.map(alias => opts[alias]); + } + if (hasAlias(opts)) { + throw new Error(`It's not possible to provide \`stdio\` in combination with one of ${aliases.map(alias => `\`${alias}\``).join(', ')}`); + } + if (typeof stdio === 'string') { + return stdio; + } + if (!Array.isArray(stdio)) { + throw new TypeError(`Expected \`stdio\` to be of type \`string\` or \`Array\`, got \`${typeof stdio}\``); + } + const length = Math.max(stdio.length, aliases.length); + return Array.from({length}, (value, index) => stdio[index]); +}; +module.exports = normalizeStdio; +// `ipc` is pushed unless it is already present +module.exports.node = opts => { + const stdio = normalizeStdio(opts); + if (stdio === 'ipc') { + return 'ipc'; + } + if (stdio === undefined || typeof stdio === 'string') { + return [stdio, stdio, stdio, 'ipc']; + } + if (stdio.includes('ipc')) { + return stdio; + } + return [...stdio, 'ipc']; +}; +/***/ }), +/* 396 */ +/***/ (function(module, exports, __webpack_require__) { +"use strict"; +const os = __webpack_require__(11); +const onExit = __webpack_require__(397); +const DEFAULT_FORCE_KILL_TIMEOUT = 1000 * 5; +// Monkey-patches `childProcess.kill()` to add `forceKillAfterTimeout` behavior +const spawnedKill = (kill, signal = 'SIGTERM', options = {}) => { + const killResult = kill(signal); + setKillTimeout(kill, signal, options, killResult); + return killResult; +}; +const setKillTimeout = (kill, signal, options, killResult) => { + if (!shouldForceKill(signal, options, killResult)) { + return; + } + const timeout = getForceKillAfterTimeout(options); + const t = setTimeout(() => { + kill('SIGKILL'); + }, timeout); + // Guarded because there's no `.unref()` when `execa` is used in the renderer + // process in Electron. This cannot be tested since we don't run tests in + // Electron. + // istanbul ignore else + if (t.unref) { + t.unref(); + } +}; +const shouldForceKill = (signal, {forceKillAfterTimeout}, killResult) => { + return isSigterm(signal) && forceKillAfterTimeout !== false && killResult; +}; +const isSigterm = signal => { + return signal === os.constants.signals.SIGTERM || + (typeof signal === 'string' && signal.toUpperCase() === 'SIGTERM'); +}; +const getForceKillAfterTimeout = ({forceKillAfterTimeout = true}) => { + if (forceKillAfterTimeout === true) { + return DEFAULT_FORCE_KILL_TIMEOUT; + } + if (!Number.isInteger(forceKillAfterTimeout) || forceKillAfterTimeout < 0) { + throw new TypeError(`Expected the \`forceKillAfterTimeout\` option to be a non-negative integer, got \`${forceKillAfterTimeout}\` (${typeof forceKillAfterTimeout})`); + } + return forceKillAfterTimeout; +}; +// `childProcess.cancel()` +const spawnedCancel = (spawned, context) => { + const killResult = spawned.kill(); + if (killResult) { + context.isCanceled = true; + } +}; +const timeoutKill = (spawned, signal, reject) => { + spawned.kill(signal); + reject(Object.assign(new Error('Timed out'), {timedOut: true, signal})); +}; +// `timeout` option handling +const setupTimeout = (spawned, {timeout, killSignal = 'SIGTERM'}, spawnedPromise) => { + if (timeout === 0 || timeout === undefined) { + return spawnedPromise; + } + if (!Number.isInteger(timeout) || timeout < 0) { + throw new TypeError(`Expected the \`timeout\` option to be a non-negative integer, got \`${timeout}\` (${typeof timeout})`); + } + let timeoutId; + const timeoutPromise = new Promise((resolve, reject) => { + timeoutId = setTimeout(() => { + timeoutKill(spawned, killSignal, reject); + }, timeout); + }); + const safeSpawnedPromise = spawnedPromise.finally(() => { + clearTimeout(timeoutId); + }); + return Promise.race([timeoutPromise, safeSpawnedPromise]); +}; +// `cleanup` option handling +const setExitHandler = async (spawned, {cleanup, detached}, timedPromise) => { + if (!cleanup || detached) { + return timedPromise; + } + const removeExitHandler = onExit(() => { + spawned.kill(); + }); + return timedPromise.finally(() => { + removeExitHandler(); + }); +}; +module.exports = { + spawnedKill, + spawnedCancel, + setupTimeout, + setExitHandler +}; +/***/ }), +/* 397 */ +/***/ (function(module, exports, __webpack_require__) { +// Note: since nyc uses this module to output coverage, any lines +// that are in the direct sync flow of nyc's outputCoverage are +// ignored, since we can never get coverage for them. +var assert = __webpack_require__(30) +var signals = __webpack_require__(398) +var EE = __webpack_require__(399) +/* istanbul ignore if */ +if (typeof EE !== 'function') { + EE = EE.EventEmitter +} +var emitter +if (process.__signal_exit_emitter__) { + emitter = process.__signal_exit_emitter__ +} else { + emitter = process.__signal_exit_emitter__ = new EE() + emitter.count = 0 + emitter.emitted = {} +} +// Because this emitter is a global, we have to check to see if a +// previous version of this library failed to enable infinite listeners. +// I know what you're about to say. But literally everything about +// signal-exit is a compromise with evil. Get used to it. +if (!emitter.infinite) { + emitter.setMaxListeners(Infinity) + emitter.infinite = true +} +module.exports = function (cb, opts) { + assert.equal(typeof cb, 'function', 'a callback must be provided for exit handler') + if (loaded === false) { + load() + } + var ev = 'exit' + if (opts && opts.alwaysLast) { + ev = 'afterexit' + } + var remove = function () { + emitter.removeListener(ev, cb) + if (emitter.listeners('exit').length === 0 && + emitter.listeners('afterexit').length === 0) { + unload() + } + } + emitter.on(ev, cb) -//# sourceMappingURL=index.js.map + return remove +} +module.exports.unload = unload +function unload () { + if (!loaded) { + return + } + loaded = false -/***/ }), -/* 392 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { + signals.forEach(function (sig) { + try { + process.removeListener(sig, sigListeners[sig]) + } catch (er) {} + }) + process.emit = originalProcessEmit + process.reallyExit = originalProcessReallyExit + emitter.count -= 1 +} -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "animationFrame", function() { return animationFrame; }); -/* harmony import */ var _AnimationFrameAction__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(393); -/* harmony import */ var _AnimationFrameScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(394); -/** PURE_IMPORTS_START _AnimationFrameAction,_AnimationFrameScheduler PURE_IMPORTS_END */ +function emit (event, code, signal) { + if (emitter.emitted[event]) { + return + } + emitter.emitted[event] = true + emitter.emit(event, code, signal) +} +// { : , ... } +var sigListeners = {} +signals.forEach(function (sig) { + sigListeners[sig] = function listener () { + // If there are no other listeners, an exit is coming! + // Simplest way: remove us and then re-send the signal. + // We know that this will kill the process, so we can + // safely emit now. + var listeners = process.listeners(sig) + if (listeners.length === emitter.count) { + unload() + emit('exit', null, sig) + /* istanbul ignore next */ + emit('afterexit', null, sig) + /* istanbul ignore next */ + process.kill(process.pid, sig) + } + } +}) -var animationFrame = /*@__PURE__*/ new _AnimationFrameScheduler__WEBPACK_IMPORTED_MODULE_1__["AnimationFrameScheduler"](_AnimationFrameAction__WEBPACK_IMPORTED_MODULE_0__["AnimationFrameAction"]); -//# sourceMappingURL=animationFrame.js.map +module.exports.signals = function () { + return signals +} +module.exports.load = load -/***/ }), -/* 393 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { +var loaded = false -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "AnimationFrameAction", function() { return AnimationFrameAction; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _AsyncAction__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(200); -/** PURE_IMPORTS_START tslib,_AsyncAction PURE_IMPORTS_END */ +function load () { + if (loaded) { + return + } + loaded = true + // This is the number of onSignalExit's that are in play. + // It's important so that we can count the correct number of + // listeners on signals, and don't wait for the other one to + // handle it instead of us. + emitter.count += 1 -var AnimationFrameAction = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](AnimationFrameAction, _super); - function AnimationFrameAction(scheduler, work) { - var _this = _super.call(this, scheduler, work) || this; - _this.scheduler = scheduler; - _this.work = work; - return _this; + signals = signals.filter(function (sig) { + try { + process.on(sig, sigListeners[sig]) + return true + } catch (er) { + return false } - AnimationFrameAction.prototype.requestAsyncId = function (scheduler, id, delay) { - if (delay === void 0) { - delay = 0; - } - if (delay !== null && delay > 0) { - return _super.prototype.requestAsyncId.call(this, scheduler, id, delay); - } - scheduler.actions.push(this); - return scheduler.scheduled || (scheduler.scheduled = requestAnimationFrame(function () { return scheduler.flush(null); })); - }; - AnimationFrameAction.prototype.recycleAsyncId = function (scheduler, id, delay) { - if (delay === void 0) { - delay = 0; - } - if ((delay !== null && delay > 0) || (delay === null && this.delay > 0)) { - return _super.prototype.recycleAsyncId.call(this, scheduler, id, delay); - } - if (scheduler.actions.length === 0) { - cancelAnimationFrame(id); - scheduler.scheduled = undefined; - } - return undefined; - }; - return AnimationFrameAction; -}(_AsyncAction__WEBPACK_IMPORTED_MODULE_1__["AsyncAction"])); - -//# sourceMappingURL=AnimationFrameAction.js.map - - -/***/ }), -/* 394 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { + }) -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "AnimationFrameScheduler", function() { return AnimationFrameScheduler; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _AsyncScheduler__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(202); -/** PURE_IMPORTS_START tslib,_AsyncScheduler PURE_IMPORTS_END */ + process.emit = processEmit + process.reallyExit = processReallyExit +} +var originalProcessReallyExit = process.reallyExit +function processReallyExit (code) { + process.exitCode = code || 0 + emit('exit', process.exitCode, null) + /* istanbul ignore next */ + emit('afterexit', process.exitCode, null) + /* istanbul ignore next */ + originalProcessReallyExit.call(process, process.exitCode) +} -var AnimationFrameScheduler = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](AnimationFrameScheduler, _super); - function AnimationFrameScheduler() { - return _super !== null && _super.apply(this, arguments) || this; +var originalProcessEmit = process.emit +function processEmit (ev, arg) { + if (ev === 'exit') { + if (arg !== undefined) { + process.exitCode = arg } - AnimationFrameScheduler.prototype.flush = function (action) { - this.active = true; - this.scheduled = undefined; - var actions = this.actions; - var error; - var index = -1; - var count = actions.length; - action = action || actions.shift(); - do { - if (error = action.execute(action.state, action.delay)) { - break; - } - } while (++index < count && (action = actions.shift())); - this.active = false; - if (error) { - while (++index < count && (action = actions.shift())) { - action.unsubscribe(); - } - throw error; - } - }; - return AnimationFrameScheduler; -}(_AsyncScheduler__WEBPACK_IMPORTED_MODULE_1__["AsyncScheduler"])); - -//# sourceMappingURL=AnimationFrameScheduler.js.map + var ret = originalProcessEmit.apply(this, arguments) + emit('exit', process.exitCode, null) + /* istanbul ignore next */ + emit('afterexit', process.exitCode, null) + return ret + } else { + return originalProcessEmit.apply(this, arguments) + } +} /***/ }), -/* 395 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "VirtualTimeScheduler", function() { return VirtualTimeScheduler; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "VirtualAction", function() { return VirtualAction; }); -/* harmony import */ var tslib__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(36); -/* harmony import */ var _AsyncAction__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(200); -/* harmony import */ var _AsyncScheduler__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(202); -/** PURE_IMPORTS_START tslib,_AsyncAction,_AsyncScheduler PURE_IMPORTS_END */ - - +/* 398 */ +/***/ (function(module, exports) { -var VirtualTimeScheduler = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](VirtualTimeScheduler, _super); - function VirtualTimeScheduler(SchedulerAction, maxFrames) { - if (SchedulerAction === void 0) { - SchedulerAction = VirtualAction; - } - if (maxFrames === void 0) { - maxFrames = Number.POSITIVE_INFINITY; - } - var _this = _super.call(this, SchedulerAction, function () { return _this.frame; }) || this; - _this.maxFrames = maxFrames; - _this.frame = 0; - _this.index = -1; - return _this; - } - VirtualTimeScheduler.prototype.flush = function () { - var _a = this, actions = _a.actions, maxFrames = _a.maxFrames; - var error, action; - while ((action = actions[0]) && action.delay <= maxFrames) { - actions.shift(); - this.frame = action.delay; - if (error = action.execute(action.state, action.delay)) { - break; - } - } - if (error) { - while (action = actions.shift()) { - action.unsubscribe(); - } - throw error; - } - }; - VirtualTimeScheduler.frameTimeFactor = 10; - return VirtualTimeScheduler; -}(_AsyncScheduler__WEBPACK_IMPORTED_MODULE_2__["AsyncScheduler"])); +// This is not the set of all possible signals. +// +// It IS, however, the set of all signals that trigger +// an exit on either Linux or BSD systems. Linux is a +// superset of the signal names supported on BSD, and +// the unknown signals just fail to register, so we can +// catch that easily enough. +// +// Don't bother with SIGKILL. It's uncatchable, which +// means that we can't fire any callbacks anyway. +// +// If a user does happen to register a handler on a non- +// fatal signal like SIGWINCH or something, and then +// exit, it'll end up firing `process.emit('exit')`, so +// the handler will be fired anyway. +// +// SIGBUS, SIGFPE, SIGSEGV and SIGILL, when not raised +// artificially, inherently leave the process in a +// state from which it is not safe to try and enter JS +// listeners. +module.exports = [ + 'SIGABRT', + 'SIGALRM', + 'SIGHUP', + 'SIGINT', + 'SIGTERM' +] -var VirtualAction = /*@__PURE__*/ (function (_super) { - tslib__WEBPACK_IMPORTED_MODULE_0__["__extends"](VirtualAction, _super); - function VirtualAction(scheduler, work, index) { - if (index === void 0) { - index = scheduler.index += 1; - } - var _this = _super.call(this, scheduler, work) || this; - _this.scheduler = scheduler; - _this.work = work; - _this.index = index; - _this.active = true; - _this.index = scheduler.index = index; - return _this; - } - VirtualAction.prototype.schedule = function (state, delay) { - if (delay === void 0) { - delay = 0; - } - if (!this.id) { - return _super.prototype.schedule.call(this, state, delay); - } - this.active = false; - var action = new VirtualAction(this.scheduler, this.work); - this.add(action); - return action.schedule(state, delay); - }; - VirtualAction.prototype.requestAsyncId = function (scheduler, id, delay) { - if (delay === void 0) { - delay = 0; - } - this.delay = scheduler.frame + delay; - var actions = scheduler.actions; - actions.push(this); - actions.sort(VirtualAction.sortActions); - return true; - }; - VirtualAction.prototype.recycleAsyncId = function (scheduler, id, delay) { - if (delay === void 0) { - delay = 0; - } - return undefined; - }; - VirtualAction.prototype._execute = function (state, delay) { - if (this.active === true) { - return _super.prototype._execute.call(this, state, delay); - } - }; - VirtualAction.sortActions = function (a, b) { - if (a.delay === b.delay) { - if (a.index === b.index) { - return 0; - } - else if (a.index > b.index) { - return 1; - } - else { - return -1; - } - } - else if (a.delay > b.delay) { - return 1; - } - else { - return -1; - } - }; - return VirtualAction; -}(_AsyncAction__WEBPACK_IMPORTED_MODULE_1__["AsyncAction"])); +if (process.platform !== 'win32') { + module.exports.push( + 'SIGVTALRM', + 'SIGXCPU', + 'SIGXFSZ', + 'SIGUSR2', + 'SIGTRAP', + 'SIGSYS', + 'SIGQUIT', + 'SIGIOT' + // should detect profiler and enable/disable accordingly. + // see #21 + // 'SIGPROF' + ) +} -//# sourceMappingURL=VirtualTimeScheduler.js.map +if (process.platform === 'linux') { + module.exports.push( + 'SIGIO', + 'SIGPOLL', + 'SIGPWR', + 'SIGSTKFLT', + 'SIGUNUSED' + ) +} /***/ }), -/* 396 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { - -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "isObservable", function() { return isObservable; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(193); -/** PURE_IMPORTS_START _Observable PURE_IMPORTS_END */ - -function isObservable(obj) { - return !!obj && (obj instanceof _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"] || (typeof obj.lift === 'function' && typeof obj.subscribe === 'function')); -} -//# sourceMappingURL=isObservable.js.map +/* 399 */ +/***/ (function(module, exports) { +module.exports = require("events"); /***/ }), -/* 397 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { +/* 400 */ +/***/ (function(module, exports, __webpack_require__) { "use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "bindCallback", function() { return bindCallback; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(193); -/* harmony import */ var _AsyncSubject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(295); -/* harmony import */ var _operators_map__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(231); -/* harmony import */ var _util_canReportError__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(194); -/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(178); -/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(206); -/** PURE_IMPORTS_START _Observable,_AsyncSubject,_operators_map,_util_canReportError,_util_isArray,_util_isScheduler PURE_IMPORTS_END */ - - - +const isStream = __webpack_require__(401); +const getStream = __webpack_require__(402); +const mergeStream = __webpack_require__(408); +// `input` option +const handleInput = (spawned, input) => { + // Checking for stdin is workaround for https://github.com/nodejs/node/issues/26852 + // TODO: Remove `|| spawned.stdin === undefined` once we drop support for Node.js <=12.2.0 + if (input === undefined || spawned.stdin === undefined) { + return; + } -function bindCallback(callbackFunc, resultSelector, scheduler) { - if (resultSelector) { - if (Object(_util_isScheduler__WEBPACK_IMPORTED_MODULE_5__["isScheduler"])(resultSelector)) { - scheduler = resultSelector; - } - else { - return function () { - var args = []; - for (var _i = 0; _i < arguments.length; _i++) { - args[_i] = arguments[_i]; - } - return bindCallback(callbackFunc, scheduler).apply(void 0, args).pipe(Object(_operators_map__WEBPACK_IMPORTED_MODULE_2__["map"])(function (args) { return Object(_util_isArray__WEBPACK_IMPORTED_MODULE_4__["isArray"])(args) ? resultSelector.apply(void 0, args) : resultSelector(args); })); - }; - } - } - return function () { - var args = []; - for (var _i = 0; _i < arguments.length; _i++) { - args[_i] = arguments[_i]; - } - var context = this; - var subject; - var params = { - context: context, - subject: subject, - callbackFunc: callbackFunc, - scheduler: scheduler, - }; - return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { - if (!scheduler) { - if (!subject) { - subject = new _AsyncSubject__WEBPACK_IMPORTED_MODULE_1__["AsyncSubject"](); - var handler = function () { - var innerArgs = []; - for (var _i = 0; _i < arguments.length; _i++) { - innerArgs[_i] = arguments[_i]; - } - subject.next(innerArgs.length <= 1 ? innerArgs[0] : innerArgs); - subject.complete(); - }; - try { - callbackFunc.apply(context, args.concat([handler])); - } - catch (err) { - if (Object(_util_canReportError__WEBPACK_IMPORTED_MODULE_3__["canReportError"])(subject)) { - subject.error(err); - } - else { - console.warn(err); - } - } - } - return subject.subscribe(subscriber); - } - else { - var state = { - args: args, subscriber: subscriber, params: params, - }; - return scheduler.schedule(dispatch, 0, state); - } - }); - }; -} -function dispatch(state) { - var _this = this; - var self = this; - var args = state.args, subscriber = state.subscriber, params = state.params; - var callbackFunc = params.callbackFunc, context = params.context, scheduler = params.scheduler; - var subject = params.subject; - if (!subject) { - subject = params.subject = new _AsyncSubject__WEBPACK_IMPORTED_MODULE_1__["AsyncSubject"](); - var handler = function () { - var innerArgs = []; - for (var _i = 0; _i < arguments.length; _i++) { - innerArgs[_i] = arguments[_i]; - } - var value = innerArgs.length <= 1 ? innerArgs[0] : innerArgs; - _this.add(scheduler.schedule(dispatchNext, 0, { value: value, subject: subject })); - }; - try { - callbackFunc.apply(context, args.concat([handler])); - } - catch (err) { - subject.error(err); - } - } - this.add(subject.subscribe(subscriber)); -} -function dispatchNext(state) { - var value = state.value, subject = state.subject; - subject.next(value); - subject.complete(); -} -function dispatchError(state) { - var err = state.err, subject = state.subject; - subject.error(err); -} -//# sourceMappingURL=bindCallback.js.map + if (isStream(input)) { + input.pipe(spawned.stdin); + } else { + spawned.stdin.end(input); + } +}; +// `all` interleaves `stdout` and `stderr` +const makeAllStream = (spawned, {all}) => { + if (!all || (!spawned.stdout && !spawned.stderr)) { + return; + } -/***/ }), -/* 398 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { + const mixed = mergeStream(); -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "bindNodeCallback", function() { return bindNodeCallback; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(193); -/* harmony import */ var _AsyncSubject__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(295); -/* harmony import */ var _operators_map__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(231); -/* harmony import */ var _util_canReportError__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(194); -/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(206); -/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(178); -/** PURE_IMPORTS_START _Observable,_AsyncSubject,_operators_map,_util_canReportError,_util_isScheduler,_util_isArray PURE_IMPORTS_END */ + if (spawned.stdout) { + mixed.add(spawned.stdout); + } + if (spawned.stderr) { + mixed.add(spawned.stderr); + } + return mixed; +}; +// On failure, `result.stdout|stderr|all` should contain the currently buffered stream +const getBufferedData = async (stream, streamPromise) => { + if (!stream) { + return; + } + stream.destroy(); + try { + return await streamPromise; + } catch (error) { + return error.bufferedData; + } +}; + +const getStreamPromise = (stream, {encoding, buffer, maxBuffer}) => { + if (!stream || !buffer) { + return; + } + + if (encoding) { + return getStream(stream, {encoding, maxBuffer}); + } + + return getStream.buffer(stream, {maxBuffer}); +}; + +// Retrieve result of child process: exit code, signal, error, streams (stdout/stderr/all) +const getSpawnedResult = async ({stdout, stderr, all}, {encoding, buffer, maxBuffer}, processDone) => { + const stdoutPromise = getStreamPromise(stdout, {encoding, buffer, maxBuffer}); + const stderrPromise = getStreamPromise(stderr, {encoding, buffer, maxBuffer}); + const allPromise = getStreamPromise(all, {encoding, buffer, maxBuffer: maxBuffer * 2}); + + try { + return await Promise.all([processDone, stdoutPromise, stderrPromise, allPromise]); + } catch (error) { + return Promise.all([ + {error, signal: error.signal, timedOut: error.timedOut}, + getBufferedData(stdout, stdoutPromise), + getBufferedData(stderr, stderrPromise), + getBufferedData(all, allPromise) + ]); + } +}; + +const validateInputSync = ({input}) => { + if (isStream(input)) { + throw new TypeError('The `input` option cannot be a stream in sync mode'); + } +}; + +module.exports = { + handleInput, + makeAllStream, + getSpawnedResult, + validateInputSync +}; -function bindNodeCallback(callbackFunc, resultSelector, scheduler) { - if (resultSelector) { - if (Object(_util_isScheduler__WEBPACK_IMPORTED_MODULE_4__["isScheduler"])(resultSelector)) { - scheduler = resultSelector; - } - else { - return function () { - var args = []; - for (var _i = 0; _i < arguments.length; _i++) { - args[_i] = arguments[_i]; - } - return bindNodeCallback(callbackFunc, scheduler).apply(void 0, args).pipe(Object(_operators_map__WEBPACK_IMPORTED_MODULE_2__["map"])(function (args) { return Object(_util_isArray__WEBPACK_IMPORTED_MODULE_5__["isArray"])(args) ? resultSelector.apply(void 0, args) : resultSelector(args); })); - }; - } - } - return function () { - var args = []; - for (var _i = 0; _i < arguments.length; _i++) { - args[_i] = arguments[_i]; - } - var params = { - subject: undefined, - args: args, - callbackFunc: callbackFunc, - scheduler: scheduler, - context: this, - }; - return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { - var context = params.context; - var subject = params.subject; - if (!scheduler) { - if (!subject) { - subject = params.subject = new _AsyncSubject__WEBPACK_IMPORTED_MODULE_1__["AsyncSubject"](); - var handler = function () { - var innerArgs = []; - for (var _i = 0; _i < arguments.length; _i++) { - innerArgs[_i] = arguments[_i]; - } - var err = innerArgs.shift(); - if (err) { - subject.error(err); - return; - } - subject.next(innerArgs.length <= 1 ? innerArgs[0] : innerArgs); - subject.complete(); - }; - try { - callbackFunc.apply(context, args.concat([handler])); - } - catch (err) { - if (Object(_util_canReportError__WEBPACK_IMPORTED_MODULE_3__["canReportError"])(subject)) { - subject.error(err); - } - else { - console.warn(err); - } - } - } - return subject.subscribe(subscriber); - } - else { - return scheduler.schedule(dispatch, 0, { params: params, subscriber: subscriber, context: context }); - } - }); - }; -} -function dispatch(state) { - var _this = this; - var params = state.params, subscriber = state.subscriber, context = state.context; - var callbackFunc = params.callbackFunc, args = params.args, scheduler = params.scheduler; - var subject = params.subject; - if (!subject) { - subject = params.subject = new _AsyncSubject__WEBPACK_IMPORTED_MODULE_1__["AsyncSubject"](); - var handler = function () { - var innerArgs = []; - for (var _i = 0; _i < arguments.length; _i++) { - innerArgs[_i] = arguments[_i]; - } - var err = innerArgs.shift(); - if (err) { - _this.add(scheduler.schedule(dispatchError, 0, { err: err, subject: subject })); - } - else { - var value = innerArgs.length <= 1 ? innerArgs[0] : innerArgs; - _this.add(scheduler.schedule(dispatchNext, 0, { value: value, subject: subject })); - } - }; - try { - callbackFunc.apply(context, args.concat([handler])); - } - catch (err) { - this.add(scheduler.schedule(dispatchError, 0, { err: err, subject: subject })); - } - } - this.add(subject.subscribe(subscriber)); -} -function dispatchNext(arg) { - var value = arg.value, subject = arg.subject; - subject.next(value); - subject.complete(); -} -function dispatchError(arg) { - var err = arg.err, subject = arg.subject; - subject.error(err); -} -//# sourceMappingURL=bindNodeCallback.js.map /***/ }), -/* 399 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { +/* 401 */ +/***/ (function(module, exports, __webpack_require__) { "use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "forkJoin", function() { return forkJoin; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(193); -/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(178); -/* harmony import */ var _operators_map__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(231); -/* harmony import */ var _util_isObject__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(179); -/* harmony import */ var _from__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(218); -/** PURE_IMPORTS_START _Observable,_util_isArray,_operators_map,_util_isObject,_from PURE_IMPORTS_END */ +const isStream = stream => + stream !== null && + typeof stream === 'object' && + typeof stream.pipe === 'function'; +isStream.writable = stream => + isStream(stream) && + stream.writable !== false && + typeof stream._write === 'function' && + typeof stream._writableState === 'object'; +isStream.readable = stream => + isStream(stream) && + stream.readable !== false && + typeof stream._read === 'function' && + typeof stream._readableState === 'object'; -function forkJoin() { - var sources = []; - for (var _i = 0; _i < arguments.length; _i++) { - sources[_i] = arguments[_i]; - } - if (sources.length === 1) { - var first_1 = sources[0]; - if (Object(_util_isArray__WEBPACK_IMPORTED_MODULE_1__["isArray"])(first_1)) { - return forkJoinInternal(first_1, null); - } - if (Object(_util_isObject__WEBPACK_IMPORTED_MODULE_3__["isObject"])(first_1) && Object.getPrototypeOf(first_1) === Object.prototype) { - var keys = Object.keys(first_1); - return forkJoinInternal(keys.map(function (key) { return first_1[key]; }), keys); - } - } - if (typeof sources[sources.length - 1] === 'function') { - var resultSelector_1 = sources.pop(); - sources = (sources.length === 1 && Object(_util_isArray__WEBPACK_IMPORTED_MODULE_1__["isArray"])(sources[0])) ? sources[0] : sources; - return forkJoinInternal(sources, null).pipe(Object(_operators_map__WEBPACK_IMPORTED_MODULE_2__["map"])(function (args) { return resultSelector_1.apply(void 0, args); })); - } - return forkJoinInternal(sources, null); -} -function forkJoinInternal(sources, keys) { - return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { - var len = sources.length; - if (len === 0) { - subscriber.complete(); - return; - } - var values = new Array(len); - var completed = 0; - var emitted = 0; - var _loop_1 = function (i) { - var source = Object(_from__WEBPACK_IMPORTED_MODULE_4__["from"])(sources[i]); - var hasValue = false; - subscriber.add(source.subscribe({ - next: function (value) { - if (!hasValue) { - hasValue = true; - emitted++; - } - values[i] = value; - }, - error: function (err) { return subscriber.error(err); }, - complete: function () { - completed++; - if (completed === len || !hasValue) { - if (emitted === len) { - subscriber.next(keys ? - keys.reduce(function (result, key, i) { return (result[key] = values[i], result); }, {}) : - values); - } - subscriber.complete(); - } - } - })); - }; - for (var i = 0; i < len; i++) { - _loop_1(i); - } - }); -} -//# sourceMappingURL=forkJoin.js.map +isStream.duplex = stream => + isStream.writable(stream) && + isStream.readable(stream); + +isStream.transform = stream => + isStream.duplex(stream) && + typeof stream._transform === 'function' && + typeof stream._transformState === 'object'; + +module.exports = isStream; /***/ }), -/* 400 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { +/* 402 */ +/***/ (function(module, exports, __webpack_require__) { "use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "fromEvent", function() { return fromEvent; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(193); -/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(178); -/* harmony import */ var _util_isFunction__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(173); -/* harmony import */ var _operators_map__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(231); -/** PURE_IMPORTS_START _Observable,_util_isArray,_util_isFunction,_operators_map PURE_IMPORTS_END */ +const pump = __webpack_require__(403); +const bufferStream = __webpack_require__(407); +class MaxBufferError extends Error { + constructor() { + super('maxBuffer exceeded'); + this.name = 'MaxBufferError'; + } +} +async function getStream(inputStream, options) { + if (!inputStream) { + return Promise.reject(new Error('Expected a stream')); + } -var toString = /*@__PURE__*/ (function () { return Object.prototype.toString; })(); -function fromEvent(target, eventName, options, resultSelector) { - if (Object(_util_isFunction__WEBPACK_IMPORTED_MODULE_2__["isFunction"])(options)) { - resultSelector = options; - options = undefined; - } - if (resultSelector) { - return fromEvent(target, eventName, options).pipe(Object(_operators_map__WEBPACK_IMPORTED_MODULE_3__["map"])(function (args) { return Object(_util_isArray__WEBPACK_IMPORTED_MODULE_1__["isArray"])(args) ? resultSelector.apply(void 0, args) : resultSelector(args); })); - } - return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { - function handler(e) { - if (arguments.length > 1) { - subscriber.next(Array.prototype.slice.call(arguments)); - } - else { - subscriber.next(e); - } - } - setupSubscription(target, eventName, handler, subscriber, options); - }); -} -function setupSubscription(sourceObj, eventName, handler, subscriber, options) { - var unsubscribe; - if (isEventTarget(sourceObj)) { - var source_1 = sourceObj; - sourceObj.addEventListener(eventName, handler, options); - unsubscribe = function () { return source_1.removeEventListener(eventName, handler, options); }; - } - else if (isJQueryStyleEventEmitter(sourceObj)) { - var source_2 = sourceObj; - sourceObj.on(eventName, handler); - unsubscribe = function () { return source_2.off(eventName, handler); }; - } - else if (isNodeStyleEventEmitter(sourceObj)) { - var source_3 = sourceObj; - sourceObj.addListener(eventName, handler); - unsubscribe = function () { return source_3.removeListener(eventName, handler); }; - } - else if (sourceObj && sourceObj.length) { - for (var i = 0, len = sourceObj.length; i < len; i++) { - setupSubscription(sourceObj[i], eventName, handler, subscriber, options); - } - } - else { - throw new TypeError('Invalid event target'); - } - subscriber.add(unsubscribe); + options = { + maxBuffer: Infinity, + ...options + }; + + const {maxBuffer} = options; + + let stream; + await new Promise((resolve, reject) => { + const rejectPromise = error => { + if (error) { // A null check + error.bufferedData = stream.getBufferedValue(); + } + + reject(error); + }; + + stream = pump(inputStream, bufferStream(options), error => { + if (error) { + rejectPromise(error); + return; + } + + resolve(); + }); + + stream.on('data', () => { + if (stream.getBufferedLength() > maxBuffer) { + rejectPromise(new MaxBufferError()); + } + }); + }); + + return stream.getBufferedValue(); } -function isNodeStyleEventEmitter(sourceObj) { - return sourceObj && typeof sourceObj.addListener === 'function' && typeof sourceObj.removeListener === 'function'; + +module.exports = getStream; +// TODO: Remove this for the next major release +module.exports.default = getStream; +module.exports.buffer = (stream, options) => getStream(stream, {...options, encoding: 'buffer'}); +module.exports.array = (stream, options) => getStream(stream, {...options, array: true}); +module.exports.MaxBufferError = MaxBufferError; + + +/***/ }), +/* 403 */ +/***/ (function(module, exports, __webpack_require__) { + +var once = __webpack_require__(404) +var eos = __webpack_require__(406) +var fs = __webpack_require__(23) // we only need fs to get the ReadStream and WriteStream prototypes + +var noop = function () {} +var ancient = /^v?\.0/.test(process.version) + +var isFn = function (fn) { + return typeof fn === 'function' } -function isJQueryStyleEventEmitter(sourceObj) { - return sourceObj && typeof sourceObj.on === 'function' && typeof sourceObj.off === 'function'; + +var isFS = function (stream) { + if (!ancient) return false // newer node version do not need to care about fs is a special way + if (!fs) return false // browser + return (stream instanceof (fs.ReadStream || noop) || stream instanceof (fs.WriteStream || noop)) && isFn(stream.close) } -function isEventTarget(sourceObj) { - return sourceObj && typeof sourceObj.addEventListener === 'function' && typeof sourceObj.removeEventListener === 'function'; + +var isRequest = function (stream) { + return stream.setHeader && isFn(stream.abort) } -//# sourceMappingURL=fromEvent.js.map +var destroyer = function (stream, reading, writing, callback) { + callback = once(callback) -/***/ }), -/* 401 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { + var closed = false + stream.on('close', function () { + closed = true + }) -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "fromEventPattern", function() { return fromEventPattern; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(193); -/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(178); -/* harmony import */ var _util_isFunction__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(173); -/* harmony import */ var _operators_map__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(231); -/** PURE_IMPORTS_START _Observable,_util_isArray,_util_isFunction,_operators_map PURE_IMPORTS_END */ + eos(stream, {readable: reading, writable: writing}, function (err) { + if (err) return callback(err) + closed = true + callback() + }) + var destroyed = false + return function (err) { + if (closed) return + if (destroyed) return + destroyed = true + if (isFS(stream)) return stream.close(noop) // use close for fs streams to avoid fd leaks + if (isRequest(stream)) return stream.abort() // request.destroy just do .end - .abort is what we want + if (isFn(stream.destroy)) return stream.destroy() -function fromEventPattern(addHandler, removeHandler, resultSelector) { - if (resultSelector) { - return fromEventPattern(addHandler, removeHandler).pipe(Object(_operators_map__WEBPACK_IMPORTED_MODULE_3__["map"])(function (args) { return Object(_util_isArray__WEBPACK_IMPORTED_MODULE_1__["isArray"])(args) ? resultSelector.apply(void 0, args) : resultSelector(args); })); - } - return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { - var handler = function () { - var e = []; - for (var _i = 0; _i < arguments.length; _i++) { - e[_i] = arguments[_i]; - } - return subscriber.next(e.length === 1 ? e[0] : e); - }; - var retValue; - try { - retValue = addHandler(handler); - } - catch (err) { - subscriber.error(err); - return undefined; - } - if (!Object(_util_isFunction__WEBPACK_IMPORTED_MODULE_2__["isFunction"])(removeHandler)) { - return undefined; - } - return function () { return removeHandler(handler, retValue); }; - }); + callback(err || new Error('stream was destroyed')) + } } -//# sourceMappingURL=fromEventPattern.js.map +var call = function (fn) { + fn() +} -/***/ }), -/* 402 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { +var pipe = function (from, to) { + return from.pipe(to) +} -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "generate", function() { return generate; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(193); -/* harmony import */ var _util_identity__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(232); -/* harmony import */ var _util_isScheduler__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(206); -/** PURE_IMPORTS_START _Observable,_util_identity,_util_isScheduler PURE_IMPORTS_END */ +var pump = function () { + var streams = Array.prototype.slice.call(arguments) + var callback = isFn(streams[streams.length - 1] || noop) && streams.pop() || noop + if (Array.isArray(streams[0])) streams = streams[0] + if (streams.length < 2) throw new Error('pump requires two streams per minimum') + var error + var destroys = streams.map(function (stream, i) { + var reading = i < streams.length - 1 + var writing = i > 0 + return destroyer(stream, reading, writing, function (err) { + if (!error) error = err + if (err) destroys.forEach(call) + if (reading) return + destroys.forEach(call) + callback(error) + }) + }) -function generate(initialStateOrOptions, condition, iterate, resultSelectorOrObservable, scheduler) { - var resultSelector; - var initialState; - if (arguments.length == 1) { - var options = initialStateOrOptions; - initialState = options.initialState; - condition = options.condition; - iterate = options.iterate; - resultSelector = options.resultSelector || _util_identity__WEBPACK_IMPORTED_MODULE_1__["identity"]; - scheduler = options.scheduler; - } - else if (resultSelectorOrObservable === undefined || Object(_util_isScheduler__WEBPACK_IMPORTED_MODULE_2__["isScheduler"])(resultSelectorOrObservable)) { - initialState = initialStateOrOptions; - resultSelector = _util_identity__WEBPACK_IMPORTED_MODULE_1__["identity"]; - scheduler = resultSelectorOrObservable; - } - else { - initialState = initialStateOrOptions; - resultSelector = resultSelectorOrObservable; - } - return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { - var state = initialState; - if (scheduler) { - return scheduler.schedule(dispatch, 0, { - subscriber: subscriber, - iterate: iterate, - condition: condition, - resultSelector: resultSelector, - state: state - }); - } - do { - if (condition) { - var conditionResult = void 0; - try { - conditionResult = condition(state); - } - catch (err) { - subscriber.error(err); - return undefined; - } - if (!conditionResult) { - subscriber.complete(); - break; - } - } - var value = void 0; - try { - value = resultSelector(state); - } - catch (err) { - subscriber.error(err); - return undefined; - } - subscriber.next(value); - if (subscriber.closed) { - break; - } - try { - state = iterate(state); - } - catch (err) { - subscriber.error(err); - return undefined; - } - } while (true); - return undefined; - }); + return streams.reduce(pipe) } -function dispatch(state) { - var subscriber = state.subscriber, condition = state.condition; - if (subscriber.closed) { - return undefined; - } - if (state.needIterate) { - try { - state.state = state.iterate(state.state); - } - catch (err) { - subscriber.error(err); - return undefined; - } - } - else { - state.needIterate = true; - } - if (condition) { - var conditionResult = void 0; - try { - conditionResult = condition(state.state); - } - catch (err) { - subscriber.error(err); - return undefined; - } - if (!conditionResult) { - subscriber.complete(); - return undefined; - } - if (subscriber.closed) { - return undefined; - } - } - var value; - try { - value = state.resultSelector(state.state); - } - catch (err) { - subscriber.error(err); - return undefined; - } - if (subscriber.closed) { - return undefined; - } - subscriber.next(value); - if (subscriber.closed) { - return undefined; - } - return this.schedule(state); + +module.exports = pump + + +/***/ }), +/* 404 */ +/***/ (function(module, exports, __webpack_require__) { + +var wrappy = __webpack_require__(405) +module.exports = wrappy(once) +module.exports.strict = wrappy(onceStrict) + +once.proto = once(function () { + Object.defineProperty(Function.prototype, 'once', { + value: function () { + return once(this) + }, + configurable: true + }) + + Object.defineProperty(Function.prototype, 'onceStrict', { + value: function () { + return onceStrict(this) + }, + configurable: true + }) +}) + +function once (fn) { + var f = function () { + if (f.called) return f.value + f.called = true + return f.value = fn.apply(this, arguments) + } + f.called = false + return f +} + +function onceStrict (fn) { + var f = function () { + if (f.called) + throw new Error(f.onceError) + f.called = true + return f.value = fn.apply(this, arguments) + } + var name = fn.name || 'Function wrapped with `once`' + f.onceError = name + " shouldn't be called more than once" + f.called = false + return f } -//# sourceMappingURL=generate.js.map /***/ }), -/* 403 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { +/* 405 */ +/***/ (function(module, exports) { -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "iif", function() { return iif; }); -/* harmony import */ var _defer__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(333); -/* harmony import */ var _empty__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(242); -/** PURE_IMPORTS_START _defer,_empty PURE_IMPORTS_END */ +// Returns a wrapper function that returns a wrapped callback +// The wrapper function should do some stuff, and return a +// presumably different callback function. +// This makes sure that own properties are retained, so that +// decorations and such are not lost along the way. +module.exports = wrappy +function wrappy (fn, cb) { + if (fn && cb) return wrappy(fn)(cb) + if (typeof fn !== 'function') + throw new TypeError('need wrapper function') -function iif(condition, trueResult, falseResult) { - if (trueResult === void 0) { - trueResult = _empty__WEBPACK_IMPORTED_MODULE_1__["EMPTY"]; + Object.keys(fn).forEach(function (k) { + wrapper[k] = fn[k] + }) + + return wrapper + + function wrapper() { + var args = new Array(arguments.length) + for (var i = 0; i < args.length; i++) { + args[i] = arguments[i] } - if (falseResult === void 0) { - falseResult = _empty__WEBPACK_IMPORTED_MODULE_1__["EMPTY"]; + var ret = fn.apply(this, args) + var cb = args[args.length-1] + if (typeof ret === 'function' && ret !== cb) { + Object.keys(cb).forEach(function (k) { + ret[k] = cb[k] + }) } - return Object(_defer__WEBPACK_IMPORTED_MODULE_0__["defer"])(function () { return condition() ? trueResult : falseResult; }); + return ret + } } -//# sourceMappingURL=iif.js.map /***/ }), -/* 404 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { +/* 406 */ +/***/ (function(module, exports, __webpack_require__) { -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "interval", function() { return interval; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(193); -/* harmony import */ var _scheduler_async__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(199); -/* harmony import */ var _util_isNumeric__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(205); -/** PURE_IMPORTS_START _Observable,_scheduler_async,_util_isNumeric PURE_IMPORTS_END */ +var once = __webpack_require__(404); +var noop = function() {}; +var isRequest = function(stream) { + return stream.setHeader && typeof stream.abort === 'function'; +}; -function interval(period, scheduler) { - if (period === void 0) { - period = 0; - } - if (scheduler === void 0) { - scheduler = _scheduler_async__WEBPACK_IMPORTED_MODULE_1__["async"]; - } - if (!Object(_util_isNumeric__WEBPACK_IMPORTED_MODULE_2__["isNumeric"])(period) || period < 0) { - period = 0; - } - if (!scheduler || typeof scheduler.schedule !== 'function') { - scheduler = _scheduler_async__WEBPACK_IMPORTED_MODULE_1__["async"]; - } - return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { - subscriber.add(scheduler.schedule(dispatch, period, { subscriber: subscriber, counter: 0, period: period })); - return subscriber; - }); -} -function dispatch(state) { - var subscriber = state.subscriber, counter = state.counter, period = state.period; - subscriber.next(counter); - this.schedule({ subscriber: subscriber, counter: counter + 1, period: period }, period); -} -//# sourceMappingURL=interval.js.map +var isChildProcess = function(stream) { + return stream.stdio && Array.isArray(stream.stdio) && stream.stdio.length === 3 +}; +var eos = function(stream, opts, callback) { + if (typeof opts === 'function') return eos(stream, null, opts); + if (!opts) opts = {}; -/***/ }), -/* 405 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { + callback = once(callback || noop); -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "NEVER", function() { return NEVER; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "never", function() { return never; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(193); -/* harmony import */ var _util_noop__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(197); -/** PURE_IMPORTS_START _Observable,_util_noop PURE_IMPORTS_END */ + var ws = stream._writableState; + var rs = stream._readableState; + var readable = opts.readable || (opts.readable !== false && stream.readable); + var writable = opts.writable || (opts.writable !== false && stream.writable); + var onlegacyfinish = function() { + if (!stream.writable) onfinish(); + }; -var NEVER = /*@__PURE__*/ new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](_util_noop__WEBPACK_IMPORTED_MODULE_1__["noop"]); -function never() { - return NEVER; -} -//# sourceMappingURL=never.js.map + var onfinish = function() { + writable = false; + if (!readable) callback.call(stream); + }; + + var onend = function() { + readable = false; + if (!writable) callback.call(stream); + }; + var onexit = function(exitCode) { + callback.call(stream, exitCode ? new Error('exited with error code: ' + exitCode) : null); + }; -/***/ }), -/* 406 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { + var onerror = function(err) { + callback.call(stream, err); + }; -"use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "onErrorResumeNext", function() { return onErrorResumeNext; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(193); -/* harmony import */ var _from__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(218); -/* harmony import */ var _util_isArray__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(178); -/* harmony import */ var _empty__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(242); -/** PURE_IMPORTS_START _Observable,_from,_util_isArray,_empty PURE_IMPORTS_END */ + var onclose = function() { + if (readable && !(rs && rs.ended)) return callback.call(stream, new Error('premature close')); + if (writable && !(ws && ws.ended)) return callback.call(stream, new Error('premature close')); + }; + + var onrequest = function() { + stream.req.on('finish', onfinish); + }; + if (isRequest(stream)) { + stream.on('complete', onfinish); + stream.on('abort', onclose); + if (stream.req) onrequest(); + else stream.on('request', onrequest); + } else if (writable && !ws) { // legacy streams + stream.on('end', onlegacyfinish); + stream.on('close', onlegacyfinish); + } + if (isChildProcess(stream)) stream.on('exit', onexit); + stream.on('end', onend); + stream.on('finish', onfinish); + if (opts.error !== false) stream.on('error', onerror); + stream.on('close', onclose); -function onErrorResumeNext() { - var sources = []; - for (var _i = 0; _i < arguments.length; _i++) { - sources[_i] = arguments[_i]; - } - if (sources.length === 0) { - return _empty__WEBPACK_IMPORTED_MODULE_3__["EMPTY"]; - } - var first = sources[0], remainder = sources.slice(1); - if (sources.length === 1 && Object(_util_isArray__WEBPACK_IMPORTED_MODULE_2__["isArray"])(first)) { - return onErrorResumeNext.apply(void 0, first); - } - return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { - var subNext = function () { return subscriber.add(onErrorResumeNext.apply(void 0, remainder).subscribe(subscriber)); }; - return Object(_from__WEBPACK_IMPORTED_MODULE_1__["from"])(first).subscribe({ - next: function (value) { subscriber.next(value); }, - error: subNext, - complete: subNext, - }); - }); -} -//# sourceMappingURL=onErrorResumeNext.js.map + return function() { + stream.removeListener('complete', onfinish); + stream.removeListener('abort', onclose); + stream.removeListener('request', onrequest); + if (stream.req) stream.req.removeListener('finish', onfinish); + stream.removeListener('end', onlegacyfinish); + stream.removeListener('close', onlegacyfinish); + stream.removeListener('finish', onfinish); + stream.removeListener('exit', onexit); + stream.removeListener('end', onend); + stream.removeListener('error', onerror); + stream.removeListener('close', onclose); + }; +}; + +module.exports = eos; /***/ }), /* 407 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { +/***/ (function(module, exports, __webpack_require__) { "use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "pairs", function() { return pairs; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "dispatch", function() { return dispatch; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(193); -/* harmony import */ var _Subscription__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(177); -/** PURE_IMPORTS_START _Observable,_Subscription PURE_IMPORTS_END */ +const {PassThrough: PassThroughStream} = __webpack_require__(27); -function pairs(obj, scheduler) { - if (!scheduler) { - return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { - var keys = Object.keys(obj); - for (var i = 0; i < keys.length && !subscriber.closed; i++) { - var key = keys[i]; - if (obj.hasOwnProperty(key)) { - subscriber.next([key, obj[key]]); - } - } - subscriber.complete(); - }); - } - else { - return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { - var keys = Object.keys(obj); - var subscription = new _Subscription__WEBPACK_IMPORTED_MODULE_1__["Subscription"](); - subscription.add(scheduler.schedule(dispatch, 0, { keys: keys, index: 0, subscriber: subscriber, subscription: subscription, obj: obj })); - return subscription; - }); - } -} -function dispatch(state) { - var keys = state.keys, index = state.index, subscriber = state.subscriber, subscription = state.subscription, obj = state.obj; - if (!subscriber.closed) { - if (index < keys.length) { - var key = keys[index]; - subscriber.next([key, obj[key]]); - subscription.add(this.schedule({ keys: keys, index: index + 1, subscriber: subscriber, subscription: subscription, obj: obj })); - } - else { - subscriber.complete(); - } - } -} -//# sourceMappingURL=pairs.js.map +module.exports = options => { + options = {...options}; + + const {array} = options; + let {encoding} = options; + const isBuffer = encoding === 'buffer'; + let objectMode = false; + + if (array) { + objectMode = !(encoding || isBuffer); + } else { + encoding = encoding || 'utf8'; + } + + if (isBuffer) { + encoding = null; + } + + const stream = new PassThroughStream({objectMode}); + + if (encoding) { + stream.setEncoding(encoding); + } + + let length = 0; + const chunks = []; + + stream.on('data', chunk => { + chunks.push(chunk); + + if (objectMode) { + length = chunks.length; + } else { + length += chunk.length; + } + }); + + stream.getBufferedValue = () => { + if (array) { + return chunks; + } + + return isBuffer ? Buffer.concat(chunks, length) : chunks.join(''); + }; + + stream.getBufferedLength = () => length; + + return stream; +}; /***/ }), /* 408 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { +/***/ (function(module, exports, __webpack_require__) { "use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "partition", function() { return partition; }); -/* harmony import */ var _util_not__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(289); -/* harmony import */ var _util_subscribeTo__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(184); -/* harmony import */ var _operators_filter__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(251); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(193); -/** PURE_IMPORTS_START _util_not,_util_subscribeTo,_operators_filter,_Observable PURE_IMPORTS_END */ +const { PassThrough } = __webpack_require__(27); + +module.exports = function (/*streams...*/) { + var sources = [] + var output = new PassThrough({objectMode: true}) + output.setMaxListeners(0) -function partition(source, predicate, thisArg) { - return [ - Object(_operators_filter__WEBPACK_IMPORTED_MODULE_2__["filter"])(predicate, thisArg)(new _Observable__WEBPACK_IMPORTED_MODULE_3__["Observable"](Object(_util_subscribeTo__WEBPACK_IMPORTED_MODULE_1__["subscribeTo"])(source))), - Object(_operators_filter__WEBPACK_IMPORTED_MODULE_2__["filter"])(Object(_util_not__WEBPACK_IMPORTED_MODULE_0__["not"])(predicate, thisArg))(new _Observable__WEBPACK_IMPORTED_MODULE_3__["Observable"](Object(_util_subscribeTo__WEBPACK_IMPORTED_MODULE_1__["subscribeTo"])(source))) - ]; + output.add = add + output.isEmpty = isEmpty + + output.on('unpipe', remove) + + Array.prototype.slice.call(arguments).forEach(add) + + return output + + function add (source) { + if (Array.isArray(source)) { + source.forEach(add) + return this + } + + sources.push(source); + source.once('end', remove.bind(null, source)) + source.once('error', output.emit.bind(output, 'error')) + source.pipe(output, {end: false}) + return this + } + + function isEmpty () { + return sources.length == 0; + } + + function remove (source) { + sources = sources.filter(function (it) { return it !== source }) + if (!sources.length && output.readable) { output.end() } + } } -//# sourceMappingURL=partition.js.map /***/ }), /* 409 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { +/***/ (function(module, exports, __webpack_require__) { "use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "range", function() { return range; }); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "dispatch", function() { return dispatch; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(193); -/** PURE_IMPORTS_START _Observable PURE_IMPORTS_END */ -function range(start, count, scheduler) { - if (start === void 0) { - start = 0; - } - return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { - if (count === undefined) { - count = start; - start = 0; - } - var index = 0; - var current = start; - if (scheduler) { - return scheduler.schedule(dispatch, 0, { - index: index, count: count, start: start, subscriber: subscriber - }); - } - else { - do { - if (index++ >= count) { - subscriber.complete(); - break; - } - subscriber.next(current++); - if (subscriber.closed) { - break; - } - } while (true); - } - return undefined; - }); -} -function dispatch(state) { - var start = state.start, index = state.index, count = state.count, subscriber = state.subscriber; - if (index >= count) { - subscriber.complete(); - return; - } - subscriber.next(start); - if (subscriber.closed) { - return; - } - state.index = index + 1; - state.start = start + 1; - this.schedule(state); -} -//# sourceMappingURL=range.js.map +const mergePromiseProperty = (spawned, promise, property) => { + // Starting the main `promise` is deferred to avoid consuming streams + const value = typeof promise === 'function' ? + (...args) => promise()[property](...args) : + promise[property].bind(promise); + + Object.defineProperty(spawned, property, { + value, + writable: true, + enumerable: false, + configurable: true + }); +}; + +// The return value is a mixin of `childProcess` and `Promise` +const mergePromise = (spawned, promise) => { + mergePromiseProperty(spawned, promise, 'then'); + mergePromiseProperty(spawned, promise, 'catch'); + mergePromiseProperty(spawned, promise, 'finally'); + return spawned; +}; + +// Use promises instead of `child_process` events +const getSpawnedPromise = spawned => { + return new Promise((resolve, reject) => { + spawned.on('exit', (exitCode, signal) => { + resolve({exitCode, signal}); + }); + + spawned.on('error', error => { + reject(error); + }); + + if (spawned.stdin) { + spawned.stdin.on('error', error => { + reject(error); + }); + } + }); +}; + +module.exports = { + mergePromise, + getSpawnedPromise +}; + /***/ }), /* 410 */ -/***/ (function(module, __webpack_exports__, __webpack_require__) { +/***/ (function(module, exports, __webpack_require__) { "use strict"; -__webpack_require__.r(__webpack_exports__); -/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "using", function() { return using; }); -/* harmony import */ var _Observable__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(193); -/* harmony import */ var _from__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(218); -/* harmony import */ var _empty__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(242); -/** PURE_IMPORTS_START _Observable,_from,_empty PURE_IMPORTS_END */ +const SPACES_REGEXP = / +/g; +const joinCommand = (file, args = []) => { + if (!Array.isArray(args)) { + return file; + } -function using(resourceFactory, observableFactory) { - return new _Observable__WEBPACK_IMPORTED_MODULE_0__["Observable"](function (subscriber) { - var resource; - try { - resource = resourceFactory(); - } - catch (err) { - subscriber.error(err); - return undefined; - } - var result; - try { - result = observableFactory(resource); - } - catch (err) { - subscriber.error(err); - return undefined; - } - var source = result ? Object(_from__WEBPACK_IMPORTED_MODULE_1__["from"])(result) : _empty__WEBPACK_IMPORTED_MODULE_2__["EMPTY"]; - var subscription = source.subscribe(subscriber); - return function () { - subscription.unsubscribe(); - if (resource) { - resource.unsubscribe(); - } - }; - }); -} -//# sourceMappingURL=using.js.map + return [file, ...args].join(' '); +}; + +// Allow spaces to be escaped by a backslash if not meant as a delimiter +const handleEscaping = (tokens, token, index) => { + if (index === 0) { + return [token]; + } + + const previousToken = tokens[tokens.length - 1]; + + if (previousToken.endsWith('\\')) { + return [...tokens.slice(0, -1), `${previousToken.slice(0, -1)} ${token}`]; + } + + return [...tokens, token]; +}; + +// Handle `execa.command()` +const parseCommand = command => { + return command + .trim() + .split(SPACES_REGEXP) + .reduce(handleEscaping, []); +}; + +module.exports = { + joinCommand, + parseCommand +}; /***/ }), @@ -36357,7 +36363,7 @@ function using(resourceFactory, observableFactory) { "use strict"; -var childProcess = __webpack_require__(352); +var childProcess = __webpack_require__(372); var spawn = childProcess.spawn; var exec = childProcess.exec; @@ -36501,8 +36507,8 @@ function buildProcessTree (parentPid, tree, pidsToProcess, spawnChildProcessesLi */ Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = __webpack_require__(36); -const Rx = tslib_1.__importStar(__webpack_require__(391)); -const operators_1 = __webpack_require__(169); +const Rx = tslib_1.__importStar(__webpack_require__(169)); +const operators_1 = __webpack_require__(270); const SEP = /\r?\n/; const observe_readable_1 = __webpack_require__(413); /** @@ -36570,8 +36576,8 @@ exports.observeLines = observeLines; */ Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = __webpack_require__(36); -const Rx = tslib_1.__importStar(__webpack_require__(391)); -const operators_1 = __webpack_require__(169); +const Rx = tslib_1.__importStar(__webpack_require__(169)); +const operators_1 = __webpack_require__(270); /** * Produces an Observable from a ReadableSteam that: * - completes on the first "end" event @@ -36645,7 +36651,7 @@ exports.ToolingLogCollectingWriter = tooling_log_collecting_writer_1.ToolingLogC */ Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = __webpack_require__(36); -const Rx = tslib_1.__importStar(__webpack_require__(391)); +const Rx = tslib_1.__importStar(__webpack_require__(169)); const tooling_log_text_writer_1 = __webpack_require__(416); class ToolingLog { constructor(writerConfig) { @@ -39453,7 +39459,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = __webpack_require__(36); const util_1 = __webpack_require__(29); // @ts-ignore @types are outdated and module is super simple -const exit_hook_1 = tslib_1.__importDefault(__webpack_require__(348)); +const exit_hook_1 = tslib_1.__importDefault(__webpack_require__(368)); const tooling_log_1 = __webpack_require__(414); const fail_1 = __webpack_require__(446); const flags_1 = __webpack_require__(447); @@ -44319,7 +44325,7 @@ var rp = __webpack_require__(504) var minimatch = __webpack_require__(506) var Minimatch = minimatch.Minimatch var inherits = __webpack_require__(510) -var EE = __webpack_require__(379).EventEmitter +var EE = __webpack_require__(399).EventEmitter var path = __webpack_require__(16) var assert = __webpack_require__(30) var isAbsolute = __webpack_require__(512) @@ -44334,7 +44340,7 @@ var util = __webpack_require__(29) var childrenIgnored = common.childrenIgnored var isIgnored = common.isIgnored -var once = __webpack_require__(384) +var once = __webpack_require__(404) function glob (pattern, options, cb) { if (typeof options === 'function') cb = options, options = {} @@ -47483,9 +47489,9 @@ function childrenIgnored (self, path) { /* 515 */ /***/ (function(module, exports, __webpack_require__) { -var wrappy = __webpack_require__(385) +var wrappy = __webpack_require__(405) var reqs = Object.create(null) -var once = __webpack_require__(384) +var once = __webpack_require__(404) module.exports = wrappy(inflight) @@ -53616,7 +53622,7 @@ module.exports._cleanupOnExit = cleanupOnExit var fs = __webpack_require__(552) var MurmurHash3 = __webpack_require__(556) -var onExit = __webpack_require__(377) +var onExit = __webpack_require__(397) var path = __webpack_require__(16) var activeFiles = {} @@ -55267,7 +55273,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "spawnStreaming", function() { return spawnStreaming; }); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2); /* harmony import */ var chalk__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(chalk__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(351); +/* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(371); /* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(execa__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var log_symbols__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(565); /* harmony import */ var log_symbols__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(log_symbols__WEBPACK_IMPORTED_MODULE_2__); @@ -56988,7 +56994,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(crypto__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(29); /* harmony import */ var util__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(util__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(351); +/* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(371); /* harmony import */ var execa__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(execa__WEBPACK_IMPORTED_MODULE_3__); /* harmony import */ var _yarn_lock__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(582); /* @@ -59784,7 +59790,7 @@ exports.f = __webpack_require__(33) ? Object.defineProperty : function definePro /* 54 */ /***/ (function(module, exports) { -module.exports = __webpack_require__(379); +module.exports = __webpack_require__(399); /***/ }), /* 55 */ @@ -68254,7 +68260,7 @@ var rp = __webpack_require__(504) var minimatch = __webpack_require__(506) var Minimatch = minimatch.Minimatch var inherits = __webpack_require__(592) -var EE = __webpack_require__(379).EventEmitter +var EE = __webpack_require__(399).EventEmitter var path = __webpack_require__(16) var assert = __webpack_require__(30) var isAbsolute = __webpack_require__(512) @@ -68269,7 +68275,7 @@ var util = __webpack_require__(29) var childrenIgnored = common.childrenIgnored var isIgnored = common.isIgnored -var once = __webpack_require__(384) +var once = __webpack_require__(404) function glob (pattern, options, cb) { if (typeof options === 'function') cb = options, options = {} @@ -74366,7 +74372,7 @@ function callSuccessCallback(callback, entries) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const events_1 = __webpack_require__(379); +const events_1 = __webpack_require__(399); const fsScandir = __webpack_require__(635); const fastq = __webpack_require__(644); const common = __webpack_require__(646); @@ -78154,7 +78160,7 @@ exports.toggle = (force, stream) => { "use strict"; const onetime = __webpack_require__(682); -const signalExit = __webpack_require__(377); +const signalExit = __webpack_require__(397); module.exports = onetime(() => { signalExit(() => { @@ -78402,8 +78408,8 @@ const WatchCommand = { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "waitUntilWatchIsReady", function() { return waitUntilWatchIsReady; }); -/* harmony import */ var rxjs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(391); -/* harmony import */ var rxjs_operators__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(169); +/* harmony import */ var rxjs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(169); +/* harmony import */ var rxjs_operators__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(270); /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with @@ -79570,7 +79576,7 @@ async function copyToBuild(project, kibanaRoot, buildRoot) { "use strict"; -const EventEmitter = __webpack_require__(379); +const EventEmitter = __webpack_require__(399); const path = __webpack_require__(16); const os = __webpack_require__(11); const pAll = __webpack_require__(708); @@ -80107,7 +80113,7 @@ var rp = __webpack_require__(504) var minimatch = __webpack_require__(506) var Minimatch = minimatch.Minimatch var inherits = __webpack_require__(715) -var EE = __webpack_require__(379).EventEmitter +var EE = __webpack_require__(399).EventEmitter var path = __webpack_require__(16) var assert = __webpack_require__(30) var isAbsolute = __webpack_require__(512) @@ -80122,7 +80128,7 @@ var util = __webpack_require__(29) var childrenIgnored = common.childrenIgnored var isIgnored = common.isIgnored -var once = __webpack_require__(384) +var once = __webpack_require__(404) function glob (pattern, options, cb) { if (typeof options === 'function') cb = options, options = {} @@ -104781,7 +104787,7 @@ function readdirSync (dir, options, internalOptions) { const Readable = __webpack_require__(27).Readable; -const EventEmitter = __webpack_require__(379).EventEmitter; +const EventEmitter = __webpack_require__(399).EventEmitter; const path = __webpack_require__(16); const normalizeOptions = __webpack_require__(894); const stat = __webpack_require__(896); @@ -110030,7 +110036,7 @@ function coerce (version, options) { "use strict"; -const EventEmitter = __webpack_require__(379); +const EventEmitter = __webpack_require__(399); const written = new WeakMap(); diff --git a/src/core/server/saved_objects/mappings/types.ts b/src/core/server/saved_objects/mappings/types.ts index c1b65763949bbe..8362d1f16bd2a0 100644 --- a/src/core/server/saved_objects/mappings/types.ts +++ b/src/core/server/saved_objects/mappings/types.ts @@ -137,6 +137,7 @@ export interface SavedObjectsCoreFieldMapping { fields?: { [subfield: string]: { type: string; + ignore_above?: number; }; }; } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index e4234689c25e81..bb8ee1d8e7a318 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1884,6 +1884,7 @@ export interface SavedObjectsCoreFieldMapping { fields?: { [subfield: string]: { type: string; + ignore_above?: number; }; }; // (undocumented) diff --git a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/tabs/tabs.tsx b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/tabs/tabs.tsx index bfd507876a9d6b..247af7e20d5815 100644 --- a/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/tabs/tabs.tsx +++ b/src/legacy/core_plugins/kibana/public/management/sections/index_patterns/edit_index_pattern/tabs/tabs.tsx @@ -49,14 +49,18 @@ interface TabsProps extends Pick { }; } +const searchAriaLabel = i18n.translate('kbn.management.editIndexPattern.fields.searchAria', { + defaultMessage: 'Search fields', +}); + const filterAriaLabel = i18n.translate('kbn.management.editIndexPattern.fields.filterAria', { - defaultMessage: 'Filter', + defaultMessage: 'Filter field types', }); const filterPlaceholder = i18n.translate( 'kbn.management.editIndexPattern.fields.filterPlaceholder', { - defaultMessage: 'Filter', + defaultMessage: 'Search', } ); @@ -108,7 +112,7 @@ export function Tabs({ config, indexPattern, fields, services, history, location value={fieldFilter} onChange={e => setFieldFilter(e.target.value)} data-test-subj="indexPatternFieldFilter" - aria-label={filterAriaLabel} + aria-label={searchAriaLabel} /> {type === TAB_INDEXED_FIELDS && indexedFieldTypes.length > 0 && ( @@ -118,6 +122,7 @@ export function Tabs({ config, indexPattern, fields, services, history, location value={indexedFieldTypeFilter} onChange={e => setIndexedFieldTypeFilter(e.target.value)} data-test-subj="indexedFieldTypeFilterDropdown" + aria-label={filterAriaLabel} /> )} diff --git a/src/legacy/core_plugins/ui_metric/index.ts b/src/legacy/core_plugins/ui_metric/index.ts deleted file mode 100644 index 2e5be3d7b0a398..00000000000000 --- a/src/legacy/core_plugins/ui_metric/index.ts +++ /dev/null @@ -1,30 +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 { resolve } from 'path'; - -// eslint-disable-next-line import/no-default-export -export default function(kibana: any) { - return new kibana.Plugin({ - id: 'ui_metric', - require: ['kibana', 'elasticsearch'], - publicDir: resolve(__dirname, 'public'), - init() {}, - }); -} diff --git a/src/legacy/core_plugins/ui_metric/package.json b/src/legacy/core_plugins/ui_metric/package.json deleted file mode 100644 index 3ddcac4eec47f6..00000000000000 --- a/src/legacy/core_plugins/ui_metric/package.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "name": "ui_metric", - "version": "kibana" -} diff --git a/src/legacy/core_plugins/ui_metric/public/index.ts b/src/legacy/core_plugins/ui_metric/public/index.ts deleted file mode 100644 index 19246b571cb842..00000000000000 --- a/src/legacy/core_plugins/ui_metric/public/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -export { createUiStatsReporter } from './services/telemetry_analytics'; -export { METRIC_TYPE, UiStatsMetricType } from '@kbn/analytics'; diff --git a/src/legacy/core_plugins/ui_metric/public/services/telemetry_analytics.ts b/src/legacy/core_plugins/ui_metric/public/services/telemetry_analytics.ts deleted file mode 100644 index 0e517e6ff22449..00000000000000 --- a/src/legacy/core_plugins/ui_metric/public/services/telemetry_analytics.ts +++ /dev/null @@ -1,24 +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 { npSetup } from 'ui/new_platform'; - -export const createUiStatsReporter = (appName: string) => { - const { usageCollection } = npSetup.plugins; - return usageCollection.reportUiStats.bind(usageCollection, appName); -}; diff --git a/src/plugins/console/public/styles/_app.scss b/src/plugins/console/public/styles/_app.scss index 047db6e3ad877c..c41df24912c2a7 100644 --- a/src/plugins/console/public/styles/_app.scss +++ b/src/plugins/console/public/styles/_app.scss @@ -1,8 +1,11 @@ // TODO: Move all of the styles here (should be modularised by, e.g., CSS-in-JS or CSS modules). @import '@elastic/eui/src/components/header/variables'; +// This value is calculated to static value using SCSS because calc in calc has issues in IE11 +$headerHeightOffset: $euiHeaderHeightCompensation * 2; + #consoleRoot { - height: calc(100vh - calc(#{$euiHeaderChildSize} * 2)); + height: calc(100vh - #{$headerHeightOffset}); display: flex; flex: 1 1 auto; // Make sure the editor actions don't create scrollbars on this container diff --git a/src/plugins/data/common/es_query/es_query/migrate_filter.ts b/src/plugins/data/common/es_query/es_query/migrate_filter.ts index 22fbfe0e1ab083..a0529d585879eb 100644 --- a/src/plugins/data/common/es_query/es_query/migrate_filter.ts +++ b/src/plugins/data/common/es_query/es_query/migrate_filter.ts @@ -43,18 +43,25 @@ export function migrateFilter(filter: Filter, indexPattern?: IIndexPattern) { if (isDeprecatedMatchPhraseFilter(filter)) { const fieldName = Object.keys(filter.query.match)[0]; const params: Record = get(filter, ['query', 'match', fieldName]); + let query = params.query; if (indexPattern) { const field = indexPattern.fields.find(f => f.name === fieldName); if (field) { - params.query = getConvertedValueForField(field, params.query); + query = getConvertedValueForField(field, params.query); } } return { ...filter, query: { match_phrase: { - [fieldName]: omit(params, 'type'), + [fieldName]: omit( + { + ...params, + query, + }, + 'type' + ), }, }, }; diff --git a/src/plugins/usage_collection/README.md b/src/plugins/usage_collection/README.md index e32dfae35832bb..99075d5d48f596 100644 --- a/src/plugins/usage_collection/README.md +++ b/src/plugins/usage_collection/README.md @@ -126,41 +126,6 @@ export function registerMyPluginUsageCollector( } ``` -### Migrating to NP from Legacy Plugins - -Pass `usageCollection` to the setup NP plugin setup function under plugins. Inside the `setup` function call the `registerCollector` like what you'd do in the NP example above. - -```js -// index.js -export const myPlugin = (kibana: any) => { - return new kibana.Plugin({ - init: async function (server) { - const { usageCollection } = server.newPlatform.setup.plugins; - const plugins = { - usageCollection, - }; - plugin(initializerContext).setup(core, plugins); - } - }); -} -``` - -### Legacy Plugins - -Typically, a plugin will create the collector object and register it with the Telemetry service from the `init` method of the plugin definition, or a helper module called from `init`. - -```js -// index.js -export const myPlugin = (kibana: any) => { - return new kibana.Plugin({ - init: async function (server) { - const { usageCollection } = server.newPlatform.setup.plugins; - registerMyPluginUsageCollector(usageCollection); - } - }); -} -``` - ## Update the telemetry payload and telemetry cluster field mappings There is a module in the telemetry service that creates the payload of data that gets sent up to the telemetry cluster. @@ -239,15 +204,6 @@ To track a user interaction, use the `reportUiStats` method exposed by the plugi } ``` -Alternatively, in the Legacy world you can still import the `createUiStatsReporter` helper function from UI Metric app: - -```js -import { createUiStatsReporter, METRIC_TYPE } from 'relative/path/to/src/legacy/core_plugins/ui_metric/public'; -const trackMetric = createUiStatsReporter(``); -trackMetric(METRIC_TYPE.CLICK, ``); -trackMetric('click', ``); -``` - Metric Types: - `METRIC_TYPE.CLICK` for tracking clicks `trackMetric(METRIC_TYPE.CLICK, 'my_button_clicked');` diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index 10704514ab8d5d..f5a840c480aa1e 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -71,6 +71,8 @@ export interface VisualizeOutput extends EmbeddableOutput { type ExpressionLoader = InstanceType; +const visTypesWithoutInspector = ['markdown', 'input_control_vis', 'metrics', 'vega', 'timelion']; + export class VisualizeEmbeddable extends Embeddable { private handler?: ExpressionLoader; private timefilter: TimefilterContract; @@ -126,7 +128,7 @@ export class VisualizeEmbeddable extends Embeddable { - if (!this.handler) { + if (!this.handler || visTypesWithoutInspector.includes(this.vis.type.name)) { return undefined; } return this.handler.inspect(); @@ -215,19 +217,7 @@ export class VisualizeEmbeddable extends Embeddable { - const visTypesWithoutInspector = [ - 'markdown', - 'input_control_vis', - 'metrics', - 'vega', - 'timelion', - ]; - if (visTypesWithoutInspector.includes(this.vis.type.name)) { - return false; - } - return this.getInspectorAdapters(); - }; + hasInspector = () => Boolean(this.getInspectorAdapters()); /** * diff --git a/test/accessibility/apps/management.ts b/test/accessibility/apps/management.ts index 9e75250403d6b1..cc60e672fe880c 100644 --- a/test/accessibility/apps/management.ts +++ b/test/accessibility/apps/management.ts @@ -20,23 +20,12 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function({ getService, getPageObjects }: FtrProviderContext) { - const PageObjects = getPageObjects(['common', 'settings']); + const PageObjects = getPageObjects(['common', 'settings', 'header']); const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); - const testSubjects = getService('testSubjects'); const a11y = getService('a11y'); - // describe('Management', () => { - // before(async () => { - // await esArchiver.loadIfNeeded('logstash_functional'); - // await kibanaServer.uiSettings.update({ - // defaultIndex: 'logstash-*', - // }); - // await PageObjects.common.navigateToApp('settings'); - // }); - - // FLAKY: https://github.com/elastic/kibana/issues/60470 - describe.skip('Management', () => { + describe('Management', () => { before(async () => { await esArchiver.load('discover'); await esArchiver.loadIfNeeded('logstash_functional'); @@ -57,23 +46,25 @@ export default function({ getService, getPageObjects }: FtrProviderContext) { it('Single indexpattern view', async () => { await PageObjects.settings.clickIndexPatternLogstash(); + await PageObjects.header.waitUntilLoadingHasFinished(); await a11y.testAppSnapshot(); }); - it('Create Index pattern wizard', async () => { + it('Open create index pattern wizard', async () => { await PageObjects.settings.clickKibanaIndexPatterns(); - await (await testSubjects.find('createIndexPatternButton')).click(); + await PageObjects.settings.clickAddNewIndexPatternButton(); + await PageObjects.header.waitUntilLoadingHasFinished(); await a11y.testAppSnapshot(); }); - // index patterns page + // We are navigating back to index pattern page to test field formatters it('Navigate back to logstash index page', async () => { await PageObjects.settings.clickKibanaIndexPatterns(); await PageObjects.settings.clickIndexPatternLogstash(); await a11y.testAppSnapshot(); }); - // Issue: https://github.com/elastic/kibana/issues/60030 + // Will be enabling this and field formatters after this issue is addressed: https://github.com/elastic/kibana/issues/60030 it.skip('Edit field type', async () => { await PageObjects.settings.clickEditFieldFormat(); await a11y.testAppSnapshot(); diff --git a/test/functional/services/find.ts b/test/functional/services/find.ts index bdcc5ba95e9fbd..3697e944610743 100644 --- a/test/functional/services/find.ts +++ b/test/functional/services/find.ts @@ -17,20 +17,16 @@ * under the License. */ -import { WebDriver, WebElement, By } from 'selenium-webdriver'; +import { WebDriver, WebElement, By, until } from 'selenium-webdriver'; import { FtrProviderContext } from '../ftr_provider_context'; import { WebElementWrapper } from './lib/web_element_wrapper'; export async function FindProvider({ getService }: FtrProviderContext) { const log = getService('log'); const config = getService('config'); - const webdriver = await getService('__webdriver__').init(); + const { driver, browserType } = await getService('__webdriver__').init(); const retry = getService('retry'); - const driver = webdriver.driver; - const until = webdriver.until; - const browserType = webdriver.browserType; - const WAIT_FOR_EXISTS_TIME = config.get('timeouts.waitForExists'); const POLLING_TIME = 500; const defaultFindTimeout = config.get('timeouts.find'); @@ -40,7 +36,7 @@ export async function FindProvider({ getService }: FtrProviderContext) { WebElementWrapper.create( webElement, locator, - webdriver, + driver, defaultFindTimeout, fixedHeaderHeight, log, @@ -198,7 +194,7 @@ export async function FindProvider({ getService }: FtrProviderContext) { if (isDisplayed) { return descendant; } else { - throw new Error('Element is not displayed'); + throw new Error(`Element "${selector}" is not displayed`); } } diff --git a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts index 8b57ecd3c82350..615dc783601bc9 100644 --- a/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts +++ b/test/functional/services/lib/web_element_wrapper/web_element_wrapper.ts @@ -18,7 +18,7 @@ */ import { delay } from 'bluebird'; -import { WebElement, WebDriver, By, Key, until } from 'selenium-webdriver'; +import { WebElement, WebDriver, By, Key } from 'selenium-webdriver'; import { PNG } from 'pngjs'; // @ts-ignore not supported yet import cheerio from 'cheerio'; @@ -29,12 +29,6 @@ import { CustomCheerio, CustomCheerioStatic } from './custom_cheerio_api'; import { scrollIntoViewIfNecessary } from './scroll_into_view_if_necessary'; import { Browsers } from '../../remote/browsers'; -interface Driver { - driver: WebDriver; - By: typeof By; - until: typeof until; -} - interface TypeOptions { charByChar: boolean; } @@ -51,16 +45,15 @@ const RETRY_CLICK_RETRY_ON_ERRORS = [ ]; export class WebElementWrapper { - private By = this.webDriver.By; - private driver: WebDriver = this.webDriver.driver; + private By = By; private Keys = Key; - public isW3CEnabled: boolean = (this.webDriver.driver as any).executor_.w3c === true; + public isW3CEnabled: boolean = (this.driver as any).executor_.w3c === true; public isChromium: boolean = [Browsers.Chrome, Browsers.ChromiumEdge].includes(this.browserType); public static create( webElement: WebElement | WebElementWrapper, locator: By | null, - webDriver: Driver, + driver: WebDriver, timeout: number, fixedHeaderHeight: number, logger: ToolingLog, @@ -73,7 +66,7 @@ export class WebElementWrapper { return new WebElementWrapper( webElement, locator, - webDriver, + driver, timeout, fixedHeaderHeight, logger, @@ -84,7 +77,7 @@ export class WebElementWrapper { constructor( public _webElement: WebElement, private locator: By | null, - private webDriver: Driver, + private driver: WebDriver, private timeout: number, private fixedHeaderHeight: number, private logger: ToolingLog, @@ -109,7 +102,7 @@ export class WebElementWrapper { return WebElementWrapper.create( otherWebElement, locator, - this.webDriver, + this.driver, this.timeout, this.fixedHeaderHeight, this.logger, diff --git a/test/functional/services/remote/remote.ts b/test/functional/services/remote/remote.ts index 933b08f7681e8b..770e82ad461d23 100644 --- a/test/functional/services/remote/remote.ts +++ b/test/functional/services/remote/remote.ts @@ -58,7 +58,7 @@ export async function RemoteProvider({ getService }: FtrProviderContext) { Fs.writeFileSync(path, JSON.stringify(JSON.parse(coverageJson), null, 2)); }; - const { driver, By, until, consoleLog$ } = await initWebDriver( + const { driver, consoleLog$ } = await initWebDriver( log, browserType, lifecycle, @@ -153,5 +153,5 @@ export async function RemoteProvider({ getService }: FtrProviderContext) { await driver.quit(); }); - return { driver, By, until, browserType, consoleLog$ }; + return { driver, browserType, consoleLog$ }; } diff --git a/test/functional/services/remote/webdriver.ts b/test/functional/services/remote/webdriver.ts index df79db50b8683e..27d17bf754659c 100644 --- a/test/functional/services/remote/webdriver.ts +++ b/test/functional/services/remote/webdriver.ts @@ -28,7 +28,7 @@ import { delay } from 'bluebird'; import chromeDriver from 'chromedriver'; // @ts-ignore types not available import geckoDriver from 'geckodriver'; -import { Builder, Capabilities, By, logging, until } from 'selenium-webdriver'; +import { Builder, Capabilities, logging } from 'selenium-webdriver'; import chrome from 'selenium-webdriver/chrome'; import firefox from 'selenium-webdriver/firefox'; import edge from 'selenium-webdriver/edge'; @@ -310,7 +310,7 @@ async function attemptToCreateCommand( return; } // abort - return { driver: session, By, until, consoleLog$ }; + return { driver: session, consoleLog$ }; } export async function initWebDriver( diff --git a/x-pack/legacy/plugins/maps/public/index.ts b/x-pack/legacy/plugins/maps/public/index.ts index 98db26859297b9..e1532cac858ac0 100644 --- a/x-pack/legacy/plugins/maps/public/index.ts +++ b/x-pack/legacy/plugins/maps/public/index.ts @@ -30,5 +30,5 @@ export const plugin = () => { export { RenderTooltipContentParams, ITooltipProperty, -} from '../../../../plugins/maps/public/layers/tooltips/tooltip_property'; +} from '../../../../plugins/maps/public/classes/tooltips/tooltip_property'; export { MapEmbeddable, MapEmbeddableInput } from '../../../../plugins/maps/public/embeddable'; diff --git a/x-pack/legacy/plugins/maps/server/plugin.js b/x-pack/legacy/plugins/maps/server/plugin.js index d2d5309606cde1..6a542dff2eb1bb 100644 --- a/x-pack/legacy/plugins/maps/server/plugin.js +++ b/x-pack/legacy/plugins/maps/server/plugin.js @@ -19,42 +19,10 @@ import { emsBoundariesSpecProvider } from './tutorials/ems'; export class MapPlugin { setup(core, plugins, __LEGACY) { - const { featuresPlugin, home, licensing, usageCollection, mapsLegacy } = plugins; + const { home, licensing, usageCollection, mapsLegacy } = plugins; let routesInitialized = false; const mapConfig = mapsLegacy.config; - featuresPlugin.registerFeature({ - id: APP_ID, - name: i18n.translate('xpack.maps.featureRegistry.mapsFeatureName', { - defaultMessage: 'Maps', - }), - order: 600, - icon: APP_ICON, - navLinkId: APP_ID, - app: [APP_ID, 'kibana'], - catalogue: [APP_ID], - privileges: { - all: { - app: [APP_ID, 'kibana'], - catalogue: [APP_ID], - savedObject: { - all: [MAP_SAVED_OBJECT_TYPE, 'query'], - read: ['index-pattern'], - }, - ui: ['save', 'show', 'saveQuery'], - }, - read: { - app: [APP_ID, 'kibana'], - catalogue: [APP_ID], - savedObject: { - all: [], - read: [MAP_SAVED_OBJECT_TYPE, 'index-pattern', 'query'], - }, - ui: ['show'], - }, - }, - }); - licensing.license$.subscribe(license => { const { state } = license.check('maps', 'basic'); if (state === 'valid' && !routesInitialized) { diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/setup_xpack_main.js b/x-pack/legacy/plugins/xpack_main/server/lib/setup_xpack_main.js index 2707858a5fec8e..9196b210bba20a 100644 --- a/x-pack/legacy/plugins/xpack_main/server/lib/setup_xpack_main.js +++ b/x-pack/legacy/plugins/xpack_main/server/lib/setup_xpack_main.js @@ -22,8 +22,7 @@ export function setupXPackMain(server) { server.ext('onPreResponse', (request, h) => injectXPackInfoSignature(info, request, h)); - const { registerFeature, getFeatures } = server.newPlatform.setup.plugins.features; - server.expose('registerFeature', registerFeature); + const { getFeatures } = server.newPlatform.setup.plugins.features; server.expose('getFeatures', getFeatures); const setPluginStatus = () => { diff --git a/x-pack/legacy/plugins/xpack_main/server/xpack_main.d.ts b/x-pack/legacy/plugins/xpack_main/server/xpack_main.d.ts index 7b5dc19760627d..2a59c2f1366d4a 100644 --- a/x-pack/legacy/plugins/xpack_main/server/xpack_main.d.ts +++ b/x-pack/legacy/plugins/xpack_main/server/xpack_main.d.ts @@ -12,5 +12,4 @@ export { XPackFeature } from './lib/xpack_info'; export interface XPackMainPlugin { info: XPackInfo; getFeatures(): Feature[]; - registerFeature(feature: FeatureConfig): void; } diff --git a/x-pack/plugins/advanced_ui_actions/public/drilldowns/drilldown_definition.ts b/x-pack/plugins/advanced_ui_actions/public/drilldowns/drilldown_definition.ts index 16c8077d727cbd..f01dd22c06bc54 100644 --- a/x-pack/plugins/advanced_ui_actions/public/drilldowns/drilldown_definition.ts +++ b/x-pack/plugins/advanced_ui_actions/public/drilldowns/drilldown_definition.ts @@ -15,11 +15,6 @@ import { ActionFactoryDefinition } from '../dynamic_actions'; * `Config` is a serializable object containing the configuration that the * drilldown is able to collect using UI. * - * `PlaceContext` is an object that the app that opens drilldown management - * flyout provides to the React component, specifying the contextual information - * about that app. For example, on Dashboard app this context contains - * information about the current embeddable and dashboard. - * * `ExecutionContext` is an object created in response to user's interaction * and provided to the `execute` function of the drilldown. This object contains * information about the action user performed. diff --git a/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/types.ts b/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/types.ts index 9148d1ec7055aa..fb513e892d413c 100644 --- a/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/types.ts +++ b/x-pack/plugins/advanced_ui_actions/public/dynamic_actions/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export interface SerializedAction { +export interface SerializedAction { readonly factoryId: string; readonly name: string; readonly config: Config; @@ -16,5 +16,5 @@ export interface SerializedAction { export interface SerializedEvent { eventId: string; triggers: string[]; - action: SerializedAction; + action: SerializedAction; } diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts index 0cdc7c4eb124d5..d3c4654de81649 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts @@ -17,7 +17,8 @@ export const getSeverityColor = (nodeSeverity: string) => { switch (nodeSeverity) { case severity.warning: return theme.euiColorVis0; - case severity.minor || severity.major: + case severity.minor: + case severity.major: return theme.euiColorVis5; case severity.critical: return theme.euiColorVis9; diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts index 8ddce66f0b853d..e15cf2df38c5f5 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/waterfall_helpers/waterfall_helpers.ts @@ -236,28 +236,33 @@ const getWaterfallItems = (items: TraceAPIResponse['trace']['items']) => } }); -/** - * Changes the parent_id of items based on the child.id property. - * Solves the problem of Inferred spans that are created as child of trace spans - * when it actually should be its parent. - * @param waterfallItems - */ -const reparentSpans = (waterfallItems: IWaterfallItem[]) => { +function reparentSpans(waterfallItems: IWaterfallItem[]) { + // find children that needs to be re-parented and map them to their correct parent id + const childIdToParentIdMapping = Object.fromEntries( + flatten( + waterfallItems.map(waterfallItem => { + if (waterfallItem.docType === 'span') { + const childIds = waterfallItem.doc.child?.id ?? []; + return childIds.map(id => [id, waterfallItem.id]); + } + return []; + }) + ) + ); + + // update parent id for children that needs it or return unchanged return waterfallItems.map(waterfallItem => { - if (waterfallItem.docType === 'span') { - const childId = waterfallItem.doc.child?.id; - if (childId) { - childId.forEach(id => { - const item = waterfallItems.find(_item => _item.id === id); - if (item) { - item.parentId = waterfallItem.id; - } - }); - } + const newParentId = childIdToParentIdMapping[waterfallItem.id]; + if (newParentId) { + return { + ...waterfallItem, + parentId: newParentId + }; } + return waterfallItem; }); -}; +} const getChildrenGroupedByParentId = (waterfallItems: IWaterfallItem[]) => groupBy(waterfallItems, item => (item.parentId ? item.parentId : ROOT_ID)); diff --git a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts index 2f28e37f73f624..6f4a0629c7bc51 100644 --- a/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts +++ b/x-pack/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/waterfallContainer.stories.data.ts @@ -2027,7 +2027,7 @@ export const inferredSpans = { id: '41226ae63af4f235', type: 'unknown' }, - child: { ids: ['8d80de06aa11a6fc'] } + child: { id: ['8d80de06aa11a6fc'] } }, { container: { diff --git a/x-pack/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts b/x-pack/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts index 85aa43f78f7dd6..19e22b3718a787 100644 --- a/x-pack/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts +++ b/x-pack/plugins/apm/scripts/kibana-security/setup-custom-kibana-user-role.ts @@ -15,7 +15,7 @@ import { argv } from 'yargs'; const config = yaml.safeLoad( fs.readFileSync( - path.join(__filename, '../../../../../../../config/kibana.dev.yml'), + path.join(__filename, '../../../../../../config/kibana.dev.yml'), 'utf8' ) ); diff --git a/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts b/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts index 4f69a3a3bd2138..f80f9da7de3874 100644 --- a/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts +++ b/x-pack/plugins/apm/scripts/upload-telemetry-data/index.ts @@ -38,7 +38,7 @@ if (!githubToken) { throw new Error('GITHUB_TOKEN was not provided.'); } -const kibanaConfigDir = path.join(__filename, '../../../../../../../config'); +const kibanaConfigDir = path.join(__filename, '../../../../../../config'); const kibanaDevConfig = path.join(kibanaConfigDir, 'kibana.dev.yml'); const kibanaConfig = path.join(kibanaConfigDir, 'kibana.yml'); diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx index 81f88e563a2586..381cad443e3ad4 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_create_drilldown/flyout_create_drilldown.tsx @@ -72,7 +72,6 @@ export class FlyoutCreateDrilldownAction implements ActionByType handle.close()} - placeContext={context} viewMode={'create'} dynamicActionManager={embeddable.enhancements.dynamicActions} /> diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx index a4499ba4d757d6..5d2a90fdaff085 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/actions/flyout_edit_drilldown/flyout_edit_drilldown.tsx @@ -60,7 +60,6 @@ export class FlyoutEditDrilldownAction implements ActionByType handle.close()} - placeContext={context} viewMode={'manage'} dynamicActionManager={embeddable.enhancements.dynamicActions} /> diff --git a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.story.tsx b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.story.tsx index 16b4d3a25d9e5d..a186feec339247 100644 --- a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.story.tsx +++ b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.story.tsx @@ -38,6 +38,6 @@ const FlyoutManageDrilldowns = createFlyoutManageDrilldowns({ storiesOf('components/FlyoutManageDrilldowns', module).add('default', () => ( {}}> - + )); diff --git a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx index 52c53f32ff09b4..152eaf18f16c10 100644 --- a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx +++ b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.test.tsx @@ -43,9 +43,7 @@ beforeEach(() => { }); test('Allows to manage drilldowns', async () => { - const screen = render( - - ); + const screen = render(); // wait for initial render. It is async because resolving compatible action factories is async await wait(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible()); @@ -112,9 +110,7 @@ test('Allows to manage drilldowns', async () => { }); test('Can delete multiple drilldowns', async () => { - const screen = render( - - ); + const screen = render(); // wait for initial render. It is async because resolving compatible action factories is async await wait(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible()); @@ -151,7 +147,6 @@ test('Create only mode', async () => { const onClose = jest.fn(); const screen = render( { test('After switching between action factories state is restored', async () => { const screen = render( - + ); // wait for initial render. It is async because resolving compatible action factories is async await wait(() => expect(screen.getAllByText(/Create/i).length).toBeGreaterThan(0)); @@ -216,9 +207,7 @@ test("Error when can't save drilldown changes", async () => { jest.spyOn(mockDynamicActionManager, 'createEvent').mockImplementationOnce(async () => { throw error; }); - const screen = render( - - ); + const screen = render(); // wait for initial render. It is async because resolving compatible action factories is async await wait(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible()); fireEvent.click(screen.getByText(/Create new/i)); @@ -236,9 +225,7 @@ test("Error when can't save drilldown changes", async () => { }); test('Should show drilldown welcome message. Should be able to dismiss it', async () => { - let screen = render( - - ); + let screen = render(); // wait for initial render. It is async because resolving compatible action factories is async await wait(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible()); @@ -248,9 +235,7 @@ test('Should show drilldown welcome message. Should be able to dismiss it', asyn expect(screen.queryByTestId(WELCOME_MESSAGE_TEST_SUBJ)).toBeNull(); cleanup(); - screen = render( - - ); + screen = render(); // wait for initial render. It is async because resolving compatible action factories is async await wait(() => expect(screen.getByText(/Manage Drilldowns/i)).toBeVisible()); expect(screen.queryByTestId(WELCOME_MESSAGE_TEST_SUBJ)).toBeNull(); diff --git a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx index 5ebda079a15bf1..ba273e7d578ff8 100644 --- a/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx +++ b/x-pack/plugins/drilldowns/public/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx @@ -32,8 +32,7 @@ import { toastDrilldownsDeleted, } from './i18n'; -interface ConnectedFlyoutManageDrilldownsProps { - placeContext: Context; +interface ConnectedFlyoutManageDrilldownsProps { dynamicActionManager: DynamicActionManager; viewMode?: 'create' | 'manage'; onClose?: () => void; @@ -75,10 +74,9 @@ export function createFlyoutManageDrilldowns({ const factoryContext: object = React.useMemo( () => ({ - placeContext: props.placeContext, triggers: selectedTriggers, }), - [props.placeContext, selectedTriggers] + [selectedTriggers] ); const actionFactories = useCompatibleActionFactoriesForCurrentContext( @@ -222,12 +220,10 @@ export function createFlyoutManageDrilldowns({ } function useCompatibleActionFactoriesForCurrentContext( - actionFactories: Array>, + actionFactories: ActionFactory[], context: Context ) { - const [compatibleActionFactories, setCompatibleActionFactories] = useState< - Array> - >(); + const [compatibleActionFactories, setCompatibleActionFactories] = useState(); useEffect(() => { let canceled = false; async function updateCompatibleFactoriesForContext() { @@ -283,7 +279,7 @@ function useDrilldownsStateManager( } async function createDrilldown( - action: UiActionsEnhancedSerializedAction, + action: UiActionsEnhancedSerializedAction, selectedTriggers: Array ) { await run(async () => { @@ -297,7 +293,7 @@ function useDrilldownsStateManager( async function editDrilldown( drilldownId: string, - action: UiActionsEnhancedSerializedAction, + action: UiActionsEnhancedSerializedAction, selectedTriggers: Array ) { await run(async () => { diff --git a/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/drilldown_hello_bar.tsx b/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/drilldown_hello_bar.tsx index 48e17dadc810f0..99885e5cc40fe2 100644 --- a/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/drilldown_hello_bar.tsx +++ b/x-pack/plugins/drilldowns/public/components/drilldown_hello_bar/drilldown_hello_bar.tsx @@ -30,33 +30,30 @@ export const DrilldownHelloBar: React.FC = ({ onHideClick = () => {}, }) => { return ( - - -
- -
-
- - - {txtHelpText} - - {docsLink && ( - <> - - {txtViewDocsLinkLabel} - - )} - - - - {txtHideHelpButtonLabel} - - - - } - /> + + + +
+ +
+
+ + + {txtHelpText} + + {docsLink && ( + <> + + {txtViewDocsLinkLabel} + + )} + + + + {txtHideHelpButtonLabel} + + +
+
); }; diff --git a/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.story.tsx b/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.story.tsx index 152cd393b9d3ec..add8b748afee94 100644 --- a/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.story.tsx +++ b/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.story.tsx @@ -15,17 +15,25 @@ import { urlFactory, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../../advanced_ui_actions/public/components/action_wizard/test_data'; +import { AdvancedUiActionsActionFactory as ActionFactory } from '../../../../advanced_ui_actions/public/'; storiesOf('components/FlyoutDrilldownWizard', module) .add('default', () => { - return ; + return ( + + ); }) .add('open in flyout - create', () => { return ( {}}> {}} - drilldownActionFactories={[urlFactory, dashboardFactory]} + drilldownActionFactories={[ + urlFactory as ActionFactory, + dashboardFactory as ActionFactory, + ]} /> ); @@ -35,7 +43,10 @@ storiesOf('components/FlyoutDrilldownWizard', module) {}}> {}} - drilldownActionFactories={[urlFactory, dashboardFactory]} + drilldownActionFactories={[ + urlFactory as ActionFactory, + dashboardFactory as ActionFactory, + ]} initialDrilldownWizardConfig={{ name: 'My fancy drilldown', actionFactory: urlFactory as any, @@ -54,7 +65,7 @@ storiesOf('components/FlyoutDrilldownWizard', module) {}}> {}} - drilldownActionFactories={[dashboardFactory]} + drilldownActionFactories={[dashboardFactory as ActionFactory]} initialDrilldownWizardConfig={{ name: 'My fancy drilldown', actionFactory: urlFactory as any, diff --git a/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx b/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx index 1f775a5ff103f1..84c1a04a71d156 100644 --- a/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx +++ b/x-pack/plugins/drilldowns/public/components/flyout_drilldown_wizard/flyout_drilldown_wizard.tsx @@ -25,7 +25,7 @@ export interface DrilldownWizardConfig { } export interface FlyoutDrilldownWizardProps { - drilldownActionFactories: Array>; + drilldownActionFactories: ActionFactory[]; onSubmit?: (drilldownWizardConfig: Required) => void; onDelete?: () => void; diff --git a/x-pack/plugins/endpoint/common/generate_data.ts b/x-pack/plugins/endpoint/common/generate_data.ts index ff8add42a50851..1d5ea46dcc08b0 100644 --- a/x-pack/plugins/endpoint/common/generate_data.ts +++ b/x-pack/plugins/endpoint/common/generate_data.ts @@ -536,8 +536,14 @@ export class EndpointDocGenerator { /** * Generates a Host Policy response message */ - public generatePolicyResponse(ts = new Date().getTime()): HostPolicyResponse { + public generatePolicyResponse( + ts = new Date().getTime(), + allStatus?: HostPolicyResponseActionStatus + ): HostPolicyResponse { const policyVersion = this.seededUUIDv4(); + const status = () => { + return allStatus || this.randomHostPolicyResponseActionStatus(); + }; return { '@timestamp': ts, agent: { @@ -588,7 +594,7 @@ export class EndpointDocGenerator { status: HostPolicyResponseActionStatus.success, }, detect_image_load_events: { - message: 'Successfuly started image load event reporting', + message: 'Successfully started image load event reporting', status: HostPolicyResponseActionStatus.success, }, detect_process_events: { @@ -596,15 +602,15 @@ export class EndpointDocGenerator { status: HostPolicyResponseActionStatus.success, }, download_global_artifacts: { - message: 'Failed to download EXE model', + message: 'Succesfully downloaded global artifacts', status: HostPolicyResponseActionStatus.success, }, load_config: { - message: 'successfully parsed configuration', + message: 'Successfully parsed configuration', status: HostPolicyResponseActionStatus.success, }, load_malware_model: { - message: 'Error deserializing EXE model; no valid malware model installed', + message: 'Successfully loaded malware model', status: HostPolicyResponseActionStatus.success, }, read_elasticsearch_config: { @@ -649,19 +655,19 @@ export class EndpointDocGenerator { configurations: { events: { concerned_actions: ['download_model'], - status: this.randomHostPolicyResponseActionStatus(), + status: status(), }, logging: { concerned_actions: this.randomHostPolicyResponseActions(), - status: this.randomHostPolicyResponseActionStatus(), + status: status(), }, malware: { concerned_actions: this.randomHostPolicyResponseActions(), - status: this.randomHostPolicyResponseActionStatus(), + status: status(), }, streaming: { concerned_actions: this.randomHostPolicyResponseActions(), - status: this.randomHostPolicyResponseActionStatus(), + status: status(), }, }, }, diff --git a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx index aaeff935b32b45..808429ccef0c58 100644 --- a/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx +++ b/x-pack/plugins/endpoint/public/applications/endpoint/view/hosts/index.test.tsx @@ -328,8 +328,20 @@ describe('when on the hosts page', () => { expect(statusHealth).not.toBeNull(); expect(message).not.toBeNull(); }); - it('should not show any numbered badges if all actions are succesful', () => { - return renderResult.findByTestId('hostDetailsPolicyResponseAttentionBadge').catch(e => { + it('should not show any numbered badges if all actions are successful', () => { + const policyResponse = docGenerator.generatePolicyResponse( + new Date().getTime(), + HostPolicyResponseActionStatus.success + ); + reactTestingLibrary.act(() => { + store.dispatch({ + type: 'serverReturnedHostPolicyResponse', + payload: { + policy_response: policyResponse, + }, + }); + }); + return renderResult.findAllByTestId('hostDetailsPolicyResponseAttentionBadge').catch(e => { expect(e).not.toBeNull(); }); }); @@ -337,14 +349,18 @@ describe('when on the hosts page', () => { reactTestingLibrary.act(() => { dispatchServerReturnedHostPolicyResponse(HostPolicyResponseActionStatus.failure); }); - const attentionBadge = renderResult.findByTestId('hostDetailsPolicyResponseAttentionBadge'); + const attentionBadge = renderResult.findAllByTestId( + 'hostDetailsPolicyResponseAttentionBadge' + ); expect(attentionBadge).not.toBeNull(); }); it('should show a numbered badge if at least one action has a warning', () => { reactTestingLibrary.act(() => { dispatchServerReturnedHostPolicyResponse(HostPolicyResponseActionStatus.warning); }); - const attentionBadge = renderResult.findByTestId('hostDetailsPolicyResponseAttentionBadge'); + const attentionBadge = renderResult.findAllByTestId( + 'hostDetailsPolicyResponseAttentionBadge' + ); expect(attentionBadge).not.toBeNull(); }); it('should include the back to details link', async () => { diff --git a/x-pack/plugins/index_management/__jest__/client_integration/template_clone.test.tsx b/x-pack/plugins/index_management/__jest__/client_integration/template_clone.test.tsx index 17e19bf881dee9..fa9d13d1ddd076 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/template_clone.test.tsx +++ b/x-pack/plugins/index_management/__jest__/client_integration/template_clone.test.tsx @@ -42,7 +42,8 @@ jest.mock('@elastic/eui', () => ({ ), })); -describe('', () => { +// FLAKY: https://github.com/elastic/kibana/issues/59849 +describe.skip('', () => { let testBed: TemplateFormTestBed; const { server, httpRequestsMockHelpers } = setupEnvironment(); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/shape_datatype.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/shape_datatype.test.tsx index 19bf6973472ff3..0816770aab3a8b 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/shape_datatype.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/datatypes/shape_datatype.test.tsx @@ -20,7 +20,9 @@ export const defaultShapeParameters = { ignore_z_value: true, }; -describe('Mappings editor: shape datatype', () => { +// That test is being flaky and is under work to be fixed +// Skipping it for now. +describe.skip('Mappings editor: shape datatype', () => { let testBed: MappingsEditorTestBed; /** diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mapped_fields.test.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mapped_fields.test.tsx index 8989e85d9f188d..57040eaeefbdfa 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mapped_fields.test.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/__jest__/client_integration/mapped_fields.test.tsx @@ -10,7 +10,8 @@ import { componentHelpers, MappingsEditorTestBed, DomFields, nextTick } from './ const { setup } = componentHelpers.mappingsEditor; const onChangeHandler = jest.fn(); -describe('Mappings editor: mapped fields', () => { +// FLAKY: https://github.com/elastic/kibana/issues/65741 +describe.skip('Mappings editor: mapped fields', () => { afterEach(() => { onChangeHandler.mockReset(); }); diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx index 406f9c7602d355..8fdba86f233d46 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression.tsx @@ -74,6 +74,7 @@ export const Expressions: React.FC = props => { fetch: alertsContext.http.fetch, toastWarning: alertsContext.toastNotifications.addWarning, }); + const [timeSize, setTimeSize] = useState(1); const [timeUnit, setTimeUnit] = useState('m'); const derivedIndexPattern = useMemo(() => createDerivedIndexPattern('metrics'), [ @@ -173,52 +174,57 @@ export const Expressions: React.FC = props => { [alertParams.criteria, setAlertParams] ); - useEffect(() => { + const preFillAlertCriteria = useCallback(() => { const md = alertsContext.metadata; - if (md) { - if (md.currentOptions?.metrics) { - setAlertParams( - 'criteria', - md.currentOptions.metrics.map(metric => ({ - metric: metric.field, - comparator: Comparator.GT, - threshold: [], - timeSize, - timeUnit, - aggType: metric.aggregation, - })) - ); - } else { - setAlertParams('criteria', [defaultExpression]); - } + if (md && md.currentOptions?.metrics) { + setAlertParams( + 'criteria', + md.currentOptions.metrics.map(metric => ({ + metric: metric.field, + comparator: Comparator.GT, + threshold: [], + timeSize, + timeUnit, + aggType: metric.aggregation, + })) + ); + } else { + setAlertParams('criteria', [defaultExpression]); + } + }, [alertsContext.metadata, setAlertParams, timeSize, timeUnit]); - if (md.currentOptions) { - if (md.currentOptions.filterQuery) { - setAlertParams('filterQueryText', md.currentOptions.filterQuery); - setAlertParams( - 'filterQuery', - convertKueryToElasticSearchQuery(md.currentOptions.filterQuery, derivedIndexPattern) || - '' - ); - } else if (md.currentOptions.groupBy && md.series) { - const filter = `${md.currentOptions.groupBy}: "${md.series.id}"`; - setAlertParams('filterQueryText', filter); - setAlertParams( - 'filterQuery', - convertKueryToElasticSearchQuery(filter, derivedIndexPattern) || '' - ); - } + const preFillAlertFilter = useCallback(() => { + const md = alertsContext.metadata; + if (md && md.currentOptions?.filterQuery) { + setAlertParams('filterQueryText', md.currentOptions.filterQuery); + setAlertParams( + 'filterQuery', + convertKueryToElasticSearchQuery(md.currentOptions.filterQuery, derivedIndexPattern) || '' + ); + } else if (md && md.currentOptions?.groupBy && md.series) { + const filter = `${md.currentOptions?.groupBy}: "${md.series.id}"`; + setAlertParams('filterQueryText', filter); + setAlertParams( + 'filterQuery', + convertKueryToElasticSearchQuery(filter, derivedIndexPattern) || '' + ); + } + }, [alertsContext.metadata, derivedIndexPattern, setAlertParams]); - setAlertParams('groupBy', md.currentOptions.groupBy); - } - setAlertParams('sourceId', source?.id); + useEffect(() => { + if (alertParams.criteria && alertParams.criteria.length) { + setTimeSize(alertParams.criteria[0].timeSize); + setTimeUnit(alertParams.criteria[0].timeUnit); } else { - if (!alertParams.criteria) { - setAlertParams('criteria', [defaultExpression]); - } - if (!alertParams.sourceId) { - setAlertParams('sourceId', source?.id || 'default'); - } + preFillAlertCriteria(); + } + + if (!alertParams.filterQuery) { + preFillAlertFilter(); + } + + if (!alertParams.sourceId) { + setAlertParams('sourceId', source?.id || 'default'); } }, [alertsContext.metadata, defaultExpression, source]); // eslint-disable-line react-hooks/exhaustive-deps diff --git a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/editor.tsx b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/editor.tsx index 8bdffbeb36f3a5..06855cc7e765dc 100644 --- a/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/editor.tsx +++ b/x-pack/plugins/infra/public/components/alerting/logs/expression_editor/editor.tsx @@ -129,7 +129,7 @@ export const Editor: React.FC = props => { const { sourceStatus } = useLogSourceContext(); useMount(() => { - for (const [key, value] of Object.entries(DEFAULT_EXPRESSION)) { + for (const [key, value] of Object.entries({ ...DEFAULT_EXPRESSION, ...alertParams })) { setAlertParams(key, value); setHasSetDefaults(true); } diff --git a/x-pack/plugins/infra/server/lib/alerting/common/utils.ts b/x-pack/plugins/infra/server/lib/alerting/common/utils.ts index a2c5b27c38fd68..5ca65b667ae11a 100644 --- a/x-pack/plugins/infra/server/lib/alerting/common/utils.ts +++ b/x-pack/plugins/infra/server/lib/alerting/common/utils.ts @@ -13,6 +13,11 @@ export const oneOfLiterals = (arrayOfLiterals: Readonly) => }); export const validateIsStringElasticsearchJSONFilter = (value: string) => { + if (value === '') { + // Allow clearing the filter. + return; + } + const errorMessage = 'filterQuery must be a valid Elasticsearch filter expressed in JSON'; try { const parsedValue = JSON.parse(value); diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/messages.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/messages.ts index 4878574e39d16f..4add0ee9af5d3c 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/messages.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/messages.ts @@ -24,7 +24,6 @@ export const stateToAlertMessage = { [AlertStates.ERROR]: i18n.translate('xpack.infra.metrics.alerting.threshold.errorState', { defaultMessage: 'ERROR', }), - // TODO: Implement recovered message state [AlertStates.OK]: i18n.translate('xpack.infra.metrics.alerting.threshold.okState', { defaultMessage: 'OK [Recovered]', }), @@ -62,6 +61,33 @@ const comparatorToI18n = (comparator: Comparator, threshold: number[], currentVa } }; +const recoveredComparatorToI18n = ( + comparator: Comparator, + threshold: number[], + currentValue: number +) => { + const belowText = i18n.translate('xpack.infra.metrics.alerting.threshold.belowRecovery', { + defaultMessage: 'below', + }); + const aboveText = i18n.translate('xpack.infra.metrics.alerting.threshold.aboveRecovery', { + defaultMessage: 'above', + }); + switch (comparator) { + case Comparator.BETWEEN: + return currentValue < threshold[0] ? belowText : aboveText; + case Comparator.OUTSIDE_RANGE: + return i18n.translate('xpack.infra.metrics.alerting.threshold.betweenRecovery', { + defaultMessage: 'between', + }); + case Comparator.GT: + case Comparator.GT_OR_EQ: + return belowText; + case Comparator.LT: + case Comparator.LT_OR_EQ: + return aboveText; + } +}; + const thresholdToI18n = ([a, b]: number[]) => { if (typeof b === 'undefined') return a; return i18n.translate('xpack.infra.metrics.alerting.threshold.thresholdRange', { @@ -87,6 +113,23 @@ export const buildFiredAlertReason: (alertResult: { }, }); +export const buildRecoveredAlertReason: (alertResult: { + metric: string; + comparator: Comparator; + threshold: number[]; + currentValue: number; +}) => string = ({ metric, comparator, threshold, currentValue }) => + i18n.translate('xpack.infra.metrics.alerting.threshold.recoveredAlertReason', { + defaultMessage: + '{metric} is now {comparator} a threshold of {threshold} (current value is {currentValue})', + values: { + metric, + comparator: recoveredComparatorToI18n(comparator, threshold, currentValue), + threshold: thresholdToI18n(threshold), + currentValue, + }, + }); + export const buildNoDataAlertReason: (alertResult: { metric: string; timeSize: number; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index ed5efc1473953f..19efc88e216cab 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -20,6 +20,8 @@ interface AlertTestInstance { state: any; } +let persistAlertInstances = false; // eslint-disable-line + describe('The metric threshold alert type', () => { describe('querying the entire infrastructure', () => { const instanceID = 'test-*'; @@ -313,6 +315,50 @@ describe('The metric threshold alert type', () => { expect(getState(instanceID).alertState).toBe(AlertStates.NO_DATA); }); }); + + // describe('querying a metric that later recovers', () => { + // const instanceID = 'test-*'; + // const execute = (threshold: number[]) => + // executor({ + // services, + // params: { + // criteria: [ + // { + // ...baseCriterion, + // comparator: Comparator.GT, + // threshold, + // }, + // ], + // }, + // }); + // beforeAll(() => (persistAlertInstances = true)); + // afterAll(() => (persistAlertInstances = false)); + + // test('sends a recovery alert as soon as the metric recovers', async () => { + // await execute([0.5]); + // expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + // expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); + // await execute([2]); + // expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + // expect(getState(instanceID).alertState).toBe(AlertStates.OK); + // }); + // test('does not continue to send a recovery alert if the metric is still OK', async () => { + // await execute([2]); + // expect(mostRecentAction(instanceID)).toBe(undefined); + // expect(getState(instanceID).alertState).toBe(AlertStates.OK); + // await execute([2]); + // expect(mostRecentAction(instanceID)).toBe(undefined); + // expect(getState(instanceID).alertState).toBe(AlertStates.OK); + // }); + // test('sends a recovery alert again once the metric alerts and recovers again', async () => { + // await execute([0.5]); + // expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + // expect(getState(instanceID).alertState).toBe(AlertStates.ALERT); + // await execute([2]); + // expect(mostRecentAction(instanceID).id).toBe(FIRED_ACTIONS.id); + // expect(getState(instanceID).alertState).toBe(AlertStates.OK); + // }); + // }); }); const createMockStaticConfiguration = (sources: any) => ({ @@ -397,12 +443,19 @@ services.savedObjectsClient.get.mockImplementation(async (type: string, sourceId const alertInstances = new Map(); services.alertInstanceFactory.mockImplementation((instanceID: string) => { - const alertInstance: AlertTestInstance = { + const newAlertInstance: AlertTestInstance = { instance: alertsMock.createAlertInstanceFactory(), actionQueue: [], state: {}, }; + const alertInstance: AlertTestInstance = persistAlertInstances + ? alertInstances.get(instanceID) || newAlertInstance + : newAlertInstance; alertInstances.set(instanceID, alertInstance); + + alertInstance.instance.getState.mockImplementation(() => { + return alertInstance.state; + }); alertInstance.instance.replaceState.mockImplementation((newState: any) => { alertInstance.state = newState; return alertInstance.instance; diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts index 24f3a11789dc7f..cd44b61974b035 100644 --- a/x-pack/plugins/ingest_manager/server/plugin.ts +++ b/x-pack/plugins/ingest_manager/server/plugin.ts @@ -8,6 +8,7 @@ import { first } from 'rxjs/operators'; import { CoreSetup, CoreStart, + Logger, Plugin, PluginInitializerContext, SavedObjectsServiceStart, @@ -73,6 +74,7 @@ export interface IngestManagerAppContext { isProductionMode: boolean; kibanaVersion: string; cloud?: CloudSetup; + logger?: Logger; httpSetup?: HttpServiceSetup; } @@ -108,6 +110,7 @@ export class IngestManagerPlugin private config$: Observable; private security: SecurityPluginSetup | undefined; private cloud: CloudSetup | undefined; + private logger: Logger | undefined; private isProductionMode: boolean; private kibanaVersion: string; @@ -117,6 +120,7 @@ export class IngestManagerPlugin this.config$ = this.initializerContext.config.create(); this.isProductionMode = this.initializerContext.env.mode.prod; this.kibanaVersion = this.initializerContext.env.packageInfo.version; + this.logger = this.initializerContext.logger.get(); } public async setup(core: CoreSetup, deps: IngestManagerSetupDeps) { @@ -208,6 +212,7 @@ export class IngestManagerPlugin kibanaVersion: this.kibanaVersion, httpSetup: this.httpSetup, cloud: this.cloud, + logger: this.logger, }); licenseService.start(this.licensing$); return { diff --git a/x-pack/plugins/ingest_manager/server/services/app_context.ts b/x-pack/plugins/ingest_manager/server/services/app_context.ts index 6da0a137fa087e..e5bc082f6f0fc7 100644 --- a/x-pack/plugins/ingest_manager/server/services/app_context.ts +++ b/x-pack/plugins/ingest_manager/server/services/app_context.ts @@ -5,7 +5,7 @@ */ import { BehaviorSubject, Observable } from 'rxjs'; import { first } from 'rxjs/operators'; -import { SavedObjectsServiceStart, HttpServiceSetup } from 'src/core/server'; +import { SavedObjectsServiceStart, HttpServiceSetup, Logger } from 'src/core/server'; import { EncryptedSavedObjectsPluginStart } from '../../../encrypted_saved_objects/server'; import { SecurityPluginSetup } from '../../../security/server'; import { IngestManagerConfigType } from '../../common'; @@ -21,6 +21,7 @@ class AppContextService { private isProductionMode: boolean = false; private kibanaVersion: string | undefined; private cloud?: CloudSetup; + private logger?: Logger; private httpSetup?: HttpServiceSetup; public async start(appContext: IngestManagerAppContext) { @@ -29,6 +30,7 @@ class AppContextService { this.savedObjects = appContext.savedObjects; this.isProductionMode = appContext.isProductionMode; this.cloud = appContext.cloud; + this.logger = appContext.logger; this.kibanaVersion = appContext.kibanaVersion; this.httpSetup = appContext.httpSetup; @@ -60,6 +62,10 @@ class AppContextService { return this.cloud; } + public getLogger() { + return this.logger; + } + public getConfig() { return this.configSubject$?.value; } diff --git a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx index e0be8d2937729c..6acb6369e2e907 100644 --- a/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx +++ b/x-pack/plugins/ingest_pipelines/__jest__/client_integration/ingest_pipelines_create.test.tsx @@ -107,15 +107,11 @@ describe('', () => { component.update(); }); - expect(form.getErrorsMessages()).toEqual([ - 'Name is required.', - 'A description is required.', - ]); + expect(form.getErrorsMessages()).toEqual(['Name is required.']); expect(find('submitButton').props().disabled).toEqual(true); // Add required fields and verify button is enabled again form.setInputValue('nameField.input', 'my_pipeline'); - form.setInputValue('descriptionField.input', 'pipeline description'); await act(async () => { await nextTick(); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx index f222d48c11af00..e122307ca9485d 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_form/schema.tsx @@ -34,17 +34,8 @@ export const pipelineFormSchema: FormSchema = { description: { type: FIELD_TYPES.TEXTAREA, label: i18n.translate('xpack.ingestPipelines.form.descriptionFieldLabel', { - defaultMessage: 'Description', + defaultMessage: 'Description (optional)', }), - validations: [ - { - validator: emptyField( - i18n.translate('xpack.ingestPipelines.form.pipelineDescriptionRequiredError', { - defaultMessage: 'A description is required.', - }) - ), - }, - ], }, processors: { label: i18n.translate('xpack.ingestPipelines.form.processorsFieldLabel', { diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_flyout.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_flyout.tsx index a6eff63b5e725f..39789bb38ed41b 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_flyout.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/details_flyout.tsx @@ -117,14 +117,16 @@ export const PipelineDetailsFlyout: FunctionComponent = ({ {/* Pipeline description */} - - {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.descriptionTitle', { - defaultMessage: 'Description', - })} - - - {pipeline.description ?? ''} - + {pipeline.description && ( + <> + + {i18n.translate('xpack.ingestPipelines.list.pipelineDetails.descriptionTitle', { + defaultMessage: 'Description', + })} + + {pipeline.description} + + )} {/* Pipeline version */} {pipeline.version && ( diff --git a/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts b/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts index 63637eaac765d8..803d34bf0042b2 100644 --- a/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts +++ b/x-pack/plugins/ingest_pipelines/server/routes/api/create.ts @@ -12,7 +12,7 @@ import { RouteDependencies } from '../../types'; const bodySchema = schema.object({ name: schema.string(), - description: schema.string(), + description: schema.maybe(schema.string()), processors: schema.arrayOf(schema.recordOf(schema.string(), schema.any())), version: schema.maybe(schema.number()), on_failure: schema.maybe(schema.arrayOf(schema.recordOf(schema.string(), schema.any()))), diff --git a/x-pack/plugins/maps/kibana.json b/x-pack/plugins/maps/kibana.json index 077601204e3eeb..efbac56a817be8 100644 --- a/x-pack/plugins/maps/kibana.json +++ b/x-pack/plugins/maps/kibana.json @@ -4,8 +4,9 @@ "kibanaVersion": "kibana", "configPath": ["xpack", "maps"], "requiredPlugins": [ - "inspector", "licensing", + "features", + "inspector", "home", "data", "fileUpload", @@ -15,5 +16,6 @@ "embeddable", "mapsLegacy" ], - "ui": true + "ui": true, + "server": true } diff --git a/x-pack/plugins/maps/public/angular/get_initial_layers.js b/x-pack/plugins/maps/public/angular/get_initial_layers.js index 09f66740af3721..598fd6ce324d02 100644 --- a/x-pack/plugins/maps/public/angular/get_initial_layers.js +++ b/x-pack/plugins/maps/public/angular/get_initial_layers.js @@ -5,17 +5,17 @@ */ import _ from 'lodash'; // Import each layer type, even those not used, to init in registry -import '../layers/sources/wms_source'; -import '../layers/sources/ems_file_source'; -import '../layers/sources/es_search_source'; -import '../layers/sources/es_pew_pew_source'; -import '../layers/sources/kibana_regionmap_source'; -import '../layers/sources/es_geo_grid_source'; -import '../layers/sources/xyz_tms_source'; -import { KibanaTilemapSource } from '../layers/sources/kibana_tilemap_source'; -import { TileLayer } from '../layers/tile_layer'; -import { EMSTMSSource } from '../layers/sources/ems_tms_source'; -import { VectorTileLayer } from '../layers/vector_tile_layer'; +import '../classes/sources/wms_source'; +import '../classes/sources/ems_file_source'; +import '../classes/sources/es_search_source'; +import '../classes/sources/es_pew_pew_source'; +import '../classes/sources/kibana_regionmap_source'; +import '../classes/sources/es_geo_grid_source'; +import '../classes/sources/xyz_tms_source'; +import { KibanaTilemapSource } from '../classes/sources/kibana_tilemap_source'; +import { TileLayer } from '../classes/layers/tile_layer/tile_layer'; +import { EMSTMSSource } from '../classes/sources/ems_tms_source'; +import { VectorTileLayer } from '../classes/layers/vector_tile_layer/vector_tile_layer'; import { getIsEmsEnabled } from '../kibana_services'; import { getKibanaTileMap } from '../meta'; diff --git a/x-pack/plugins/maps/public/angular/services/saved_gis_map.js b/x-pack/plugins/maps/public/angular/services/saved_gis_map.js index 1a58b0cefaed97..2de4432871347c 100644 --- a/x-pack/plugins/maps/public/angular/services/saved_gis_map.js +++ b/x-pack/plugins/maps/public/angular/services/saved_gis_map.js @@ -19,7 +19,7 @@ import { } from '../../selectors/map_selectors'; import { getIsLayerTOCOpen, getOpenTOCDetails } from '../../selectors/ui_selectors'; -import { convertMapExtentToPolygon } from '../../elasticsearch_geo_utils'; +import { formatEnvelopeAsPolygon } from '../../elasticsearch_geo_utils'; import { copyPersistentState } from '../../reducers/util'; import { extractReferences, injectReferences } from '../../../common/migrations/references'; @@ -107,7 +107,7 @@ export function createSavedGisMapClass(services) { openTOCDetails: getOpenTOCDetails(state), }); - this.bounds = convertMapExtentToPolygon(getMapExtent(state)); + this.bounds = formatEnvelopeAsPolygon(getMapExtent(state)); } } return SavedGisMap; diff --git a/x-pack/plugins/maps/public/layers/_index.scss b/x-pack/plugins/maps/public/classes/_index.scss similarity index 100% rename from x-pack/plugins/maps/public/layers/_index.scss rename to x-pack/plugins/maps/public/classes/_index.scss diff --git a/x-pack/plugins/maps/public/layers/fields/ems_file_field.ts b/x-pack/plugins/maps/public/classes/fields/ems_file_field.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/fields/ems_file_field.ts rename to x-pack/plugins/maps/public/classes/fields/ems_file_field.ts diff --git a/x-pack/plugins/maps/public/layers/fields/es_agg_field.test.ts b/x-pack/plugins/maps/public/classes/fields/es_agg_field.test.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/fields/es_agg_field.test.ts rename to x-pack/plugins/maps/public/classes/fields/es_agg_field.test.ts diff --git a/x-pack/plugins/maps/public/layers/fields/es_agg_field.ts b/x-pack/plugins/maps/public/classes/fields/es_agg_field.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/fields/es_agg_field.ts rename to x-pack/plugins/maps/public/classes/fields/es_agg_field.ts diff --git a/x-pack/plugins/maps/public/layers/fields/es_doc_field.ts b/x-pack/plugins/maps/public/classes/fields/es_doc_field.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/fields/es_doc_field.ts rename to x-pack/plugins/maps/public/classes/fields/es_doc_field.ts diff --git a/x-pack/plugins/maps/public/layers/fields/field.ts b/x-pack/plugins/maps/public/classes/fields/field.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/fields/field.ts rename to x-pack/plugins/maps/public/classes/fields/field.ts diff --git a/x-pack/plugins/maps/public/layers/fields/kibana_region_field.ts b/x-pack/plugins/maps/public/classes/fields/kibana_region_field.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/fields/kibana_region_field.ts rename to x-pack/plugins/maps/public/classes/fields/kibana_region_field.ts diff --git a/x-pack/plugins/maps/public/layers/fields/top_term_percentage_field.ts b/x-pack/plugins/maps/public/classes/fields/top_term_percentage_field.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/fields/top_term_percentage_field.ts rename to x-pack/plugins/maps/public/classes/fields/top_term_percentage_field.ts diff --git a/x-pack/plugins/maps/public/layers/joins/inner_join.js b/x-pack/plugins/maps/public/classes/joins/inner_join.js similarity index 100% rename from x-pack/plugins/maps/public/layers/joins/inner_join.js rename to x-pack/plugins/maps/public/classes/joins/inner_join.js diff --git a/x-pack/plugins/maps/public/layers/joins/inner_join.test.js b/x-pack/plugins/maps/public/classes/joins/inner_join.test.js similarity index 98% rename from x-pack/plugins/maps/public/layers/joins/inner_join.test.js rename to x-pack/plugins/maps/public/classes/joins/inner_join.test.js index f197a67becfaef..ca40ab1ea7db7f 100644 --- a/x-pack/plugins/maps/public/layers/joins/inner_join.test.js +++ b/x-pack/plugins/maps/public/classes/joins/inner_join.test.js @@ -7,7 +7,7 @@ import { InnerJoin } from './inner_join'; jest.mock('../../kibana_services', () => {}); -jest.mock('../vector_layer', () => {}); +jest.mock('../layers/vector_layer/vector_layer', () => {}); const rightSource = { id: 'd3625663-5b34-4d50-a784-0d743f676a0c', diff --git a/x-pack/plugins/maps/public/layers/joins/join.ts b/x-pack/plugins/maps/public/classes/joins/join.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/joins/join.ts rename to x-pack/plugins/maps/public/classes/joins/join.ts diff --git a/x-pack/plugins/maps/public/layers/blended_vector_layer.ts b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts similarity index 89% rename from x-pack/plugins/maps/public/layers/blended_vector_layer.ts rename to x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts index adf04b4155659c..b5b824c8594c3d 100644 --- a/x-pack/plugins/maps/public/layers/blended_vector_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts @@ -5,11 +5,11 @@ */ import { i18n } from '@kbn/i18n'; -import { VectorLayer } from './vector_layer'; -import { IVectorStyle, VectorStyle } from './styles/vector/vector_style'; -import { getDefaultDynamicProperties } from './styles/vector/vector_style_defaults'; -import { IDynamicStyleProperty } from './styles/vector/properties/dynamic_style_property'; -import { IStyleProperty } from './styles/vector/properties/style_property'; +import { VectorLayer } from '../vector_layer/vector_layer'; +import { IVectorStyle, VectorStyle } from '../../styles/vector/vector_style'; +import { getDefaultDynamicProperties } from '../../styles/vector/vector_style_defaults'; +import { IDynamicStyleProperty } from '../../styles/vector/properties/dynamic_style_property'; +import { IStyleProperty } from '../../styles/vector/properties/style_property'; import { SOURCE_TYPES, COUNT_PROP_LABEL, @@ -21,23 +21,23 @@ import { VECTOR_STYLES, LAYER_STYLE_TYPE, FIELD_ORIGIN, -} from '../../common/constants'; -import { ESGeoGridSource } from './sources/es_geo_grid_source/es_geo_grid_source'; -import { canSkipSourceUpdate } from './util/can_skip_fetch'; -import { IVectorLayer } from './vector_layer'; -import { IESSource } from './sources/es_source'; -import { IESAggSource } from './sources/es_agg_source'; -import { ISource } from './sources/source'; -import { SyncContext } from '../actions/map_actions'; -import { DataRequestAbortError } from './util/data_request'; +} from '../../../../common/constants'; +import { ESGeoGridSource } from '../../sources/es_geo_grid_source/es_geo_grid_source'; +import { canSkipSourceUpdate } from '../../util/can_skip_fetch'; +import { IVectorLayer } from '../vector_layer/vector_layer'; +import { IESSource } from '../../sources/es_source'; +import { IESAggSource } from '../../sources/es_agg_source'; +import { ISource } from '../../sources/source'; +import { SyncContext } from '../../../actions/map_actions'; +import { DataRequestAbortError } from '../../util/data_request'; import { VectorStyleDescriptor, SizeDynamicOptions, DynamicStylePropertyOptions, VectorLayerDescriptor, -} from '../../common/descriptor_types'; -import { IStyle } from './styles/style'; -import { IVectorSource } from './sources/vector_source'; +} from '../../../../common/descriptor_types'; +import { IStyle } from '../../styles/style'; +import { IVectorSource } from '../../sources/vector_source'; const ACTIVE_COUNT_DATA_ID = 'ACTIVE_COUNT_DATA_ID'; diff --git a/x-pack/plugins/maps/public/layers/heatmap_layer.js b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.js similarity index 93% rename from x-pack/plugins/maps/public/layers/heatmap_layer.js rename to x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.js index 22f7a92c17c51a..f6b9bd62802901 100644 --- a/x-pack/plugins/maps/public/layers/heatmap_layer.js +++ b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.js @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AbstractLayer } from './layer'; -import { VectorLayer } from './vector_layer'; -import { HeatmapStyle } from './styles/heatmap/heatmap_style'; -import { EMPTY_FEATURE_COLLECTION, LAYER_TYPE } from '../../common/constants'; +import { AbstractLayer } from '../layer'; +import { VectorLayer } from '../vector_layer/vector_layer'; +import { HeatmapStyle } from '../../styles/heatmap/heatmap_style'; +import { EMPTY_FEATURE_COLLECTION, LAYER_TYPE } from '../../../../common/constants'; const SCALED_PROPERTY_NAME = '__kbn_heatmap_weight__'; //unique name to store scaled value for weighting diff --git a/x-pack/plugins/maps/public/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx similarity index 97% rename from x-pack/plugins/maps/public/layers/layer.tsx rename to x-pack/plugins/maps/public/classes/layers/layer.tsx index 8ecaf4d903251f..c46d22ef0bd889 100644 --- a/x-pack/plugins/maps/public/layers/layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx @@ -12,25 +12,25 @@ import { EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; import uuid from 'uuid/v4'; import { i18n } from '@kbn/i18n'; import { FeatureCollection } from 'geojson'; -import { DataRequest } from './util/data_request'; +import { DataRequest } from '../util/data_request'; import { MAX_ZOOM, MB_SOURCE_ID_LAYER_ID_PREFIX_DELIMITER, MIN_ZOOM, SOURCE_DATA_ID_ORIGIN, -} from '../../common/constants'; +} from '../../../common/constants'; // @ts-ignore // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { copyPersistentState } from '../reducers/util.js'; +import { copyPersistentState } from '../../reducers/util.js'; import { LayerDescriptor, MapExtent, MapFilters, StyleDescriptor, -} from '../../common/descriptor_types'; -import { Attribution, ImmutableSourceProperty, ISource, SourceEditorArgs } from './sources/source'; -import { SyncContext } from '../actions/map_actions'; -import { IStyle } from './styles/style'; +} from '../../../common/descriptor_types'; +import { Attribution, ImmutableSourceProperty, ISource, SourceEditorArgs } from '../sources/source'; +import { SyncContext } from '../../actions/map_actions'; +import { IStyle } from '../styles/style'; export interface ILayer { getBounds(mapFilters: MapFilters): Promise; diff --git a/x-pack/plugins/maps/public/layers/layer_wizard_registry.ts b/x-pack/plugins/maps/public/classes/layers/layer_wizard_registry.ts similarity index 95% rename from x-pack/plugins/maps/public/layers/layer_wizard_registry.ts rename to x-pack/plugins/maps/public/classes/layers/layer_wizard_registry.ts index f866fc10b1f475..7a2a038c1b2864 100644 --- a/x-pack/plugins/maps/public/layers/layer_wizard_registry.ts +++ b/x-pack/plugins/maps/public/classes/layers/layer_wizard_registry.ts @@ -6,7 +6,7 @@ /* eslint-disable @typescript-eslint/consistent-type-definitions */ import { ReactElement } from 'react'; -import { LayerDescriptor } from '../../common/descriptor_types'; +import { LayerDescriptor } from '../../../common/descriptor_types'; export type RenderWizardArguments = { previewLayer: (layerDescriptor: LayerDescriptor | null, isIndexingSource?: boolean) => void; diff --git a/x-pack/plugins/maps/public/classes/layers/load_layer_wizards.ts b/x-pack/plugins/maps/public/classes/layers/load_layer_wizards.ts new file mode 100644 index 00000000000000..590c1a9d6ae448 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/load_layer_wizards.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { registerLayerWizard } from './layer_wizard_registry'; +// @ts-ignore +import { uploadLayerWizardConfig } from '../sources/client_file_source'; +// @ts-ignore +import { esDocumentsLayerWizardConfig } from '../sources/es_search_source'; +// @ts-ignore +import { clustersLayerWizardConfig, heatmapLayerWizardConfig } from '../sources/es_geo_grid_source'; +// @ts-ignore +import { point2PointLayerWizardConfig } from '../sources/es_pew_pew_source'; +// @ts-ignore +import { emsBoundariesLayerWizardConfig } from '../sources/ems_file_source'; +// @ts-ignore +import { emsBaseMapLayerWizardConfig } from '../sources/ems_tms_source'; +// @ts-ignore +import { kibanaRegionMapLayerWizardConfig } from '../sources/kibana_regionmap_source'; +// @ts-ignore +import { kibanaBasemapLayerWizardConfig } from '../sources/kibana_tilemap_source'; +import { tmsLayerWizardConfig } from '../sources/xyz_tms_source'; +// @ts-ignore +import { wmsLayerWizardConfig } from '../sources/wms_source'; +import { mvtVectorSourceWizardConfig } from '../sources/mvt_single_layer_vector_source'; +import { ObservabilityLayerWizardConfig } from './solution_layers/observability'; +import { getInjectedVarFunc } from '../../kibana_services'; + +let registered = false; +export function registerLayerWizards() { + if (registered) { + return; + } + + // Registration order determines display order + // @ts-ignore + registerLayerWizard(uploadLayerWizardConfig); + registerLayerWizard(ObservabilityLayerWizardConfig); + // @ts-ignore + registerLayerWizard(esDocumentsLayerWizardConfig); + // @ts-ignore + registerLayerWizard(clustersLayerWizardConfig); + // @ts-ignore + registerLayerWizard(heatmapLayerWizardConfig); + // @ts-ignore + registerLayerWizard(point2PointLayerWizardConfig); + // @ts-ignore + registerLayerWizard(emsBoundariesLayerWizardConfig); + // @ts-ignore + registerLayerWizard(emsBaseMapLayerWizardConfig); + // @ts-ignore + registerLayerWizard(kibanaRegionMapLayerWizardConfig); + // @ts-ignore + registerLayerWizard(kibanaBasemapLayerWizardConfig); + registerLayerWizard(tmsLayerWizardConfig); + // @ts-ignore + registerLayerWizard(wmsLayerWizardConfig); + + const getInjectedVar = getInjectedVarFunc(); + if (getInjectedVar && getInjectedVar('enableVectorTiles', false)) { + // eslint-disable-next-line no-console + console.warn('Vector tiles are an experimental feature and should not be used in production.'); + registerLayerWizard(mvtVectorSourceWizardConfig); + } + registered = true; +} diff --git a/x-pack/plugins/maps/public/layers/solution_layers/observability/create_layer_descriptor.test.ts b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.test.ts similarity index 99% rename from x-pack/plugins/maps/public/layers/solution_layers/observability/create_layer_descriptor.test.ts rename to x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.test.ts index 6c0b6cdc39b85f..ce079d67c15e4e 100644 --- a/x-pack/plugins/maps/public/layers/solution_layers/observability/create_layer_descriptor.test.ts +++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('../../../kibana_services', () => { +jest.mock('../../../../kibana_services', () => { const mockUiSettings = { get: () => { return undefined; diff --git a/x-pack/plugins/maps/public/layers/solution_layers/observability/create_layer_descriptor.ts b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.ts similarity index 92% rename from x-pack/plugins/maps/public/layers/solution_layers/observability/create_layer_descriptor.ts rename to x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.ts index e2833d5abd0c22..ba019f97b287fa 100644 --- a/x-pack/plugins/maps/public/layers/solution_layers/observability/create_layer_descriptor.ts +++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/create_layer_descriptor.ts @@ -13,7 +13,7 @@ import { SizeDynamicOptions, StylePropertyField, VectorStylePropertiesDescriptor, -} from '../../../../common/descriptor_types'; +} from '../../../../../common/descriptor_types'; import { AGG_TYPE, COLOR_MAP_TYPE, @@ -23,20 +23,20 @@ import { SOURCE_TYPES, STYLE_TYPE, VECTOR_STYLES, -} from '../../../../common/constants'; -import { getJoinAggKey, getSourceAggKey } from '../../../../common/get_agg_key'; +} from '../../../../../common/constants'; +import { getJoinAggKey, getSourceAggKey } from '../../../../../common/get_agg_key'; import { OBSERVABILITY_LAYER_TYPE } from './layer_select'; import { OBSERVABILITY_METRIC_TYPE } from './metric_select'; import { DISPLAY } from './display_select'; -import { VectorStyle } from '../../styles/vector/vector_style'; +import { VectorStyle } from '../../../styles/vector/vector_style'; // @ts-ignore -import { EMSFileSource } from '../../sources/ems_file_source'; +import { EMSFileSource } from '../../../sources/ems_file_source'; // @ts-ignore -import { ESGeoGridSource } from '../../sources/es_geo_grid_source'; -import { VectorLayer } from '../../vector_layer'; +import { ESGeoGridSource } from '../../../sources/es_geo_grid_source'; +import { VectorLayer } from '../../vector_layer/vector_layer'; // @ts-ignore -import { HeatmapLayer } from '../../heatmap_layer'; -import { getDefaultDynamicProperties } from '../../styles/vector/vector_style_defaults'; +import { HeatmapLayer } from '../../heatmap_layer/heatmap_layer'; +import { getDefaultDynamicProperties } from '../../../styles/vector/vector_style_defaults'; // redefining APM constant to avoid making maps app depend on APM plugin export const APM_INDEX_PATTERN_ID = 'apm_static_index_pattern_id'; diff --git a/x-pack/plugins/maps/public/layers/solution_layers/observability/display_select.tsx b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/display_select.tsx similarity index 100% rename from x-pack/plugins/maps/public/layers/solution_layers/observability/display_select.tsx rename to x-pack/plugins/maps/public/classes/layers/solution_layers/observability/display_select.tsx diff --git a/x-pack/plugins/maps/public/layers/solution_layers/observability/index.ts b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/index.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/solution_layers/observability/index.ts rename to x-pack/plugins/maps/public/classes/layers/solution_layers/observability/index.ts diff --git a/x-pack/plugins/maps/public/layers/solution_layers/observability/layer_select.tsx b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/layer_select.tsx similarity index 100% rename from x-pack/plugins/maps/public/layers/solution_layers/observability/layer_select.tsx rename to x-pack/plugins/maps/public/classes/layers/solution_layers/observability/layer_select.tsx diff --git a/x-pack/plugins/maps/public/layers/solution_layers/observability/metric_select.tsx b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/metric_select.tsx similarity index 100% rename from x-pack/plugins/maps/public/layers/solution_layers/observability/metric_select.tsx rename to x-pack/plugins/maps/public/classes/layers/solution_layers/observability/metric_select.tsx diff --git a/x-pack/plugins/maps/public/layers/solution_layers/observability/observability_layer_template.tsx b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/observability_layer_template.tsx similarity index 100% rename from x-pack/plugins/maps/public/layers/solution_layers/observability/observability_layer_template.tsx rename to x-pack/plugins/maps/public/classes/layers/solution_layers/observability/observability_layer_template.tsx diff --git a/x-pack/plugins/maps/public/layers/solution_layers/observability/observability_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/observability_layer_wizard.tsx similarity index 94% rename from x-pack/plugins/maps/public/layers/solution_layers/observability/observability_layer_wizard.tsx rename to x-pack/plugins/maps/public/classes/layers/solution_layers/observability/observability_layer_wizard.tsx index 3fbb3157ae62a0..db97c08596e065 100644 --- a/x-pack/plugins/maps/public/layers/solution_layers/observability/observability_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/layers/solution_layers/observability/observability_layer_wizard.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; import { ObservabilityLayerTemplate } from './observability_layer_template'; import { APM_INDEX_PATTERN_ID } from './create_layer_descriptor'; -import { getIndexPatternService } from '../../../kibana_services'; +import { getIndexPatternService } from '../../../../kibana_services'; export const ObservabilityLayerWizardConfig: LayerWizard = { checkVisibility: async () => { diff --git a/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.d.ts b/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.d.ts new file mode 100644 index 00000000000000..6f719d8abdcb91 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.d.ts @@ -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. + */ + +import { AbstractLayer } from '../layer'; +import { ITMSSource } from '../../sources/tms_source'; +import { LayerDescriptor } from '../../../../common/descriptor_types'; + +interface ITileLayerArguments { + source: ITMSSource; + layerDescriptor: LayerDescriptor; +} + +export class TileLayer extends AbstractLayer { + constructor(args: ITileLayerArguments); +} diff --git a/x-pack/plugins/maps/public/layers/tile_layer.js b/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.js similarity index 95% rename from x-pack/plugins/maps/public/layers/tile_layer.js rename to x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.js index baded3c287637c..69f5033e3af0fa 100644 --- a/x-pack/plugins/maps/public/layers/tile_layer.js +++ b/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.js @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AbstractLayer } from './layer'; +import { AbstractLayer } from '../layer'; import _ from 'lodash'; -import { SOURCE_DATA_ID_ORIGIN, LAYER_TYPE, LAYER_STYLE_TYPE } from '../../common/constants'; -import { TileStyle } from './styles/tile/tile_style'; +import { SOURCE_DATA_ID_ORIGIN, LAYER_TYPE, LAYER_STYLE_TYPE } from '../../../../common/constants'; +import { TileStyle } from '../../styles/tile/tile_style'; export class TileLayer extends AbstractLayer { static type = LAYER_TYPE.TILE; diff --git a/x-pack/plugins/maps/public/layers/tile_layer.test.ts b/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.test.ts similarity index 87% rename from x-pack/plugins/maps/public/layers/tile_layer.test.ts rename to x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.test.ts index d536b18af4aade..7954d0c59d97f1 100644 --- a/x-pack/plugins/maps/public/layers/tile_layer.test.ts +++ b/x-pack/plugins/maps/public/classes/layers/tile_layer/tile_layer.test.ts @@ -6,10 +6,10 @@ // eslint-disable-next-line max-classes-per-file import { ITileLayerArguments, TileLayer } from './tile_layer'; -import { SOURCE_TYPES } from '../../common/constants'; -import { XYZTMSSourceDescriptor } from '../../common/descriptor_types'; -import { ITMSSource, AbstractTMSSource } from './sources/tms_source'; -import { ILayer } from './layer'; +import { SOURCE_TYPES } from '../../../../common/constants'; +import { XYZTMSSourceDescriptor } from '../../../../common/descriptor_types'; +import { ITMSSource, AbstractTMSSource } from '../../sources/tms_source'; +import { ILayer } from '../layer'; const sourceDescriptor: XYZTMSSourceDescriptor = { type: SOURCE_TYPES.EMS_XYZ, diff --git a/x-pack/plugins/maps/public/layers/tiled_vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx similarity index 87% rename from x-pack/plugins/maps/public/layers/tiled_vector_layer.tsx rename to x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx index 06c5ef579b2212..bb4fbe9d01b609 100644 --- a/x-pack/plugins/maps/public/layers/tiled_vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx @@ -6,15 +6,18 @@ import React from 'react'; import { EuiIcon } from '@elastic/eui'; -import { VectorStyle } from './styles/vector/vector_style'; -import { SOURCE_DATA_ID_ORIGIN, LAYER_TYPE } from '../../common/constants'; -import { VectorLayer, VectorLayerArguments } from './vector_layer'; -import { canSkipSourceUpdate } from './util/can_skip_fetch'; -import { ITiledSingleLayerVectorSource } from './sources/vector_source'; -import { SyncContext } from '../actions/map_actions'; -import { ISource } from './sources/source'; -import { VectorLayerDescriptor, VectorSourceRequestMeta } from '../../common/descriptor_types'; -import { MVTSingleLayerVectorSourceConfig } from './sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor'; +import { VectorStyle } from '../../styles/vector/vector_style'; +import { SOURCE_DATA_ID_ORIGIN, LAYER_TYPE } from '../../../../common/constants'; +import { VectorLayer, VectorLayerArguments } from '../vector_layer/vector_layer'; +import { canSkipSourceUpdate } from '../../util/can_skip_fetch'; +import { ITiledSingleLayerVectorSource } from '../../sources/vector_source'; +import { SyncContext } from '../../../actions/map_actions'; +import { ISource } from '../../sources/source'; +import { + VectorLayerDescriptor, + VectorSourceRequestMeta, +} from '../../../../common/descriptor_types'; +import { MVTSingleLayerVectorSourceConfig } from '../../sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor'; export class TiledVectorLayer extends VectorLayer { static type = LAYER_TYPE.TILED_VECTOR; diff --git a/x-pack/plugins/maps/public/layers/vector_layer.d.ts b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.d.ts similarity index 82% rename from x-pack/plugins/maps/public/layers/vector_layer.d.ts rename to x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.d.ts index 710b95b045e71f..73785d4cc04e01 100644 --- a/x-pack/plugins/maps/public/layers/vector_layer.d.ts +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.d.ts @@ -5,18 +5,18 @@ */ /* eslint-disable @typescript-eslint/consistent-type-definitions */ -import { AbstractLayer } from './layer'; -import { IVectorSource } from './sources/vector_source'; +import { AbstractLayer } from '../layer'; +import { IVectorSource } from '../../sources/vector_source'; import { MapFilters, VectorLayerDescriptor, VectorSourceRequestMeta, -} from '../../common/descriptor_types'; -import { ILayer } from './layer'; -import { IJoin } from './joins/join'; -import { IVectorStyle } from './styles/vector/vector_style'; -import { IField } from './fields/field'; -import { SyncContext } from '../actions/map_actions'; +} from '../../../../common/descriptor_types'; +import { ILayer } from '../layer'; +import { IJoin } from '../../joins/join'; +import { IVectorStyle } from '../../styles/vector/vector_style'; +import { IField } from '../../fields/field'; +import { SyncContext } from '../../../actions/map_actions'; export type VectorLayerArguments = { source: IVectorSource; diff --git a/x-pack/plugins/maps/public/layers/vector_layer.js b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js similarity index 98% rename from x-pack/plugins/maps/public/layers/vector_layer.js rename to x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js index 74ddf11c6beb4c..6c04f7c19ac7db 100644 --- a/x-pack/plugins/maps/public/layers/vector_layer.js +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.js @@ -6,8 +6,8 @@ import turf from 'turf'; import React from 'react'; -import { AbstractLayer } from './layer'; -import { VectorStyle } from './styles/vector/vector_style'; +import { AbstractLayer } from '../layer'; +import { VectorStyle } from '../../styles/vector/vector_style'; import { FEATURE_ID_PROPERTY_NAME, SOURCE_DATA_ID_ORIGIN, @@ -18,23 +18,23 @@ import { LAYER_TYPE, FIELD_ORIGIN, LAYER_STYLE_TYPE, -} from '../../common/constants'; +} from '../../../../common/constants'; import _ from 'lodash'; -import { JoinTooltipProperty } from './tooltips/join_tooltip_property'; +import { JoinTooltipProperty } from '../../tooltips/join_tooltip_property'; import { EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { DataRequestAbortError } from './util/data_request'; +import { DataRequestAbortError } from '../../util/data_request'; import { canSkipSourceUpdate, canSkipStyleMetaUpdate, canSkipFormattersUpdate, -} from './util/can_skip_fetch'; -import { assignFeatureIds } from './util/assign_feature_ids'; +} from '../../util/can_skip_fetch'; +import { assignFeatureIds } from '../../util/assign_feature_ids'; import { getFillFilterExpression, getLineFilterExpression, getPointFilterExpression, -} from './util/mb_filter_expressions'; +} from '../../util/mb_filter_expressions'; export class VectorLayer extends AbstractLayer { static type = LAYER_TYPE.VECTOR; diff --git a/x-pack/plugins/maps/public/layers/vector_tile_layer.js b/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.js similarity index 97% rename from x-pack/plugins/maps/public/layers/vector_tile_layer.js rename to x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.js index fc7812a2c86c70..fe1ff58922162b 100644 --- a/x-pack/plugins/maps/public/layers/vector_tile_layer.js +++ b/x-pack/plugins/maps/public/classes/layers/vector_tile_layer/vector_tile_layer.js @@ -4,14 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TileLayer } from './tile_layer'; +import { TileLayer } from '../tile_layer/tile_layer'; import _ from 'lodash'; -import { SOURCE_DATA_ID_ORIGIN, LAYER_TYPE, LAYER_STYLE_TYPE } from '../../common/constants'; -import { isRetina } from '../meta'; +import { SOURCE_DATA_ID_ORIGIN, LAYER_TYPE, LAYER_STYLE_TYPE } from '../../../../common/constants'; +import { isRetina } from '../../../meta'; import { addSpriteSheetToMapFromImageData, loadSpriteSheetImageData, -} from '../connected_components/map/mb/utils'; //todo move this implementation +} from '../../../connected_components/map/mb/utils'; //todo move this implementation const MB_STYLE_TYPE_TO_OPACITY = { fill: ['fill-opacity'], diff --git a/x-pack/plugins/maps/public/layers/sources/client_file_source/create_client_file_source_editor.js b/x-pack/plugins/maps/public/classes/sources/client_file_source/create_client_file_source_editor.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/client_file_source/create_client_file_source_editor.js rename to x-pack/plugins/maps/public/classes/sources/client_file_source/create_client_file_source_editor.js diff --git a/x-pack/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js b/x-pack/plugins/maps/public/classes/sources/client_file_source/geojson_file_source.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/client_file_source/geojson_file_source.js rename to x-pack/plugins/maps/public/classes/sources/client_file_source/geojson_file_source.js diff --git a/x-pack/plugins/maps/public/layers/sources/client_file_source/index.js b/x-pack/plugins/maps/public/classes/sources/client_file_source/index.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/client_file_source/index.js rename to x-pack/plugins/maps/public/classes/sources/client_file_source/index.js diff --git a/x-pack/plugins/maps/public/layers/sources/client_file_source/upload_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/client_file_source/upload_layer_wizard.tsx similarity index 93% rename from x-pack/plugins/maps/public/layers/sources/client_file_source/upload_layer_wizard.tsx rename to x-pack/plugins/maps/public/classes/sources/client_file_source/upload_layer_wizard.tsx index 2f8aa67d74b52f..d5ee354914e5cf 100644 --- a/x-pack/plugins/maps/public/layers/sources/client_file_source/upload_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/client_file_source/upload_layer_wizard.tsx @@ -13,13 +13,13 @@ import { SCALING_TYPES, } from '../../../../common/constants'; // @ts-ignore -import { ESSearchSource, createDefaultLayerDescriptor } from '../es_search_source'; -import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; +import { createDefaultLayerDescriptor } from '../es_search_source'; +import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; // @ts-ignore import { ClientFileCreateSourceEditor } from './create_client_file_source_editor'; // @ts-ignore import { GeojsonFileSource } from './geojson_file_source'; -import { VectorLayer } from '../../vector_layer'; +import { VectorLayer } from '../../layers/vector_layer/vector_layer'; export const uploadLayerWizardConfig: LayerWizard = { description: i18n.translate('xpack.maps.source.geojsonFileDescription', { diff --git a/x-pack/plugins/maps/public/layers/sources/ems_file_source/create_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/create_source_editor.tsx similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/ems_file_source/create_source_editor.tsx rename to x-pack/plugins/maps/public/classes/sources/ems_file_source/create_source_editor.tsx diff --git a/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_boundaries_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx similarity index 89% rename from x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_boundaries_layer_wizard.tsx rename to x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx index cc7e04a7313ac9..4f1edca75b3086 100644 --- a/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_boundaries_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_boundaries_layer_wizard.tsx @@ -6,8 +6,8 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { VectorLayer } from '../../vector_layer'; -import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; +import { VectorLayer } from '../../layers/vector_layer/vector_layer'; +import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; import { EMSFileCreateSourceEditor } from './create_source_editor'; import { EMSFileSource, sourceTitle } from './ems_file_source'; // @ts-ignore diff --git a/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.test.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.test.tsx similarity index 96% rename from x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.test.tsx rename to x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.test.tsx index 03e3b2a8f49414..24c111a72ac059 100644 --- a/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.test.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.test.tsx @@ -7,7 +7,7 @@ import { EMSFileSource } from './ems_file_source'; jest.mock('ui/new_platform'); -jest.mock('../../vector_layer', () => {}); +jest.mock('../../layers/vector_layer/vector_layer', () => {}); function makeEMSFileSource(tooltipProperties: string[]) { const emsFileSource = new EMSFileSource({ tooltipProperties }); diff --git a/x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.tsx similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/ems_file_source/ems_file_source.tsx rename to x-pack/plugins/maps/public/classes/sources/ems_file_source/ems_file_source.tsx diff --git a/x-pack/plugins/maps/public/layers/sources/ems_file_source/index.ts b/x-pack/plugins/maps/public/classes/sources/ems_file_source/index.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/ems_file_source/index.ts rename to x-pack/plugins/maps/public/classes/sources/ems_file_source/index.ts diff --git a/x-pack/plugins/maps/public/layers/sources/ems_file_source/update_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/ems_file_source/update_source_editor.tsx similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/ems_file_source/update_source_editor.tsx rename to x-pack/plugins/maps/public/classes/sources/ems_file_source/update_source_editor.tsx diff --git a/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx similarity index 87% rename from x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_base_map_layer_wizard.tsx rename to x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx index 391ab5691938db..7a25609c6a5d10 100644 --- a/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_base_map_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_base_map_layer_wizard.tsx @@ -5,11 +5,11 @@ */ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; +import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; // @ts-ignore import { EMSTMSSource, sourceTitle } from './ems_tms_source'; // @ts-ignore -import { VectorTileLayer } from '../../vector_tile_layer'; +import { VectorTileLayer } from '../../layers/vector_tile_layer/vector_tile_layer'; // @ts-ignore import { TileServiceSelect } from './tile_service_select'; import { getIsEmsEnabled } from '../../../kibana_services'; diff --git a/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.js b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_tms_source.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.js rename to x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_tms_source.js diff --git a/x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.test.js b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_tms_source.test.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/ems_tms_source/ems_tms_source.test.js rename to x-pack/plugins/maps/public/classes/sources/ems_tms_source/ems_tms_source.test.js diff --git a/x-pack/plugins/maps/public/layers/sources/ems_tms_source/index.js b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/index.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/ems_tms_source/index.js rename to x-pack/plugins/maps/public/classes/sources/ems_tms_source/index.js diff --git a/x-pack/plugins/maps/public/layers/sources/ems_tms_source/tile_service_select.js b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/tile_service_select.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/ems_tms_source/tile_service_select.js rename to x-pack/plugins/maps/public/classes/sources/ems_tms_source/tile_service_select.js diff --git a/x-pack/plugins/maps/public/layers/sources/ems_tms_source/update_source_editor.js b/x-pack/plugins/maps/public/classes/sources/ems_tms_source/update_source_editor.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/ems_tms_source/update_source_editor.js rename to x-pack/plugins/maps/public/classes/sources/ems_tms_source/update_source_editor.js diff --git a/x-pack/plugins/maps/public/layers/sources/ems_unavailable_message.ts b/x-pack/plugins/maps/public/classes/sources/ems_unavailable_message.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/ems_unavailable_message.ts rename to x-pack/plugins/maps/public/classes/sources/ems_unavailable_message.ts diff --git a/x-pack/plugins/maps/public/layers/sources/es_agg_source/es_agg_source.d.ts b/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.d.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_agg_source/es_agg_source.d.ts rename to x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.d.ts diff --git a/x-pack/plugins/maps/public/layers/sources/es_agg_source/es_agg_source.js b/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_agg_source/es_agg_source.js rename to x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.js diff --git a/x-pack/plugins/maps/public/layers/sources/es_agg_source/es_agg_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.test.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_agg_source/es_agg_source.test.ts rename to x-pack/plugins/maps/public/classes/sources/es_agg_source/es_agg_source.test.ts diff --git a/x-pack/plugins/maps/public/layers/sources/es_agg_source/index.ts b/x-pack/plugins/maps/public/classes/sources/es_agg_source/index.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_agg_source/index.ts rename to x-pack/plugins/maps/public/classes/sources/es_agg_source/index.ts diff --git a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/clusters_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx similarity index 95% rename from x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/clusters_layer_wizard.tsx rename to x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx index f9092e64833f13..4e75ae8823385c 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/clusters_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/clusters_layer_wizard.tsx @@ -10,8 +10,8 @@ import React from 'react'; import { CreateSourceEditor } from './create_source_editor'; // @ts-ignore import { ESGeoGridSource, clustersTitle } from './es_geo_grid_source'; -import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; -import { VectorLayer } from '../../vector_layer'; +import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; +import { VectorLayer } from '../../layers/vector_layer/vector_layer'; import { ESGeoGridSourceDescriptor, ColorDynamicOptions, diff --git a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/convert_to_geojson.js b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/convert_to_geojson.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/convert_to_geojson.js rename to x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/convert_to_geojson.js diff --git a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/convert_to_geojson.test.ts b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/convert_to_geojson.test.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/convert_to_geojson.test.ts rename to x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/convert_to_geojson.test.ts diff --git a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/create_source_editor.js b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/create_source_editor.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/create_source_editor.js rename to x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/create_source_editor.js diff --git a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.d.ts b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.d.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.d.ts rename to x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.d.ts diff --git a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.js rename to x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.js diff --git a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.test.ts b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/es_geo_grid_source.test.ts rename to x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/es_geo_grid_source.test.ts diff --git a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/geo_tile_utils.js b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/geo_tile_utils.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/geo_tile_utils.js rename to x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/geo_tile_utils.js diff --git a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/geo_tile_utils.test.js b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/geo_tile_utils.test.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/geo_tile_utils.test.js rename to x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/geo_tile_utils.test.js diff --git a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/heatmap_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx similarity index 90% rename from x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/heatmap_layer_wizard.tsx rename to x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx index fee1a81a5c63ab..d0e45cb05ca064 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/heatmap_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/heatmap_layer_wizard.tsx @@ -10,9 +10,9 @@ import React from 'react'; import { CreateSourceEditor } from './create_source_editor'; // @ts-ignore import { ESGeoGridSource, heatmapTitle } from './es_geo_grid_source'; -import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; +import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; // @ts-ignore -import { HeatmapLayer } from '../../heatmap_layer'; +import { HeatmapLayer } from '../../layers/heatmap_layer/heatmap_layer'; import { ESGeoGridSourceDescriptor } from '../../../../common/descriptor_types'; import { RENDER_AS } from '../../../../common/constants'; diff --git a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/index.js b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/index.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/index.js rename to x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/index.js diff --git a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/render_as_select.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/render_as_select.tsx similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/render_as_select.tsx rename to x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/render_as_select.tsx diff --git a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/resolution_editor.js b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/resolution_editor.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/resolution_editor.js rename to x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/resolution_editor.js diff --git a/x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/update_source_editor.js b/x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_geo_grid_source/update_source_editor.js rename to x-pack/plugins/maps/public/classes/sources/es_geo_grid_source/update_source_editor.js diff --git a/x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/convert_to_lines.js b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/convert_to_lines.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/convert_to_lines.js rename to x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/convert_to_lines.js diff --git a/x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/convert_to_lines.test.ts b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/convert_to_lines.test.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/convert_to_lines.test.ts rename to x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/convert_to_lines.test.ts diff --git a/x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/create_source_editor.js b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/create_source_editor.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/create_source_editor.js rename to x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/create_source_editor.js diff --git a/x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/es_pew_pew_source.js rename to x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/es_pew_pew_source.js diff --git a/x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/index.js b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/index.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/index.js rename to x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/index.js diff --git a/x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx similarity index 94% rename from x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx rename to x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx index 3ad6d64903d4aa..bda1a6650c48a6 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/point_2_point_layer_wizard.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { getDefaultDynamicProperties } from '../../styles/vector/vector_style_defaults'; -import { VectorLayer } from '../../vector_layer'; +import { VectorLayer } from '../../layers/vector_layer/vector_layer'; // @ts-ignore import { ESPewPewSource, sourceTitle } from './es_pew_pew_source'; import { VectorStyle } from '../../styles/vector/vector_style'; @@ -21,7 +21,7 @@ import { import { COLOR_GRADIENTS } from '../../styles/color_utils'; // @ts-ignore import { CreateSourceEditor } from './create_source_editor'; -import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; +import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; import { ColorDynamicOptions, SizeDynamicOptions } from '../../../../common/descriptor_types'; export const point2PointLayerWizardConfig: LayerWizard = { diff --git a/x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/update_source_editor.js b/x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/update_source_editor.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_pew_pew_source/update_source_editor.js rename to x-pack/plugins/maps/public/classes/sources/es_pew_pew_source/update_source_editor.js diff --git a/x-pack/plugins/maps/public/layers/sources/es_search_source/__snapshots__/scaling_form.test.tsx.snap b/x-pack/plugins/maps/public/classes/sources/es_search_source/__snapshots__/scaling_form.test.tsx.snap similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_search_source/__snapshots__/scaling_form.test.tsx.snap rename to x-pack/plugins/maps/public/classes/sources/es_search_source/__snapshots__/scaling_form.test.tsx.snap diff --git a/x-pack/plugins/maps/public/layers/sources/es_search_source/__snapshots__/update_source_editor.test.js.snap b/x-pack/plugins/maps/public/classes/sources/es_search_source/__snapshots__/update_source_editor.test.js.snap similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_search_source/__snapshots__/update_source_editor.test.js.snap rename to x-pack/plugins/maps/public/classes/sources/es_search_source/__snapshots__/update_source_editor.test.js.snap diff --git a/x-pack/plugins/maps/public/layers/sources/es_search_source/constants.js b/x-pack/plugins/maps/public/classes/sources/es_search_source/constants.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_search_source/constants.js rename to x-pack/plugins/maps/public/classes/sources/es_search_source/constants.js diff --git a/x-pack/plugins/maps/public/layers/sources/es_search_source/create_source_editor.js b/x-pack/plugins/maps/public/classes/sources/es_search_source/create_source_editor.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_search_source/create_source_editor.js rename to x-pack/plugins/maps/public/classes/sources/es_search_source/create_source_editor.js diff --git a/x-pack/plugins/maps/public/layers/sources/es_search_source/es_documents_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx similarity index 85% rename from x-pack/plugins/maps/public/layers/sources/es_search_source/es_documents_layer_wizard.tsx rename to x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx index 4a775dd78f7873..8898735427ccb7 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_search_source/es_documents_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_documents_layer_wizard.tsx @@ -8,11 +8,11 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; // @ts-ignore import { CreateSourceEditor } from './create_source_editor'; -import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; +import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; // @ts-ignore import { ESSearchSource, sourceTitle } from './es_search_source'; -import { BlendedVectorLayer } from '../../blended_vector_layer'; -import { VectorLayer } from '../../vector_layer'; +import { BlendedVectorLayer } from '../../layers/blended_vector_layer/blended_vector_layer'; +import { VectorLayer } from '../../layers/vector_layer/vector_layer'; import { SCALING_TYPES } from '../../../../common/constants'; export function createDefaultLayerDescriptor(sourceConfig: unknown, mapColors: string[]) { diff --git a/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.d.ts b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.d.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.d.ts rename to x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.d.ts diff --git a/x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.js b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_search_source/es_search_source.js rename to x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.js diff --git a/x-pack/plugins/maps/public/layers/sources/es_search_source/index.js b/x-pack/plugins/maps/public/classes/sources/es_search_source/index.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_search_source/index.js rename to x-pack/plugins/maps/public/classes/sources/es_search_source/index.js diff --git a/x-pack/plugins/maps/public/layers/sources/es_search_source/load_index_settings.js b/x-pack/plugins/maps/public/classes/sources/es_search_source/load_index_settings.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_search_source/load_index_settings.js rename to x-pack/plugins/maps/public/classes/sources/es_search_source/load_index_settings.js diff --git a/x-pack/plugins/maps/public/layers/sources/es_search_source/scaling_form.test.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/scaling_form.test.tsx similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_search_source/scaling_form.test.tsx rename to x-pack/plugins/maps/public/classes/sources/es_search_source/scaling_form.test.tsx diff --git a/x-pack/plugins/maps/public/layers/sources/es_search_source/scaling_form.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/scaling_form.tsx similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_search_source/scaling_form.tsx rename to x-pack/plugins/maps/public/classes/sources/es_search_source/scaling_form.tsx diff --git a/x-pack/plugins/maps/public/layers/sources/es_search_source/update_source_editor.js b/x-pack/plugins/maps/public/classes/sources/es_search_source/update_source_editor.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_search_source/update_source_editor.js rename to x-pack/plugins/maps/public/classes/sources/es_search_source/update_source_editor.js diff --git a/x-pack/plugins/maps/public/layers/sources/es_search_source/update_source_editor.test.js b/x-pack/plugins/maps/public/classes/sources/es_search_source/update_source_editor.test.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_search_source/update_source_editor.test.js rename to x-pack/plugins/maps/public/classes/sources/es_search_source/update_source_editor.test.js diff --git a/x-pack/plugins/maps/public/layers/sources/es_source/es_source.d.ts b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.d.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_source/es_source.d.ts rename to x-pack/plugins/maps/public/classes/sources/es_source/es_source.d.ts diff --git a/x-pack/plugins/maps/public/layers/sources/es_source/es_source.js b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_source/es_source.js rename to x-pack/plugins/maps/public/classes/sources/es_source/es_source.js diff --git a/x-pack/plugins/maps/public/layers/sources/es_source/index.ts b/x-pack/plugins/maps/public/classes/sources/es_source/index.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_source/index.ts rename to x-pack/plugins/maps/public/classes/sources/es_source/index.ts diff --git a/x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.d.ts b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.d.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.d.ts rename to x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.d.ts diff --git a/x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.js b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.js rename to x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.js diff --git a/x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.test.js b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.test.js similarity index 98% rename from x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.test.js rename to x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.test.js index 14eb39180a6b8b..f6779206868a59 100644 --- a/x-pack/plugins/maps/public/layers/sources/es_term_source/es_term_source.test.js +++ b/x-pack/plugins/maps/public/classes/sources/es_term_source/es_term_source.test.js @@ -7,7 +7,7 @@ import { ESTermSource, extractPropertiesMap } from './es_term_source'; jest.mock('ui/new_platform'); -jest.mock('../../vector_layer', () => {}); +jest.mock('../../layers/vector_layer/vector_layer', () => {}); const indexPatternTitle = 'myIndex'; const termFieldName = 'myTermField'; diff --git a/x-pack/plugins/maps/public/layers/sources/es_term_source/index.ts b/x-pack/plugins/maps/public/classes/sources/es_term_source/index.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/es_term_source/index.ts rename to x-pack/plugins/maps/public/classes/sources/es_term_source/index.ts diff --git a/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/create_source_editor.js b/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/create_source_editor.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/create_source_editor.js rename to x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/create_source_editor.js diff --git a/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/index.js b/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/index.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/index.js rename to x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/index.js diff --git a/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx similarity index 89% rename from x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx rename to x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx index a9adec2bda2c81..309cb3abd83b25 100644 --- a/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_layer_wizard.tsx @@ -6,10 +6,10 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; +import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; // @ts-ignore import { KibanaRegionmapSource, sourceTitle } from './kibana_regionmap_source'; -import { VectorLayer } from '../../vector_layer'; +import { VectorLayer } from '../../layers/vector_layer/vector_layer'; // @ts-ignore import { CreateSourceEditor } from './create_source_editor'; // @ts-ignore diff --git a/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.d.ts b/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_source.d.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.d.ts rename to x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_source.d.ts diff --git a/x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js b/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_source.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/kibana_regionmap_source/kibana_regionmap_source.js rename to x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_source.js diff --git a/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/create_source_editor.js b/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/create_source_editor.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/create_source_editor.js rename to x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/create_source_editor.js diff --git a/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/index.js b/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/index.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/index.js rename to x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/index.js diff --git a/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx similarity index 89% rename from x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx rename to x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx index 3b4015641ede9a..46513985ed1ab2 100644 --- a/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_base_map_layer_wizard.tsx @@ -6,12 +6,12 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; +import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; // @ts-ignore import { CreateSourceEditor } from './create_source_editor'; // @ts-ignore import { KibanaTilemapSource, sourceTitle } from './kibana_tilemap_source'; -import { TileLayer } from '../../tile_layer'; +import { TileLayer } from '../../layers/tile_layer/tile_layer'; // @ts-ignore import { getKibanaTileMap } from '../../../meta'; diff --git a/x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_tilemap_source.js b/x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_tilemap_source.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/kibana_tilemap_source/kibana_tilemap_source.js rename to x-pack/plugins/maps/public/classes/sources/kibana_tilemap_source/kibana_tilemap_source.js diff --git a/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/index.ts b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/index.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/index.ts rename to x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/index.ts diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx new file mode 100644 index 00000000000000..86f8108d5e23bc --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/layer_wizard.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { + MVTSingleLayerVectorSourceEditor, + MVTSingleLayerVectorSourceConfig, +} from './mvt_single_layer_vector_source_editor'; +import { MVTSingleLayerVectorSource, sourceTitle } from './mvt_single_layer_vector_source'; +import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; +import { TiledVectorLayer } from '../../layers/tiled_vector_layer/tiled_vector_layer'; + +export const mvtVectorSourceWizardConfig: LayerWizard = { + description: i18n.translate('xpack.maps.source.mvtVectorSourceWizard', { + defaultMessage: 'Vector source wizard', + }), + icon: 'grid', + renderWizard: ({ previewLayer, mapColors }: RenderWizardArguments) => { + const onSourceConfigChange = (sourceConfig: MVTSingleLayerVectorSourceConfig) => { + const sourceDescriptor = MVTSingleLayerVectorSource.createDescriptor(sourceConfig); + const layerDescriptor = TiledVectorLayer.createDescriptor({ sourceDescriptor }, mapColors); + previewLayer(layerDescriptor); + }; + + return ; + }, + title: sourceTitle, +}; diff --git a/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts rename to x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.ts diff --git a/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor.tsx similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor.tsx rename to x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source_editor.tsx diff --git a/x-pack/plugins/maps/public/layers/sources/source.ts b/x-pack/plugins/maps/public/classes/sources/source.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/source.ts rename to x-pack/plugins/maps/public/classes/sources/source.ts diff --git a/x-pack/plugins/maps/public/layers/sources/source_registry.ts b/x-pack/plugins/maps/public/classes/sources/source_registry.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/source_registry.ts rename to x-pack/plugins/maps/public/classes/sources/source_registry.ts diff --git a/x-pack/plugins/maps/public/layers/sources/tms_source/index.ts b/x-pack/plugins/maps/public/classes/sources/tms_source/index.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/tms_source/index.ts rename to x-pack/plugins/maps/public/classes/sources/tms_source/index.ts diff --git a/x-pack/plugins/maps/public/layers/sources/tms_source/tms_source.d.ts b/x-pack/plugins/maps/public/classes/sources/tms_source/tms_source.d.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/tms_source/tms_source.d.ts rename to x-pack/plugins/maps/public/classes/sources/tms_source/tms_source.d.ts diff --git a/x-pack/plugins/maps/public/layers/sources/tms_source/tms_source.js b/x-pack/plugins/maps/public/classes/sources/tms_source/tms_source.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/tms_source/tms_source.js rename to x-pack/plugins/maps/public/classes/sources/tms_source/tms_source.js diff --git a/x-pack/plugins/maps/public/layers/sources/vector_feature_types.ts b/x-pack/plugins/maps/public/classes/sources/vector_feature_types.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/vector_feature_types.ts rename to x-pack/plugins/maps/public/classes/sources/vector_feature_types.ts diff --git a/x-pack/plugins/maps/public/layers/sources/vector_source/index.ts b/x-pack/plugins/maps/public/classes/sources/vector_source/index.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/vector_source/index.ts rename to x-pack/plugins/maps/public/classes/sources/vector_source/index.ts diff --git a/x-pack/plugins/maps/public/layers/sources/vector_source/vector_source.d.ts b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/vector_source/vector_source.d.ts rename to x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.d.ts diff --git a/x-pack/plugins/maps/public/layers/sources/vector_source/vector_source.js b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/vector_source/vector_source.js rename to x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.js diff --git a/x-pack/plugins/maps/public/layers/sources/wms_source/index.js b/x-pack/plugins/maps/public/classes/sources/wms_source/index.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/wms_source/index.js rename to x-pack/plugins/maps/public/classes/sources/wms_source/index.js diff --git a/x-pack/plugins/maps/public/layers/sources/wms_source/wms_client.js b/x-pack/plugins/maps/public/classes/sources/wms_source/wms_client.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/wms_source/wms_client.js rename to x-pack/plugins/maps/public/classes/sources/wms_source/wms_client.js diff --git a/x-pack/plugins/maps/public/layers/sources/wms_source/wms_client.test.js b/x-pack/plugins/maps/public/classes/sources/wms_source/wms_client.test.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/wms_source/wms_client.test.js rename to x-pack/plugins/maps/public/classes/sources/wms_source/wms_client.test.js diff --git a/x-pack/plugins/maps/public/layers/sources/wms_source/wms_create_source_editor.js b/x-pack/plugins/maps/public/classes/sources/wms_source/wms_create_source_editor.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/wms_source/wms_create_source_editor.js rename to x-pack/plugins/maps/public/classes/sources/wms_source/wms_create_source_editor.js diff --git a/x-pack/plugins/maps/public/layers/sources/wms_source/wms_layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/wms_source/wms_layer_wizard.tsx similarity index 88% rename from x-pack/plugins/maps/public/layers/sources/wms_source/wms_layer_wizard.tsx rename to x-pack/plugins/maps/public/classes/sources/wms_source/wms_layer_wizard.tsx index fbf5e25c78b170..9261b8866d115f 100644 --- a/x-pack/plugins/maps/public/layers/sources/wms_source/wms_layer_wizard.tsx +++ b/x-pack/plugins/maps/public/classes/sources/wms_source/wms_layer_wizard.tsx @@ -10,8 +10,8 @@ import { i18n } from '@kbn/i18n'; import { WMSCreateSourceEditor } from './wms_create_source_editor'; // @ts-ignore import { sourceTitle, WMSSource } from './wms_source'; -import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; -import { TileLayer } from '../../tile_layer'; +import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; +import { TileLayer } from '../../layers/tile_layer/tile_layer'; export const wmsLayerWizardConfig: LayerWizard = { description: i18n.translate('xpack.maps.source.wmsDescription', { diff --git a/x-pack/plugins/maps/public/layers/sources/wms_source/wms_source.js b/x-pack/plugins/maps/public/classes/sources/wms_source/wms_source.js similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/wms_source/wms_source.js rename to x-pack/plugins/maps/public/classes/sources/wms_source/wms_source.js diff --git a/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/index.ts b/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/index.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/xyz_tms_source/index.ts rename to x-pack/plugins/maps/public/classes/sources/xyz_tms_source/index.ts diff --git a/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx b/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx new file mode 100644 index 00000000000000..574aaa262569f9 --- /dev/null +++ b/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/layer_wizard.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { XYZTMSEditor, XYZTMSSourceConfig } from './xyz_tms_editor'; +import { XYZTMSSource, sourceTitle } from './xyz_tms_source'; +import { LayerWizard, RenderWizardArguments } from '../../layers/layer_wizard_registry'; +import { TileLayer } from '../../layers/tile_layer/tile_layer'; + +export const tmsLayerWizardConfig: LayerWizard = { + description: i18n.translate('xpack.maps.source.ems_xyzDescription', { + defaultMessage: 'Tile map service configured in interface', + }), + icon: 'grid', + renderWizard: ({ previewLayer }: RenderWizardArguments) => { + const onSourceConfigChange = (sourceConfig: XYZTMSSourceConfig) => { + const layerDescriptor = TileLayer.createDescriptor({ + sourceDescriptor: XYZTMSSource.createDescriptor(sourceConfig), + }); + previewLayer(layerDescriptor); + }; + return ; + }, + title: sourceTitle, +}; diff --git a/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_editor.tsx b/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/xyz_tms_editor.tsx similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_editor.tsx rename to x-pack/plugins/maps/public/classes/sources/xyz_tms_source/xyz_tms_editor.tsx diff --git a/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.test.ts b/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/xyz_tms_source.test.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.test.ts rename to x-pack/plugins/maps/public/classes/sources/xyz_tms_source/xyz_tms_source.test.ts diff --git a/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.ts b/x-pack/plugins/maps/public/classes/sources/xyz_tms_source/xyz_tms_source.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/sources/xyz_tms_source/xyz_tms_source.ts rename to x-pack/plugins/maps/public/classes/sources/xyz_tms_source/xyz_tms_source.ts diff --git a/x-pack/plugins/maps/public/layers/styles/_index.scss b/x-pack/plugins/maps/public/classes/styles/_index.scss similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/_index.scss rename to x-pack/plugins/maps/public/classes/styles/_index.scss diff --git a/x-pack/plugins/maps/public/layers/styles/color_utils.js b/x-pack/plugins/maps/public/classes/styles/color_utils.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/color_utils.js rename to x-pack/plugins/maps/public/classes/styles/color_utils.js diff --git a/x-pack/plugins/maps/public/layers/styles/color_utils.test.js b/x-pack/plugins/maps/public/classes/styles/color_utils.test.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/color_utils.test.js rename to x-pack/plugins/maps/public/classes/styles/color_utils.test.js diff --git a/x-pack/plugins/maps/public/layers/styles/components/_color_gradient.scss b/x-pack/plugins/maps/public/classes/styles/components/_color_gradient.scss similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/components/_color_gradient.scss rename to x-pack/plugins/maps/public/classes/styles/components/_color_gradient.scss diff --git a/x-pack/plugins/maps/public/layers/styles/components/color_gradient.js b/x-pack/plugins/maps/public/classes/styles/components/color_gradient.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/components/color_gradient.js rename to x-pack/plugins/maps/public/classes/styles/components/color_gradient.js diff --git a/x-pack/plugins/maps/public/layers/styles/components/ranged_style_legend_row.js b/x-pack/plugins/maps/public/classes/styles/components/ranged_style_legend_row.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/components/ranged_style_legend_row.js rename to x-pack/plugins/maps/public/classes/styles/components/ranged_style_legend_row.js diff --git a/x-pack/plugins/maps/public/layers/styles/heatmap/components/__snapshots__/heatmap_style_editor.test.js.snap b/x-pack/plugins/maps/public/classes/styles/heatmap/components/__snapshots__/heatmap_style_editor.test.js.snap similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/heatmap/components/__snapshots__/heatmap_style_editor.test.js.snap rename to x-pack/plugins/maps/public/classes/styles/heatmap/components/__snapshots__/heatmap_style_editor.test.js.snap diff --git a/x-pack/plugins/maps/public/layers/styles/heatmap/components/heatmap_constants.js b/x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_constants.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/heatmap/components/heatmap_constants.js rename to x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_constants.js diff --git a/x-pack/plugins/maps/public/layers/styles/heatmap/components/heatmap_style_editor.js b/x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/heatmap/components/heatmap_style_editor.js rename to x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.js diff --git a/x-pack/plugins/maps/public/layers/styles/heatmap/components/heatmap_style_editor.test.js b/x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.test.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/heatmap/components/heatmap_style_editor.test.js rename to x-pack/plugins/maps/public/classes/styles/heatmap/components/heatmap_style_editor.test.js diff --git a/x-pack/plugins/maps/public/layers/styles/heatmap/components/legend/heatmap_legend.js b/x-pack/plugins/maps/public/classes/styles/heatmap/components/legend/heatmap_legend.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/heatmap/components/legend/heatmap_legend.js rename to x-pack/plugins/maps/public/classes/styles/heatmap/components/legend/heatmap_legend.js diff --git a/x-pack/plugins/maps/public/layers/styles/heatmap/heatmap_style.js b/x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/heatmap/heatmap_style.js rename to x-pack/plugins/maps/public/classes/styles/heatmap/heatmap_style.js diff --git a/x-pack/plugins/maps/public/layers/styles/style.ts b/x-pack/plugins/maps/public/classes/styles/style.ts similarity index 97% rename from x-pack/plugins/maps/public/layers/styles/style.ts rename to x-pack/plugins/maps/public/classes/styles/style.ts index 38fdc36904412a..7d39acd504c426 100644 --- a/x-pack/plugins/maps/public/layers/styles/style.ts +++ b/x-pack/plugins/maps/public/classes/styles/style.ts @@ -6,7 +6,7 @@ import { ReactElement } from 'react'; import { StyleDescriptor, StyleMetaDescriptor } from '../../../common/descriptor_types'; -import { ILayer } from '../layer'; +import { ILayer } from '../layers/layer'; import { IField } from '../fields/field'; import { DataRequest } from '../util/data_request'; diff --git a/x-pack/plugins/maps/public/layers/styles/tile/tile_style.ts b/x-pack/plugins/maps/public/classes/styles/tile/tile_style.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/tile/tile_style.ts rename to x-pack/plugins/maps/public/classes/styles/tile/tile_style.ts diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/_style_prop_editor.scss b/x-pack/plugins/maps/public/classes/styles/vector/components/_style_prop_editor.scss similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/_style_prop_editor.scss rename to x-pack/plugins/maps/public/classes/styles/vector/components/_style_prop_editor.scss diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/color/_color_stops.scss b/x-pack/plugins/maps/public/classes/styles/vector/components/color/_color_stops.scss similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/color/_color_stops.scss rename to x-pack/plugins/maps/public/classes/styles/vector/components/color/_color_stops.scss diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/color/color_map_select.js b/x-pack/plugins/maps/public/classes/styles/vector/components/color/color_map_select.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/color/color_map_select.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/color/color_map_select.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/color/color_stops.js b/x-pack/plugins/maps/public/classes/styles/vector/components/color/color_stops.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/color/color_stops.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/color/color_stops.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/color/color_stops_categorical.js b/x-pack/plugins/maps/public/classes/styles/vector/components/color/color_stops_categorical.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/color/color_stops_categorical.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/color/color_stops_categorical.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/color/color_stops_ordinal.js b/x-pack/plugins/maps/public/classes/styles/vector/components/color/color_stops_ordinal.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/color/color_stops_ordinal.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/color/color_stops_ordinal.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/color/color_stops_utils.js b/x-pack/plugins/maps/public/classes/styles/vector/components/color/color_stops_utils.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/color/color_stops_utils.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/color/color_stops_utils.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js b/x-pack/plugins/maps/public/classes/styles/vector/components/color/dynamic_color_form.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/color/dynamic_color_form.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/color/dynamic_color_form.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/color/mb_validated_color_picker.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/color/mb_validated_color_picker.tsx similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/color/mb_validated_color_picker.tsx rename to x-pack/plugins/maps/public/classes/styles/vector/components/color/mb_validated_color_picker.tsx diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/color/static_color_form.js b/x-pack/plugins/maps/public/classes/styles/vector/components/color/static_color_form.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/color/static_color_form.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/color/static_color_form.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/color/vector_style_color_editor.js b/x-pack/plugins/maps/public/classes/styles/vector/components/color/vector_style_color_editor.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/color/vector_style_color_editor.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/color/vector_style_color_editor.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/field_meta/categorical_field_meta_popover.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/field_meta/categorical_field_meta_popover.tsx similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/field_meta/categorical_field_meta_popover.tsx rename to x-pack/plugins/maps/public/classes/styles/vector/components/field_meta/categorical_field_meta_popover.tsx diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/field_meta/field_meta_popover.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/field_meta/field_meta_popover.tsx similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/field_meta/field_meta_popover.tsx rename to x-pack/plugins/maps/public/classes/styles/vector/components/field_meta/field_meta_popover.tsx diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/field_meta/ordinal_field_meta_popover.tsx b/x-pack/plugins/maps/public/classes/styles/vector/components/field_meta/ordinal_field_meta_popover.tsx similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/field_meta/ordinal_field_meta_popover.tsx rename to x-pack/plugins/maps/public/classes/styles/vector/components/field_meta/ordinal_field_meta_popover.tsx diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/field_select.js b/x-pack/plugins/maps/public/classes/styles/vector/components/field_select.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/field_select.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/field_select.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/get_vector_style_label.js b/x-pack/plugins/maps/public/classes/styles/vector/components/get_vector_style_label.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/get_vector_style_label.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/get_vector_style_label.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/label/dynamic_label_form.js b/x-pack/plugins/maps/public/classes/styles/vector/components/label/dynamic_label_form.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/label/dynamic_label_form.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/label/dynamic_label_form.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/label/static_label_form.js b/x-pack/plugins/maps/public/classes/styles/vector/components/label/static_label_form.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/label/static_label_form.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/label/static_label_form.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/label/vector_style_label_border_size_editor.js b/x-pack/plugins/maps/public/classes/styles/vector/components/label/vector_style_label_border_size_editor.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/label/vector_style_label_border_size_editor.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/label/vector_style_label_border_size_editor.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/label/vector_style_label_editor.js b/x-pack/plugins/maps/public/classes/styles/vector/components/label/vector_style_label_editor.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/label/vector_style_label_editor.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/label/vector_style_label_editor.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/legend/__snapshots__/vector_icon.test.js.snap b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/__snapshots__/vector_icon.test.js.snap similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/legend/__snapshots__/vector_icon.test.js.snap rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/__snapshots__/vector_icon.test.js.snap diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/legend/category.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/category.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/legend/category.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/category.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/legend/circle_icon.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/circle_icon.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/legend/circle_icon.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/circle_icon.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/legend/extract_color_from_style_property.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/extract_color_from_style_property.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/legend/extract_color_from_style_property.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/extract_color_from_style_property.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/legend/line_icon.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/line_icon.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/legend/line_icon.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/line_icon.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/legend/polygon_icon.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/polygon_icon.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/legend/polygon_icon.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/polygon_icon.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/legend/symbol_icon.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/symbol_icon.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/legend/symbol_icon.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/symbol_icon.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/legend/vector_icon.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/legend/vector_icon.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/legend/vector_icon.test.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.test.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/legend/vector_icon.test.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_icon.test.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/legend/vector_style_legend.js b/x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/legend/vector_style_legend.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/legend/vector_style_legend.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/orientation/dynamic_orientation_form.js b/x-pack/plugins/maps/public/classes/styles/vector/components/orientation/dynamic_orientation_form.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/orientation/dynamic_orientation_form.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/orientation/dynamic_orientation_form.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/orientation/orientation_editor.js b/x-pack/plugins/maps/public/classes/styles/vector/components/orientation/orientation_editor.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/orientation/orientation_editor.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/orientation/orientation_editor.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/orientation/static_orientation_form.js b/x-pack/plugins/maps/public/classes/styles/vector/components/orientation/static_orientation_form.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/orientation/static_orientation_form.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/orientation/static_orientation_form.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/size/dynamic_size_form.js b/x-pack/plugins/maps/public/classes/styles/vector/components/size/dynamic_size_form.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/size/dynamic_size_form.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/size/dynamic_size_form.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/size/size_range_selector.js b/x-pack/plugins/maps/public/classes/styles/vector/components/size/size_range_selector.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/size/size_range_selector.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/size/size_range_selector.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/size/static_size_form.js b/x-pack/plugins/maps/public/classes/styles/vector/components/size/static_size_form.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/size/static_size_form.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/size/static_size_form.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/size/vector_style_size_editor.js b/x-pack/plugins/maps/public/classes/styles/vector/components/size/vector_style_size_editor.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/size/vector_style_size_editor.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/size/vector_style_size_editor.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/stop_input.js b/x-pack/plugins/maps/public/classes/styles/vector/components/stop_input.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/stop_input.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/stop_input.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/style_map_select.js b/x-pack/plugins/maps/public/classes/styles/vector/components/style_map_select.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/style_map_select.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/style_map_select.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/style_option_shapes.js b/x-pack/plugins/maps/public/classes/styles/vector/components/style_option_shapes.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/style_option_shapes.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/style_option_shapes.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/style_prop_editor.js b/x-pack/plugins/maps/public/classes/styles/vector/components/style_prop_editor.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/style_prop_editor.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/style_prop_editor.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/symbol/__snapshots__/icon_select.test.js.snap b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/__snapshots__/icon_select.test.js.snap similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/symbol/__snapshots__/icon_select.test.js.snap rename to x-pack/plugins/maps/public/classes/styles/vector/components/symbol/__snapshots__/icon_select.test.js.snap diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/symbol/_icon_select.scss b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/_icon_select.scss similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/symbol/_icon_select.scss rename to x-pack/plugins/maps/public/classes/styles/vector/components/symbol/_icon_select.scss diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/symbol/dynamic_icon_form.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/dynamic_icon_form.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/symbol/dynamic_icon_form.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/symbol/dynamic_icon_form.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/symbol/icon_map_select.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/symbol/icon_map_select.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_map_select.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/symbol/icon_select.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_select.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/symbol/icon_select.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_select.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/symbol/icon_select.test.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_select.test.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/symbol/icon_select.test.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_select.test.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/symbol/icon_stops.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_stops.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/symbol/icon_stops.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_stops.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/symbol/icon_stops.test.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_stops.test.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/symbol/icon_stops.test.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/symbol/icon_stops.test.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/symbol/static_icon_form.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/static_icon_form.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/symbol/static_icon_form.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/symbol/static_icon_form.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/symbol/vector_style_icon_editor.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/vector_style_icon_editor.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/symbol/vector_style_icon_editor.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/symbol/vector_style_icon_editor.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/symbol/vector_style_symbolize_as_editor.js b/x-pack/plugins/maps/public/classes/styles/vector/components/symbol/vector_style_symbolize_as_editor.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/symbol/vector_style_symbolize_as_editor.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/symbol/vector_style_symbolize_as_editor.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js b/x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/components/vector_style_editor.js rename to x-pack/plugins/maps/public/classes/styles/vector/components/vector_style_editor.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/properties/__snapshots__/dynamic_color_property.test.js.snap b/x-pack/plugins/maps/public/classes/styles/vector/properties/__snapshots__/dynamic_color_property.test.js.snap similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/properties/__snapshots__/dynamic_color_property.test.js.snap rename to x-pack/plugins/maps/public/classes/styles/vector/properties/__snapshots__/dynamic_color_property.test.js.snap diff --git a/x-pack/plugins/maps/public/layers/styles/vector/properties/components/categorical_legend.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/components/categorical_legend.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/properties/components/categorical_legend.js rename to x-pack/plugins/maps/public/classes/styles/vector/properties/components/categorical_legend.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/properties/components/ordinal_legend.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/components/ordinal_legend.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/properties/components/ordinal_legend.js rename to x-pack/plugins/maps/public/classes/styles/vector/properties/components/ordinal_legend.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.js rename to x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.test.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/properties/dynamic_color_property.test.js rename to x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_color_property.test.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/properties/dynamic_icon_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/properties/dynamic_icon_property.js rename to x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_icon_property.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_orientation_property.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/properties/dynamic_orientation_property.js rename to x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_orientation_property.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/properties/dynamic_size_property.js rename to x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_size_property.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.d.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.d.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.d.ts rename to x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.d.ts diff --git a/x-pack/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/properties/dynamic_style_property.js rename to x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_style_property.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/properties/dynamic_text_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/properties/dynamic_text_property.js rename to x-pack/plugins/maps/public/classes/styles/vector/properties/dynamic_text_property.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/properties/label_border_size_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/label_border_size_property.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/properties/label_border_size_property.js rename to x-pack/plugins/maps/public/classes/styles/vector/properties/label_border_size_property.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/properties/static_color_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/static_color_property.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/properties/static_color_property.js rename to x-pack/plugins/maps/public/classes/styles/vector/properties/static_color_property.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/properties/static_icon_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/static_icon_property.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/properties/static_icon_property.js rename to x-pack/plugins/maps/public/classes/styles/vector/properties/static_icon_property.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/properties/static_orientation_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/static_orientation_property.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/properties/static_orientation_property.js rename to x-pack/plugins/maps/public/classes/styles/vector/properties/static_orientation_property.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/properties/static_size_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/static_size_property.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/properties/static_size_property.js rename to x-pack/plugins/maps/public/classes/styles/vector/properties/static_size_property.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/properties/static_style_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/static_style_property.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/properties/static_style_property.js rename to x-pack/plugins/maps/public/classes/styles/vector/properties/static_style_property.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/properties/static_text_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/static_text_property.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/properties/static_text_property.js rename to x-pack/plugins/maps/public/classes/styles/vector/properties/static_text_property.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/properties/style_property.ts b/x-pack/plugins/maps/public/classes/styles/vector/properties/style_property.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/properties/style_property.ts rename to x-pack/plugins/maps/public/classes/styles/vector/properties/style_property.ts diff --git a/x-pack/plugins/maps/public/layers/styles/vector/properties/symbolize_as_property.js b/x-pack/plugins/maps/public/classes/styles/vector/properties/symbolize_as_property.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/properties/symbolize_as_property.js rename to x-pack/plugins/maps/public/classes/styles/vector/properties/symbolize_as_property.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/style_meta.ts b/x-pack/plugins/maps/public/classes/styles/vector/style_meta.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/style_meta.ts rename to x-pack/plugins/maps/public/classes/styles/vector/style_meta.ts diff --git a/x-pack/plugins/maps/public/layers/styles/vector/style_util.js b/x-pack/plugins/maps/public/classes/styles/vector/style_util.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/style_util.js rename to x-pack/plugins/maps/public/classes/styles/vector/style_util.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/style_util.test.js b/x-pack/plugins/maps/public/classes/styles/vector/style_util.test.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/style_util.test.js rename to x-pack/plugins/maps/public/classes/styles/vector/style_util.test.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/symbol_utils.js b/x-pack/plugins/maps/public/classes/styles/vector/symbol_utils.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/symbol_utils.js rename to x-pack/plugins/maps/public/classes/styles/vector/symbol_utils.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/symbol_utils.test.js b/x-pack/plugins/maps/public/classes/styles/vector/symbol_utils.test.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/symbol_utils.test.js rename to x-pack/plugins/maps/public/classes/styles/vector/symbol_utils.test.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/vector_style.d.ts b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.d.ts similarity index 94% rename from x-pack/plugins/maps/public/layers/styles/vector/vector_style.d.ts rename to x-pack/plugins/maps/public/classes/styles/vector/vector_style.d.ts index 762322b8e09f9a..beea9439439944 100644 --- a/x-pack/plugins/maps/public/layers/styles/vector/vector_style.d.ts +++ b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.d.ts @@ -5,7 +5,7 @@ */ import { IStyleProperty } from './properties/style_property'; import { IDynamicStyleProperty } from './properties/dynamic_style_property'; -import { IVectorLayer } from '../../vector_layer'; +import { IVectorLayer } from '../../layers/vector_layer/vector_layer'; import { IVectorSource } from '../../sources/vector_source'; import { AbstractStyle, IStyle } from '../style'; import { diff --git a/x-pack/plugins/maps/public/layers/styles/vector/vector_style.js b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/vector_style.js rename to x-pack/plugins/maps/public/classes/styles/vector/vector_style.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/vector_style.test.js b/x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/vector_style.test.js rename to x-pack/plugins/maps/public/classes/styles/vector/vector_style.test.js diff --git a/x-pack/plugins/maps/public/layers/styles/vector/vector_style_defaults.ts b/x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/styles/vector/vector_style_defaults.ts rename to x-pack/plugins/maps/public/classes/styles/vector/vector_style_defaults.ts diff --git a/x-pack/plugins/maps/public/layers/tooltips/es_agg_tooltip_property.ts b/x-pack/plugins/maps/public/classes/tooltips/es_agg_tooltip_property.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/tooltips/es_agg_tooltip_property.ts rename to x-pack/plugins/maps/public/classes/tooltips/es_agg_tooltip_property.ts diff --git a/x-pack/plugins/maps/public/layers/tooltips/es_tooltip_property.test.ts b/x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.test.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/tooltips/es_tooltip_property.test.ts rename to x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.test.ts diff --git a/x-pack/plugins/maps/public/layers/tooltips/es_tooltip_property.ts b/x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/tooltips/es_tooltip_property.ts rename to x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.ts diff --git a/x-pack/plugins/maps/public/layers/tooltips/join_tooltip_property.ts b/x-pack/plugins/maps/public/classes/tooltips/join_tooltip_property.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/tooltips/join_tooltip_property.ts rename to x-pack/plugins/maps/public/classes/tooltips/join_tooltip_property.ts diff --git a/x-pack/plugins/maps/public/layers/tooltips/tooltip_property.ts b/x-pack/plugins/maps/public/classes/tooltips/tooltip_property.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/tooltips/tooltip_property.ts rename to x-pack/plugins/maps/public/classes/tooltips/tooltip_property.ts diff --git a/x-pack/plugins/maps/public/layers/util/assign_feature_ids.test.ts b/x-pack/plugins/maps/public/classes/util/assign_feature_ids.test.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/util/assign_feature_ids.test.ts rename to x-pack/plugins/maps/public/classes/util/assign_feature_ids.test.ts diff --git a/x-pack/plugins/maps/public/layers/util/assign_feature_ids.ts b/x-pack/plugins/maps/public/classes/util/assign_feature_ids.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/util/assign_feature_ids.ts rename to x-pack/plugins/maps/public/classes/util/assign_feature_ids.ts diff --git a/x-pack/plugins/maps/public/layers/util/can_skip_fetch.test.js b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.test.js similarity index 100% rename from x-pack/plugins/maps/public/layers/util/can_skip_fetch.test.js rename to x-pack/plugins/maps/public/classes/util/can_skip_fetch.test.js diff --git a/x-pack/plugins/maps/public/layers/util/can_skip_fetch.ts b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/util/can_skip_fetch.ts rename to x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts diff --git a/x-pack/plugins/maps/public/layers/util/data_request.ts b/x-pack/plugins/maps/public/classes/util/data_request.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/util/data_request.ts rename to x-pack/plugins/maps/public/classes/util/data_request.ts diff --git a/x-pack/plugins/maps/public/layers/util/es_agg_utils.test.ts b/x-pack/plugins/maps/public/classes/util/es_agg_utils.test.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/util/es_agg_utils.test.ts rename to x-pack/plugins/maps/public/classes/util/es_agg_utils.test.ts diff --git a/x-pack/plugins/maps/public/layers/util/es_agg_utils.ts b/x-pack/plugins/maps/public/classes/util/es_agg_utils.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/util/es_agg_utils.ts rename to x-pack/plugins/maps/public/classes/util/es_agg_utils.ts diff --git a/x-pack/plugins/maps/public/layers/util/is_metric_countable.ts b/x-pack/plugins/maps/public/classes/util/is_metric_countable.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/util/is_metric_countable.ts rename to x-pack/plugins/maps/public/classes/util/is_metric_countable.ts diff --git a/x-pack/plugins/maps/public/layers/util/is_refresh_only_query.ts b/x-pack/plugins/maps/public/classes/util/is_refresh_only_query.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/util/is_refresh_only_query.ts rename to x-pack/plugins/maps/public/classes/util/is_refresh_only_query.ts diff --git a/x-pack/plugins/maps/public/layers/util/mb_filter_expressions.ts b/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts similarity index 100% rename from x-pack/plugins/maps/public/layers/util/mb_filter_expressions.ts rename to x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts diff --git a/x-pack/plugins/maps/public/components/tooltip_selector/tooltip_selector.test.tsx b/x-pack/plugins/maps/public/components/tooltip_selector/tooltip_selector.test.tsx index 10d3f6af633706..f3ac62717519df 100644 --- a/x-pack/plugins/maps/public/components/tooltip_selector/tooltip_selector.test.tsx +++ b/x-pack/plugins/maps/public/components/tooltip_selector/tooltip_selector.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { TooltipSelector } from './tooltip_selector'; -import { AbstractField } from '../../layers/fields/field'; +import { AbstractField } from '../../classes/fields/field'; import { FIELD_ORIGIN } from '../../../common/constants'; class MockField extends AbstractField { diff --git a/x-pack/plugins/maps/public/components/tooltip_selector/tooltip_selector.tsx b/x-pack/plugins/maps/public/components/tooltip_selector/tooltip_selector.tsx index 211276cda904a6..34c58c4c8a183e 100644 --- a/x-pack/plugins/maps/public/components/tooltip_selector/tooltip_selector.tsx +++ b/x-pack/plugins/maps/public/components/tooltip_selector/tooltip_selector.tsx @@ -17,7 +17,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { AddTooltipFieldPopover, FieldProps } from './add_tooltip_field_popover'; -import { IField } from '../../layers/fields/field'; +import { IField } from '../../classes/fields/field'; // TODO import reorder from EUI once its exposed as service // https://github.com/elastic/eui/issues/2372 diff --git a/x-pack/plugins/maps/public/connected_components/gis_map/index.d.ts b/x-pack/plugins/maps/public/connected_components/gis_map/index.d.ts index 7edc51d9d78b30..3f3fa48b3d7692 100644 --- a/x-pack/plugins/maps/public/connected_components/gis_map/index.d.ts +++ b/x-pack/plugins/maps/public/connected_components/gis_map/index.d.ts @@ -7,7 +7,7 @@ import React from 'react'; import { Filter } from 'src/plugins/data/public'; -import { RenderToolTipContent } from '../../layers/tooltips/tooltip_property'; +import { RenderToolTipContent } from '../../classes/tooltips/tooltip_property'; declare const GisMap: React.ComponentType<{ addFilters: ((filters: Filter[]) => void) | null; diff --git a/x-pack/plugins/maps/public/connected_components/gis_map/view.js b/x-pack/plugins/maps/public/connected_components/gis_map/view.js index ca4b062ee7273f..0100db0393d060 100644 --- a/x-pack/plugins/maps/public/connected_components/gis_map/view.js +++ b/x-pack/plugins/maps/public/connected_components/gis_map/view.js @@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n'; import uuid from 'uuid/v4'; import { FLYOUT_STATE } from '../../reducers/ui'; import { MapSettingsPanel } from '../map_settings_panel'; -import { registerLayerWizards } from '../../layers/load_layer_wizards'; +import { registerLayerWizards } from '../../classes/layers/load_layer_wizards'; const RENDER_COMPLETE_EVENT = 'renderComplete'; diff --git a/x-pack/plugins/maps/public/connected_components/layer_addpanel/import_editor/view.js b/x-pack/plugins/maps/public/connected_components/layer_addpanel/import_editor/view.js index 8ebb17ac4fff56..0dca2b8bd002ca 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_addpanel/import_editor/view.js +++ b/x-pack/plugins/maps/public/connected_components/layer_addpanel/import_editor/view.js @@ -7,7 +7,7 @@ import React from 'react'; import { EuiPanel } from '@elastic/eui'; -import { uploadLayerWizardConfig } from '../../../layers/sources/client_file_source'; +import { uploadLayerWizardConfig } from '../../../classes/sources/client_file_source'; export const ImportEditor = props => { const editorProperties = getEditorProperties(props); diff --git a/x-pack/plugins/maps/public/connected_components/layer_addpanel/layer_wizard_select.tsx b/x-pack/plugins/maps/public/connected_components/layer_addpanel/layer_wizard_select.tsx index 0359ed2c6269d9..80afe5c8f8a36e 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_addpanel/layer_wizard_select.tsx +++ b/x-pack/plugins/maps/public/connected_components/layer_addpanel/layer_wizard_select.tsx @@ -7,7 +7,7 @@ import _ from 'lodash'; import React, { Component, Fragment } from 'react'; import { EuiSpacer, EuiCard, EuiIcon } from '@elastic/eui'; -import { getLayerWizards, LayerWizard } from '../../layers/layer_wizard_registry'; +import { getLayerWizards, LayerWizard } from '../../classes/layers/layer_wizard_registry'; interface Props { onSelect: (layerWizard: LayerWizard) => void; diff --git a/x-pack/plugins/maps/public/connected_components/map_settings_panel/spatial_filters_panel.tsx b/x-pack/plugins/maps/public/connected_components/map_settings_panel/spatial_filters_panel.tsx index cae703e982966d..e9ed740873e1a8 100644 --- a/x-pack/plugins/maps/public/connected_components/map_settings_panel/spatial_filters_panel.tsx +++ b/x-pack/plugins/maps/public/connected_components/map_settings_panel/spatial_filters_panel.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { MapSettings } from '../../reducers/map'; import { AlphaSlider } from '../../components/alpha_slider'; -import { MbValidatedColorPicker } from '../../layers/styles/vector/components/color/mb_validated_color_picker'; +import { MbValidatedColorPicker } from '../../classes/styles/vector/components/color/mb_validated_color_picker'; interface Props { settings: MapSettings; diff --git a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/fit_to_data.tsx b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/fit_to_data.tsx index 0b168badb2f3ff..ca75060c4f8dfe 100644 --- a/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/fit_to_data.tsx +++ b/x-pack/plugins/maps/public/connected_components/toolbar_overlay/fit_to_data/fit_to_data.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { ILayer } from '../../../layers/layer'; +import { ILayer } from '../../../classes/layers/layer'; interface Props { layerList: ILayer[]; diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.test.tsx b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.test.tsx index b873119fd7d139..5eaba5330a3a73 100644 --- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.test.tsx +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.test.tsx @@ -7,9 +7,9 @@ import React from 'react'; import { shallowWithIntl } from 'test_utils/enzyme_helpers'; -import { AbstractLayer, ILayer } from '../../../../../../layers/layer'; -import { AbstractSource, ISource } from '../../../../../../layers/sources/source'; -import { AbstractStyle, IStyle } from '../../../../../../layers/styles/style'; +import { AbstractLayer, ILayer } from '../../../../../../classes/layers/layer'; +import { AbstractSource, ISource } from '../../../../../../classes/sources/source'; +import { AbstractStyle, IStyle } from '../../../../../../classes/styles/style'; import { TOCEntryActionsPopover } from './toc_entry_actions_popover'; diff --git a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx index dfc93c29263ee8..344e96e511f2e0 100644 --- a/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx +++ b/x-pack/plugins/maps/public/connected_components/widget_overlay/layer_control/layer_toc/toc_entry/toc_entry_actions_popover/toc_entry_actions_popover.tsx @@ -8,7 +8,7 @@ import React, { Component, Fragment } from 'react'; import { EuiButtonEmpty, EuiPopover, EuiContextMenu, EuiIcon, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { ILayer } from '../../../../../../layers/layer'; +import { ILayer } from '../../../../../../classes/layers/layer'; interface Props { cloneLayer: (layerId: string) => void; diff --git a/x-pack/plugins/maps/public/elasticsearch_geo_utils.js b/x-pack/plugins/maps/public/elasticsearch_geo_utils.js index 888fce7e7afe00..419b169138ef12 100644 --- a/x-pack/plugins/maps/public/elasticsearch_geo_utils.js +++ b/x-pack/plugins/maps/public/elasticsearch_geo_utils.js @@ -225,41 +225,62 @@ export function geoShapeToGeometry(value, accumulator) { accumulator.push(geoJson); } -function createGeoBoundBoxFilter(geometry, geoFieldName, filterProps = {}) { - ensureGeometryType(geometry.type, [GEO_JSON_TYPE.POLYGON]); +function createGeoBoundBoxFilter({ maxLat, maxLon, minLat, minLon }, geoFieldName) { + const top = clampToLatBounds(maxLat); + const bottom = clampToLatBounds(minLat); + + // geo_bounding_box does not support ranges outside of -180 and 180 + // When the area crosses the 180° meridian, + // the value of the lower left longitude will be greater than the value of the upper right longitude. + // http://docs.opengeospatial.org/is/12-063r5/12-063r5.html#30 + let boundingBox; + if (maxLon - minLon >= 360) { + boundingBox = { + top_left: [-180, top], + bottom_right: [180, bottom], + }; + } else if (maxLon > 180) { + const overflow = maxLon - 180; + boundingBox = { + top_left: [minLon, top], + bottom_right: [-180 + overflow, bottom], + }; + } else if (minLon < -180) { + const overflow = Math.abs(minLon) - 180; + boundingBox = { + top_left: [180 - overflow, top], + bottom_right: [maxLon, bottom], + }; + } else { + boundingBox = { + top_left: [minLon, top], + bottom_right: [maxLon, bottom], + }; + } - const TOP_LEFT_INDEX = 0; - const BOTTOM_RIGHT_INDEX = 2; - const verticies = geometry.coordinates[POLYGON_COORDINATES_EXTERIOR_INDEX]; return { geo_bounding_box: { - [geoFieldName]: { - top_left: verticies[TOP_LEFT_INDEX], - bottom_right: verticies[BOTTOM_RIGHT_INDEX], - }, + [geoFieldName]: boundingBox, }, - ...filterProps, }; } export function createExtentFilter(mapExtent, geoFieldName, geoFieldType) { ensureGeoField(geoFieldType); - const safePolygon = convertMapExtentToPolygon(mapExtent); - // Extent filters are used to dynamically filter data for the current map view port. // Continue to use geo_bounding_box queries for extent filters // 1) geo_bounding_box queries are faster than polygon queries // 2) geo_shape benefits of pre-indexed shapes and // compatability across multi-indices with geo_point and geo_shape do not apply to this use case. if (geoFieldType === ES_GEO_FIELD_TYPE.GEO_POINT) { - return createGeoBoundBoxFilter(safePolygon, geoFieldName); + return createGeoBoundBoxFilter(mapExtent, geoFieldName); } return { geo_shape: { [geoFieldName]: { - shape: safePolygon, + shape: formatEnvelopeAsPolygon(mapExtent), relation: ES_SPATIAL_RELATIONS.INTERSECTS, }, }, @@ -376,16 +397,16 @@ export function getBoundingBoxGeometry(geometry) { extent.maxLat = Math.max(exterior[i][LAT_INDEX], extent.maxLat); } - return convertMapExtentToPolygon(extent); + return formatEnvelopeAsPolygon(extent); } -function formatEnvelopeAsPolygon({ maxLat, maxLon, minLat, minLon }) { +export function formatEnvelopeAsPolygon({ maxLat, maxLon, minLat, minLon }) { // GeoJSON mandates that the outer polygon must be counterclockwise to avoid ambiguous polygons // when the shape crosses the dateline const left = minLon; const right = maxLon; - const top = maxLat > 90 ? 90 : maxLat; - const bottom = minLat < -90 ? -90 : minLat; + const top = clampToLatBounds(maxLat); + const bottom = clampToLatBounds(minLat); const topLeft = [left, top]; const bottomLeft = [left, bottom]; const bottomRight = [right, bottom]; @@ -396,45 +417,6 @@ function formatEnvelopeAsPolygon({ maxLat, maxLon, minLat, minLon }) { }; } -/* - * Convert map bounds to polygon - */ -export function convertMapExtentToPolygon({ maxLat, maxLon, minLat, minLon }) { - const lonDelta = maxLon - minLon; - if (lonDelta >= 360) { - return formatEnvelopeAsPolygon({ - maxLat, - maxLon: 180, - minLat, - minLon: -180, - }); - } - - if (maxLon > 180) { - // bounds cross dateline east to west - const overlapWestOfDateLine = maxLon - 180; - return formatEnvelopeAsPolygon({ - maxLat, - maxLon: -180 + overlapWestOfDateLine, - minLat, - minLon, - }); - } - - if (minLon < -180) { - // bounds cross dateline west to east - const overlapEastOfDateLine = Math.abs(minLon) - 180; - return formatEnvelopeAsPolygon({ - maxLat, - maxLon, - minLat, - minLon: 180 - overlapEastOfDateLine, - }); - } - - return formatEnvelopeAsPolygon({ maxLat, maxLon, minLat, minLon }); -} - export function clampToLatBounds(lat) { return clamp(lat, -89, 89); } diff --git a/x-pack/plugins/maps/public/elasticsearch_geo_utils.test.js b/x-pack/plugins/maps/public/elasticsearch_geo_utils.test.js index d13291a8e2ba59..c0baf5e4bcb6e8 100644 --- a/x-pack/plugins/maps/public/elasticsearch_geo_utils.test.js +++ b/x-pack/plugins/maps/public/elasticsearch_geo_utils.test.js @@ -17,19 +17,12 @@ import { geoPointToGeometry, geoShapeToGeometry, createExtentFilter, - convertMapExtentToPolygon, roundCoordinates, extractFeaturesFromFilters, } from './elasticsearch_geo_utils'; import { indexPatterns } from '../../../../src/plugins/data/public'; const geoFieldName = 'location'; -const mapExtent = { - maxLat: 39, - maxLon: -83, - minLat: 35, - minLon: -89, -}; const flattenHitMock = hit => { const properties = {}; @@ -317,174 +310,143 @@ describe('geoShapeToGeometry', () => { }); describe('createExtentFilter', () => { - it('should return elasticsearch geo_bounding_box filter for geo_point field', () => { - const filter = createExtentFilter(mapExtent, geoFieldName, 'geo_point'); - expect(filter).toEqual({ - geo_bounding_box: { - location: { - bottom_right: [-83, 35], - top_left: [-89, 39], + describe('geo_point field', () => { + it('should return elasticsearch geo_bounding_box filter for geo_point field', () => { + const mapExtent = { + maxLat: 39, + maxLon: -83, + minLat: 35, + minLon: -89, + }; + const filter = createExtentFilter(mapExtent, geoFieldName, 'geo_point'); + expect(filter).toEqual({ + geo_bounding_box: { + location: { + top_left: [-89, 39], + bottom_right: [-83, 35], + }, }, - }, + }); }); - }); - it('should return elasticsearch geo_shape filter for geo_shape field', () => { - const filter = createExtentFilter(mapExtent, geoFieldName, 'geo_shape'); - expect(filter).toEqual({ - geo_shape: { - location: { - relation: 'INTERSECTS', - shape: { - coordinates: [ - [ - [-89, 39], - [-89, 35], - [-83, 35], - [-83, 39], - [-89, 39], - ], - ], - type: 'Polygon', + it('should clamp longitudes to -180 to 180 and latitudes to -90 to 90', () => { + const mapExtent = { + maxLat: 120, + maxLon: 200, + minLat: -100, + minLon: -190, + }; + const filter = createExtentFilter(mapExtent, geoFieldName, 'geo_point'); + expect(filter).toEqual({ + geo_bounding_box: { + location: { + top_left: [-180, 89], + bottom_right: [180, -89], }, }, - }, + }); }); - }); - it('should clamp longitudes to -180 to 180', () => { - const mapExtent = { - maxLat: 39, - maxLon: 209, - minLat: 35, - minLon: -191, - }; - const filter = createExtentFilter(mapExtent, geoFieldName, 'geo_shape'); - expect(filter).toEqual({ - geo_shape: { - location: { - relation: 'INTERSECTS', - shape: { - coordinates: [ - [ - [-180, 39], - [-180, 35], - [180, 35], - [180, 39], - [-180, 39], - ], - ], - type: 'Polygon', + it('should make left longitude greater then right longitude when area crosses 180 meridian east to west', () => { + const mapExtent = { + maxLat: 39, + maxLon: 200, + minLat: 35, + minLon: 100, + }; + const filter = createExtentFilter(mapExtent, geoFieldName, 'geo_point'); + const leftLon = filter.geo_bounding_box.location.top_left[0]; + const rightLon = filter.geo_bounding_box.location.bottom_right[0]; + expect(leftLon).toBeGreaterThan(rightLon); + expect(filter).toEqual({ + geo_bounding_box: { + location: { + top_left: [100, 39], + bottom_right: [-160, 35], }, }, - }, - }); - }); -}); - -describe('convertMapExtentToPolygon', () => { - it('should convert bounds to envelope', () => { - const bounds = { - maxLat: 10, - maxLon: 100, - minLat: -10, - minLon: 90, - }; - expect(convertMapExtentToPolygon(bounds)).toEqual({ - type: 'Polygon', - coordinates: [ - [ - [90, 10], - [90, -10], - [100, -10], - [100, 10], - [90, 10], - ], - ], - }); - }); - - it('should clamp longitudes to -180 to 180', () => { - const bounds = { - maxLat: 10, - maxLon: 200, - minLat: -10, - minLon: -400, - }; - expect(convertMapExtentToPolygon(bounds)).toEqual({ - type: 'Polygon', - coordinates: [ - [ - [-180, 10], - [-180, -10], - [180, -10], - [180, 10], - [-180, 10], - ], - ], + }); }); - }); - it('should clamp longitudes to -180 to 180 when bounds span entire globe (360)', () => { - const bounds = { - maxLat: 10, - maxLon: 170, - minLat: -10, - minLon: -400, - }; - expect(convertMapExtentToPolygon(bounds)).toEqual({ - type: 'Polygon', - coordinates: [ - [ - [-180, 10], - [-180, -10], - [180, -10], - [180, 10], - [-180, 10], - ], - ], + it('should make left longitude greater then right longitude when area crosses 180 meridian west to east', () => { + const mapExtent = { + maxLat: 39, + maxLon: -100, + minLat: 35, + minLon: -200, + }; + const filter = createExtentFilter(mapExtent, geoFieldName, 'geo_point'); + const leftLon = filter.geo_bounding_box.location.top_left[0]; + const rightLon = filter.geo_bounding_box.location.bottom_right[0]; + expect(leftLon).toBeGreaterThan(rightLon); + expect(filter).toEqual({ + geo_bounding_box: { + location: { + top_left: [160, 39], + bottom_right: [-100, 35], + }, + }, + }); }); }); - it('should handle bounds that cross dateline(east to west)', () => { - const bounds = { - maxLat: 10, - maxLon: 190, - minLat: -10, - minLon: 170, - }; - expect(convertMapExtentToPolygon(bounds)).toEqual({ - type: 'Polygon', - coordinates: [ - [ - [170, 10], - [170, -10], - [-170, -10], - [-170, 10], - [170, 10], - ], - ], + describe('geo_shape field', () => { + it('should return elasticsearch geo_shape filter', () => { + const mapExtent = { + maxLat: 39, + maxLon: -83, + minLat: 35, + minLon: -89, + }; + const filter = createExtentFilter(mapExtent, geoFieldName, 'geo_shape'); + expect(filter).toEqual({ + geo_shape: { + location: { + relation: 'INTERSECTS', + shape: { + coordinates: [ + [ + [-89, 39], + [-89, 35], + [-83, 35], + [-83, 39], + [-89, 39], + ], + ], + type: 'Polygon', + }, + }, + }, + }); }); - }); - it('should handle bounds that cross dateline(west to east)', () => { - const bounds = { - maxLat: 10, - maxLon: -170, - minLat: -10, - minLon: -190, - }; - expect(convertMapExtentToPolygon(bounds)).toEqual({ - type: 'Polygon', - coordinates: [ - [ - [170, 10], - [170, -10], - [-170, -10], - [-170, 10], - [170, 10], - ], - ], + it('should not clamp longitudes to -180 to 180', () => { + const mapExtent = { + maxLat: 39, + maxLon: 209, + minLat: 35, + minLon: -191, + }; + const filter = createExtentFilter(mapExtent, geoFieldName, 'geo_shape'); + expect(filter).toEqual({ + geo_shape: { + location: { + relation: 'INTERSECTS', + shape: { + coordinates: [ + [ + [-191, 39], + [-191, 35], + [209, 35], + [209, 39], + [-191, 39], + ], + ], + type: 'Polygon', + }, + }, + }, + }); }); }); }); diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx index c3937ba4cdcbb9..fa255cc73a2100 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable.tsx @@ -51,7 +51,7 @@ import { } from '../reducers/non_serializable_instances'; import { getMapCenter, getMapZoom, getHiddenLayerIds } from '../selectors/map_selectors'; import { MAP_SAVED_OBJECT_TYPE } from '../../common/constants'; -import { RenderToolTipContent } from '../layers/tooltips/tooltip_property'; +import { RenderToolTipContent } from '../classes/tooltips/tooltip_property'; import { getUiActions, getCoreI18n } from '../kibana_services'; import { MapEmbeddableInput, MapEmbeddableConfig } from './types'; diff --git a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts index 7e3a8387bed116..f33885c2a24620 100644 --- a/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts +++ b/x-pack/plugins/maps/public/embeddable/map_embeddable_factory.ts @@ -18,7 +18,7 @@ import { createMapPath, MAP_SAVED_OBJECT_TYPE, APP_ICON } from '../../common/con import { MapStore, MapStoreState } from '../reducers/store'; import { MapEmbeddableConfig, MapEmbeddableInput } from './types'; import { MapEmbeddableOutput } from './map_embeddable'; -import { RenderToolTipContent } from '../layers/tooltips/tooltip_property'; +import { RenderToolTipContent } from '../classes/tooltips/tooltip_property'; import { EventHandlers } from '../reducers/non_serializable_instances'; let whenModulesLoadedPromise: Promise; diff --git a/x-pack/plugins/maps/public/index.scss b/x-pack/plugins/maps/public/index.scss index 8b2f6d3cb61563..fe974fa610c03e 100644 --- a/x-pack/plugins/maps/public/index.scss +++ b/x-pack/plugins/maps/public/index.scss @@ -14,4 +14,4 @@ @import 'mapbox_hacks'; @import 'connected_components/index'; @import 'components/index'; -@import 'layers/index'; +@import 'classes/index'; diff --git a/x-pack/plugins/maps/public/layers/load_layer_wizards.ts b/x-pack/plugins/maps/public/layers/load_layer_wizards.ts deleted file mode 100644 index 098ff51791d794..00000000000000 --- a/x-pack/plugins/maps/public/layers/load_layer_wizards.ts +++ /dev/null @@ -1,68 +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 { registerLayerWizard } from './layer_wizard_registry'; -// @ts-ignore -import { uploadLayerWizardConfig } from './sources/client_file_source'; -// @ts-ignore -import { esDocumentsLayerWizardConfig } from './sources/es_search_source'; -// @ts-ignore -import { clustersLayerWizardConfig, heatmapLayerWizardConfig } from './sources/es_geo_grid_source'; -// @ts-ignore -import { point2PointLayerWizardConfig } from './sources/es_pew_pew_source'; -// @ts-ignore -import { emsBoundariesLayerWizardConfig } from './sources/ems_file_source'; -// @ts-ignore -import { emsBaseMapLayerWizardConfig } from './sources/ems_tms_source'; -// @ts-ignore -import { kibanaRegionMapLayerWizardConfig } from './sources/kibana_regionmap_source'; -// @ts-ignore -import { kibanaBasemapLayerWizardConfig } from './sources/kibana_tilemap_source'; -import { tmsLayerWizardConfig } from './sources/xyz_tms_source'; -// @ts-ignore -import { wmsLayerWizardConfig } from './sources/wms_source'; -import { mvtVectorSourceWizardConfig } from './sources/mvt_single_layer_vector_source'; -import { ObservabilityLayerWizardConfig } from './solution_layers/observability'; -import { getInjectedVarFunc } from '../kibana_services'; - -let registered = false; -export function registerLayerWizards() { - if (registered) { - return; - } - - // Registration order determines display order - // @ts-ignore - registerLayerWizard(uploadLayerWizardConfig); - registerLayerWizard(ObservabilityLayerWizardConfig); - // @ts-ignore - registerLayerWizard(esDocumentsLayerWizardConfig); - // @ts-ignore - registerLayerWizard(clustersLayerWizardConfig); - // @ts-ignore - registerLayerWizard(heatmapLayerWizardConfig); - // @ts-ignore - registerLayerWizard(point2PointLayerWizardConfig); - // @ts-ignore - registerLayerWizard(emsBoundariesLayerWizardConfig); - // @ts-ignore - registerLayerWizard(emsBaseMapLayerWizardConfig); - // @ts-ignore - registerLayerWizard(kibanaRegionMapLayerWizardConfig); - // @ts-ignore - registerLayerWizard(kibanaBasemapLayerWizardConfig); - registerLayerWizard(tmsLayerWizardConfig); - // @ts-ignore - registerLayerWizard(wmsLayerWizardConfig); - - const getInjectedVar = getInjectedVarFunc(); - if (getInjectedVar && getInjectedVar('enableVectorTiles', false)) { - // eslint-disable-next-line no-console - console.warn('Vector tiles are an experimental feature and should not be used in production.'); - registerLayerWizard(mvtVectorSourceWizardConfig); - } - registered = true; -} diff --git a/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/layer_wizard.tsx deleted file mode 100644 index c94fec3deac67e..00000000000000 --- a/x-pack/plugins/maps/public/layers/sources/mvt_single_layer_vector_source/layer_wizard.tsx +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { - MVTSingleLayerVectorSourceEditor, - MVTSingleLayerVectorSourceConfig, -} from './mvt_single_layer_vector_source_editor'; -import { MVTSingleLayerVectorSource, sourceTitle } from './mvt_single_layer_vector_source'; -import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; -import { TiledVectorLayer } from '../../tiled_vector_layer'; - -export const mvtVectorSourceWizardConfig: LayerWizard = { - description: i18n.translate('xpack.maps.source.mvtVectorSourceWizard', { - defaultMessage: 'Vector source wizard', - }), - icon: 'grid', - renderWizard: ({ previewLayer, mapColors }: RenderWizardArguments) => { - const onSourceConfigChange = (sourceConfig: MVTSingleLayerVectorSourceConfig) => { - const sourceDescriptor = MVTSingleLayerVectorSource.createDescriptor(sourceConfig); - const layerDescriptor = TiledVectorLayer.createDescriptor({ sourceDescriptor }, mapColors); - previewLayer(layerDescriptor); - }; - - return ; - }, - title: sourceTitle, -}; diff --git a/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/layer_wizard.tsx b/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/layer_wizard.tsx deleted file mode 100644 index e970c75fa7adf0..00000000000000 --- a/x-pack/plugins/maps/public/layers/sources/xyz_tms_source/layer_wizard.tsx +++ /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 { i18n } from '@kbn/i18n'; -import React from 'react'; -import { XYZTMSEditor, XYZTMSSourceConfig } from './xyz_tms_editor'; -import { XYZTMSSource, sourceTitle } from './xyz_tms_source'; -import { LayerWizard, RenderWizardArguments } from '../../layer_wizard_registry'; -import { TileLayer } from '../../tile_layer'; - -export const tmsLayerWizardConfig: LayerWizard = { - description: i18n.translate('xpack.maps.source.ems_xyzDescription', { - defaultMessage: 'Tile map service configured in interface', - }), - icon: 'grid', - renderWizard: ({ previewLayer }: RenderWizardArguments) => { - const onSourceConfigChange = (sourceConfig: XYZTMSSourceConfig) => { - const layerDescriptor = TileLayer.createDescriptor({ - sourceDescriptor: XYZTMSSource.createDescriptor(sourceConfig), - }); - previewLayer(layerDescriptor); - }; - return ; - }, - title: sourceTitle, -}; diff --git a/x-pack/plugins/maps/public/layers/tile_layer.d.ts b/x-pack/plugins/maps/public/layers/tile_layer.d.ts deleted file mode 100644 index 8a1ef0f172717d..00000000000000 --- a/x-pack/plugins/maps/public/layers/tile_layer.d.ts +++ /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 { AbstractLayer } from './layer'; -import { ITMSSource } from './sources/tms_source'; -import { LayerDescriptor } from '../../common/descriptor_types'; - -interface ITileLayerArguments { - source: ITMSSource; - layerDescriptor: LayerDescriptor; -} - -export class TileLayer extends AbstractLayer { - constructor(args: ITileLayerArguments); -} diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.d.ts b/x-pack/plugins/maps/public/selectors/map_selectors.d.ts index 9caa151db6d5a4..77b5293dfaa095 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.d.ts +++ b/x-pack/plugins/maps/public/selectors/map_selectors.d.ts @@ -7,8 +7,8 @@ import { MapCenter } from '../../common/descriptor_types'; import { MapStoreState } from '../reducers/store'; import { MapSettings } from '../reducers/map'; -import { IVectorLayer } from '../layers/vector_layer'; -import { ILayer } from '../layers/layer'; +import { IVectorLayer } from '../classes/layers/vector_layer/vector_layer'; +import { ILayer } from '../classes/layers/layer'; export function getHiddenLayerIds(state: MapStoreState): string[]; diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.js b/x-pack/plugins/maps/public/selectors/map_selectors.js index 38a862973623a2..c2933dc3052cc7 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.js +++ b/x-pack/plugins/maps/public/selectors/map_selectors.js @@ -6,18 +6,18 @@ import { createSelector } from 'reselect'; import _ from 'lodash'; -import { TileLayer } from '../layers/tile_layer'; -import { VectorTileLayer } from '../layers/vector_tile_layer'; -import { VectorLayer } from '../layers/vector_layer'; -import { HeatmapLayer } from '../layers/heatmap_layer'; -import { BlendedVectorLayer } from '../layers/blended_vector_layer'; +import { TileLayer } from '../classes/layers/tile_layer/tile_layer'; +import { VectorTileLayer } from '../classes/layers/vector_tile_layer/vector_tile_layer'; +import { VectorLayer } from '../classes/layers/vector_layer/vector_layer'; +import { HeatmapLayer } from '../classes/layers/heatmap_layer/heatmap_layer'; +import { BlendedVectorLayer } from '../classes/layers/blended_vector_layer/blended_vector_layer'; import { getTimeFilter } from '../kibana_services'; import { getInspectorAdapters } from '../reducers/non_serializable_instances'; -import { TiledVectorLayer } from '../layers/tiled_vector_layer'; +import { TiledVectorLayer } from '../classes/layers/tiled_vector_layer/tiled_vector_layer'; import { copyPersistentState, TRACKED_LAYER_DESCRIPTOR } from '../reducers/util'; -import { InnerJoin } from '../layers/joins/inner_join'; -import { getSourceByType } from '../layers/sources/source_registry'; -import { GeojsonFileSource } from '../layers/sources/client_file_source'; +import { InnerJoin } from '../classes/joins/inner_join'; +import { getSourceByType } from '../classes/sources/source_registry'; +import { GeojsonFileSource } from '../classes/sources/client_file_source'; import { LAYER_TYPE, SOURCE_DATA_ID_ORIGIN, diff --git a/x-pack/plugins/maps/public/selectors/map_selectors.test.js b/x-pack/plugins/maps/public/selectors/map_selectors.test.js index fec16251914ead..b6b192ecd9bca5 100644 --- a/x-pack/plugins/maps/public/selectors/map_selectors.test.js +++ b/x-pack/plugins/maps/public/selectors/map_selectors.test.js @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('../layers/vector_layer', () => {}); -jest.mock('../layers/tiled_vector_layer', () => {}); -jest.mock('../layers/blended_vector_layer', () => {}); -jest.mock('../layers/heatmap_layer', () => {}); -jest.mock('../layers/vector_tile_layer', () => {}); -jest.mock('../layers/joins/inner_join', () => {}); +jest.mock('../classes/layers/vector_layer/vector_layer', () => {}); +jest.mock('../classes/layers/tiled_vector_layer/tiled_vector_layer', () => {}); +jest.mock('../classes/layers/blended_vector_layer/blended_vector_layer', () => {}); +jest.mock('../classes/layers/heatmap_layer/heatmap_layer', () => {}); +jest.mock('../classes/layers/vector_tile_layer/vector_tile_layer', () => {}); +jest.mock('../classes/joins/inner_join', () => {}); jest.mock('../reducers/non_serializable_instances', () => ({ getInspectorAdapters: () => { return {}; diff --git a/x-pack/plugins/maps/server/index.ts b/x-pack/plugins/maps/server/index.ts new file mode 100644 index 00000000000000..1b714a3555b345 --- /dev/null +++ b/x-pack/plugins/maps/server/index.ts @@ -0,0 +1,10 @@ +/* + * 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 { PluginInitializerContext } from 'src/core/server'; +import { MapsPlugin } from './plugin'; + +export const plugin = (initializerContext: PluginInitializerContext) => + new MapsPlugin(initializerContext); diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts new file mode 100644 index 00000000000000..7de0a69fb00796 --- /dev/null +++ b/x-pack/plugins/maps/server/plugin.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { i18n } from '@kbn/i18n'; +import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/server'; +import { PluginSetupContract as FeaturesPluginSetupContract } from '../../features/server'; + +import { APP_ID, APP_ICON, MAP_SAVED_OBJECT_TYPE } from '../common/constants'; + +interface SetupDeps { + features: FeaturesPluginSetupContract; +} + +export class MapsPlugin implements Plugin { + constructor(initializerContext: PluginInitializerContext) {} + setup(core: CoreSetup, plugins: SetupDeps) { + plugins.features.registerFeature({ + id: APP_ID, + name: i18n.translate('xpack.maps.featureRegistry.mapsFeatureName', { + defaultMessage: 'Maps', + }), + order: 600, + icon: APP_ICON, + navLinkId: APP_ID, + app: [APP_ID, 'kibana'], + catalogue: [APP_ID], + privileges: { + all: { + app: [APP_ID, 'kibana'], + catalogue: [APP_ID], + savedObject: { + all: [MAP_SAVED_OBJECT_TYPE, 'query'], + read: ['index-pattern'], + }, + ui: ['save', 'show', 'saveQuery'], + }, + read: { + app: [APP_ID, 'kibana'], + catalogue: [APP_ID], + savedObject: { + all: [], + read: [MAP_SAVED_OBJECT_TYPE, 'index-pattern', 'query'], + }, + ui: ['show'], + }, + }, + }); + } + start(core: CoreStart) {} +} diff --git a/x-pack/plugins/ml/public/application/explorer/__mocks__/mock_viewby_swimlane.json b/x-pack/plugins/ml/public/application/explorer/__mocks__/mock_viewby_swimlane.json new file mode 100644 index 00000000000000..05095c90ac1d65 --- /dev/null +++ b/x-pack/plugins/ml/public/application/explorer/__mocks__/mock_viewby_swimlane.json @@ -0,0 +1,33 @@ +{ + "laneLabels": [ + "View by" + ], + "points": [ + { + "laneLabel": "test-1", + "time": 1486425600, + "value": 0 + }, + { + "laneLabel": "test-2", + "time": 1486440000, + "value": 0.01107053 + }, + { + "laneLabel": "other-3", + "time": 1486454400, + "value": 0.1870243 + }, + { + "laneLabel": "other-4", + "time": 1486468800, + "value": 0.5947769 + }, + { + "laneLabel": "other-5", + "time": 1486483200, + "value": 0 + } + ], + "interval": 14400 +} diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.js b/x-pack/plugins/ml/public/application/explorer/explorer.js index 8fd24798178073..9c9c82a2124725 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer.js @@ -81,6 +81,7 @@ import { AnomaliesTable } from '../components/anomalies_table/anomalies_table'; import { ResizeChecker } from '../../../../../../src/plugins/kibana_utils/public'; import { getTimefilter, getToastNotifications } from '../util/dependency_cache'; import { MlTooltipComponent } from '../components/chart_tooltip'; +import { hasMatchingPoints } from './has_matching_points'; function mapSwimlaneOptionsToEuiOptions(options) { return options.map(option => ({ @@ -284,6 +285,7 @@ export class Explorer extends React.Component { annotationsData, chartsData, filterActive, + filteredFields, filterPlaceHolder, indexPattern, influencers, @@ -418,6 +420,7 @@ export class Explorer extends React.Component { { if (selectedCellTimes.length > 0) { this.highlightOverall(selectedCellTimes); } - this.maskIrrelevantSwimlanes(!!maskAll); + this.maskIrrelevantSwimlanes(Boolean(maskAll)); } else { this.clearSelection(); } diff --git a/x-pack/plugins/ml/public/application/explorer/has_matching_points.test.ts b/x-pack/plugins/ml/public/application/explorer/has_matching_points.test.ts new file mode 100644 index 00000000000000..8d6f71bb7720d5 --- /dev/null +++ b/x-pack/plugins/ml/public/application/explorer/has_matching_points.test.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 mockViewBySwimlaneData from './__mocks__/mock_viewby_swimlane.json'; +import { hasMatchingPoints } from './has_matching_points'; + +const filteredFieldsMatch = ['test@kuery-wildcard@']; +const filteredFieldsNoMatch = ['no-match@kuery-wildcard@', 'nonexistant']; + +describe('hasMatchingPoints', () => { + test('is true', () => { + expect( + hasMatchingPoints({ + filteredFields: filteredFieldsMatch, + swimlaneData: mockViewBySwimlaneData, + }) + ).toBe(true); + }); + + test('is false', () => { + expect( + hasMatchingPoints({ + filteredFields: filteredFieldsNoMatch, + swimlaneData: mockViewBySwimlaneData, + }) + ).toBe(false); + }); +}); diff --git a/x-pack/plugins/ml/public/application/explorer/has_matching_points.ts b/x-pack/plugins/ml/public/application/explorer/has_matching_points.ts new file mode 100644 index 00000000000000..397615d68f189d --- /dev/null +++ b/x-pack/plugins/ml/public/application/explorer/has_matching_points.ts @@ -0,0 +1,30 @@ +/* + * 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 { ExplorerState } from './reducers'; +import { OverallSwimlaneData, SwimlaneData } from './explorer_utils'; + +interface HasMatchingPointsParams { + filteredFields?: ExplorerState['filteredFields']; + swimlaneData: SwimlaneData | OverallSwimlaneData; +} + +export const hasMatchingPoints = ({ + filteredFields = [], + swimlaneData, +}: HasMatchingPointsParams): boolean => { + // If filtered fields includes a wildcard search maskAll only if there are no points matching the pattern + const wildCardField = filteredFields.find(field => /\@kuery-wildcard\@$/.test(field)); + const substring = + wildCardField !== undefined ? wildCardField.replace(/\@kuery-wildcard\@$/, '') : null; + + return ( + substring !== null && + swimlaneData.points.some(point => { + return point.laneLabel.includes(substring); + }) + ); +}; diff --git a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts index 645625f92df295..8ccd359137b670 100644 --- a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts +++ b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts @@ -342,8 +342,8 @@ export class DataVisualizer { aggregatableFields: string[], samplerShardSize: number, timeFieldName: string, - earliestMs: number, - latestMs: number + earliestMs?: number, + latestMs?: number ) { const index = indexPatternTitle; const size = 0; diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.d.ts b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.d.ts deleted file mode 100644 index 2fad1252e64465..00000000000000 --- a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.d.ts +++ /dev/null @@ -1,13 +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 { APICaller } from 'kibana/server'; -import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; - -export function validateCardinality( - callAsCurrentUser: APICaller, - job?: CombinedJob -): Promise; diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.js b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.js deleted file mode 100644 index 22e0510782e115..00000000000000 --- a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.js +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import _ from 'lodash'; - -import { DataVisualizer } from '../data_visualizer'; - -import { validateJobObject } from './validate_job_object'; - -function isValidCategorizationConfig(job, fieldName) { - return ( - typeof job.analysis_config.categorization_field_name !== 'undefined' && - fieldName === 'mlcategory' - ); -} - -function isScriptField(job, fieldName) { - const scriptFields = Object.keys(_.get(job, 'datafeed_config.script_fields', {})); - return scriptFields.includes(fieldName); -} - -// Thresholds to determine whether cardinality is -// too high or low for certain fields analysis -const OVER_FIELD_CARDINALITY_THRESHOLD_LOW = 10; -const OVER_FIELD_CARDINALITY_THRESHOLD_HIGH = 1000000; -const PARTITION_FIELD_CARDINALITY_THRESHOLD = 1000; -const BY_FIELD_CARDINALITY_THRESHOLD = 1000; -const MODEL_PLOT_THRESHOLD_HIGH = 100; - -const validateFactory = (callWithRequest, job) => { - const dv = new DataVisualizer(callWithRequest); - - const modelPlotConfigTerms = _.get(job, ['model_plot_config', 'terms'], ''); - const modelPlotConfigFieldCount = - modelPlotConfigTerms.length > 0 ? modelPlotConfigTerms.split(',').length : 0; - - return async ({ type, isInvalid, messageId }) => { - // modelPlotCardinality counts the cardinality of fields used within detectors. - // if model_plot_config.terms is used, it doesn't count the real cardinality of the field - // but adds only the count of fields used in model_plot_config.terms - let modelPlotCardinality = 0; - const messages = []; - const fieldName = `${type}_field_name`; - - const detectors = job.analysis_config.detectors; - const relevantDetectors = detectors.filter(detector => { - return typeof detector[fieldName] !== 'undefined'; - }); - - if (relevantDetectors.length > 0) { - try { - const uniqueFieldNames = _.uniq(relevantDetectors.map(f => f[fieldName])); - - // use fieldCaps endpoint to get data about whether fields are aggregatable - const fieldCaps = await callWithRequest('fieldCaps', { - index: job.datafeed_config.indices.join(','), - fields: uniqueFieldNames, - }); - - let aggregatableFieldNames = []; - // parse fieldCaps to return an array of just the fields which are aggregatable - if (typeof fieldCaps === 'object' && typeof fieldCaps.fields === 'object') { - aggregatableFieldNames = uniqueFieldNames.filter(field => { - if (typeof fieldCaps.fields[field] !== 'undefined') { - const fieldType = Object.keys(fieldCaps.fields[field])[0]; - return fieldCaps.fields[field][fieldType].aggregatable; - } - return false; - }); - } - - const stats = await dv.checkAggregatableFieldsExist( - job.datafeed_config.indices.join(','), - job.datafeed_config.query, - aggregatableFieldNames, - 0, - job.data_description.time_field - ); - - uniqueFieldNames.forEach(uniqueFieldName => { - const field = _.find(stats.aggregatableExistsFields, { fieldName: uniqueFieldName }); - if (typeof field === 'object') { - modelPlotCardinality += - modelPlotConfigFieldCount > 0 ? modelPlotConfigFieldCount : field.stats.cardinality; - - if (isInvalid(field.stats.cardinality)) { - messages.push({ - id: messageId || `cardinality_${type}_field`, - fieldName: uniqueFieldName, - }); - } - } else { - // only report uniqueFieldName as not aggregatable if it's not part - // of a valid categorization configuration and if it's not a scripted field. - if ( - !isValidCategorizationConfig(job, uniqueFieldName) && - !isScriptField(job, uniqueFieldName) - ) { - messages.push({ - id: 'field_not_aggregatable', - fieldName: uniqueFieldName, - }); - } - } - }); - } catch (e) { - // checkAggregatableFieldsExist may return an error if 'fielddata' is - // disabled for text fields (which is the default). If there was only - // one field we know it was the cause for the error. If there were more - // fields we cannot tell which field caused the error so we return a - // more generic message. - if (relevantDetectors.length === 1) { - messages.push({ - id: 'field_not_aggregatable', - fieldName: relevantDetectors[0][fieldName], - }); - } else { - messages.push({ id: 'fields_not_aggregatable' }); - } - // return here so an outer try/catch doesn't trigger to avoid a BOOM error - return { modelPlotCardinality, messages }; - } - } - - return { modelPlotCardinality, messages }; - }; -}; - -export async function validateCardinality(callWithRequest, job) { - const messages = []; - - validateJobObject(job); - - // find out if there are any relevant detector field names - // where cardinality checks could be run against. - const numDetectorsWithFieldNames = job.analysis_config.detectors.filter(d => { - return d.by_field_name || d.over_field_name || d.partition_field_name; - }); - if (numDetectorsWithFieldNames.length === 0) { - return Promise.resolve([]); - } - - // validate({ type, isInvalid }) asynchronously returns an array of validation messages - const validate = validateFactory(callWithRequest, job); - - const modelPlotEnabled = - (job.model_plot_config && job.model_plot_config.enabled === true) || false; - - // check over fields (population analysis) - const validateOverFieldsLow = validate({ - type: 'over', - isInvalid: cardinality => cardinality < OVER_FIELD_CARDINALITY_THRESHOLD_LOW, - messageId: 'cardinality_over_field_low', - }); - const validateOverFieldsHigh = validate({ - type: 'over', - isInvalid: cardinality => cardinality > OVER_FIELD_CARDINALITY_THRESHOLD_HIGH, - messageId: 'cardinality_over_field_high', - }); - - // check partition/by fields (multi-metric analysis) - const validatePartitionFields = validate({ - type: 'partition', - isInvalid: cardinality => cardinality > PARTITION_FIELD_CARDINALITY_THRESHOLD, - }); - const validateByFields = validate({ - type: 'by', - isInvalid: cardinality => cardinality > BY_FIELD_CARDINALITY_THRESHOLD, - }); - - // we already called the validation functions above, - // but add "await" only here so they can be run in parallel. - const validations = [ - await validateByFields, - await validateOverFieldsLow, - await validateOverFieldsHigh, - await validatePartitionFields, - ]; - - // if model plot is enabled, check against the - // overall cardinality of all fields used in the detectors. - const modelPlotCardinality = validations.reduce((p, c) => { - return p + c.modelPlotCardinality; - }, 0); - - if (modelPlotEnabled && modelPlotCardinality > MODEL_PLOT_THRESHOLD_HIGH) { - messages.push({ - id: 'cardinality_model_plot_high', - modelPlotCardinality, - }); - } - - // add all messages returned from the individual cardinality checks - validations.forEach(v => messages.push(...v.messages)); - - if (messages.length === 0) { - messages.push({ id: 'success_cardinality' }); - } - - return messages; -} diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.ts b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.ts new file mode 100644 index 00000000000000..cf3d6d004c37e2 --- /dev/null +++ b/x-pack/plugins/ml/server/models/job_validation/validate_cardinality.ts @@ -0,0 +1,226 @@ +/* + * 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 'kibana/server'; +import { DataVisualizer } from '../data_visualizer'; + +import { validateJobObject } from './validate_job_object'; +import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; +import { Detector } from '../../../common/types/anomaly_detection_jobs'; + +function isValidCategorizationConfig(job: CombinedJob, fieldName: string): boolean { + return ( + typeof job.analysis_config.categorization_field_name !== 'undefined' && + fieldName === 'mlcategory' + ); +} + +function isScriptField(job: CombinedJob, fieldName: string): boolean { + const scriptFields = Object.keys(job.datafeed_config.script_fields ?? {}); + return scriptFields.includes(fieldName); +} + +// Thresholds to determine whether cardinality is +// too high or low for certain fields analysis +const OVER_FIELD_CARDINALITY_THRESHOLD_LOW = 10; +const OVER_FIELD_CARDINALITY_THRESHOLD_HIGH = 1000000; +const PARTITION_FIELD_CARDINALITY_THRESHOLD = 1000; +const BY_FIELD_CARDINALITY_THRESHOLD = 1000; +const MODEL_PLOT_THRESHOLD_HIGH = 100; + +type Messages = Array<{ id: string; fieldName?: string }>; + +type Validator = (obj: { + type: string; + isInvalid: (cardinality: number) => boolean; + messageId?: string; +}) => Promise<{ + modelPlotCardinality: number; + messages: Messages; +}>; + +const validateFactory = (callWithRequest: APICaller, job: CombinedJob): Validator => { + const dv = new DataVisualizer(callWithRequest); + + const modelPlotConfigTerms = job?.model_plot_config?.terms ?? ''; + const modelPlotConfigFieldCount = + modelPlotConfigTerms.length > 0 ? modelPlotConfigTerms.split(',').length : 0; + + return async ({ type, isInvalid, messageId }) => { + // modelPlotCardinality counts the cardinality of fields used within detectors. + // if model_plot_config.terms is used, it doesn't count the real cardinality of the field + // but adds only the count of fields used in model_plot_config.terms + let modelPlotCardinality = 0; + const messages: Messages = []; + const fieldName = `${type}_field_name` as keyof Pick< + Detector, + 'by_field_name' | 'over_field_name' | 'partition_field_name' + >; + + const detectors = job.analysis_config.detectors; + const relevantDetectors = detectors.filter(detector => { + return typeof detector[fieldName] !== 'undefined'; + }); + + if (relevantDetectors.length > 0) { + try { + const uniqueFieldNames = [...new Set(relevantDetectors.map(f => f[fieldName]))] as string[]; + + // use fieldCaps endpoint to get data about whether fields are aggregatable + const fieldCaps = await callWithRequest('fieldCaps', { + index: job.datafeed_config.indices.join(','), + fields: uniqueFieldNames, + }); + + let aggregatableFieldNames: string[] = []; + // parse fieldCaps to return an array of just the fields which are aggregatable + if (typeof fieldCaps === 'object' && typeof fieldCaps.fields === 'object') { + aggregatableFieldNames = uniqueFieldNames.filter(field => { + if (typeof fieldCaps.fields[field] !== 'undefined') { + const fieldType = Object.keys(fieldCaps.fields[field])[0]; + return fieldCaps.fields[field][fieldType].aggregatable; + } + return false; + }); + } + + const stats = await dv.checkAggregatableFieldsExist( + job.datafeed_config.indices.join(','), + job.datafeed_config.query, + aggregatableFieldNames, + 0, + job.data_description.time_field + ); + + uniqueFieldNames.forEach(uniqueFieldName => { + const field = stats.aggregatableExistsFields.find( + fieldData => fieldData.fieldName === uniqueFieldName + ); + if (field !== undefined && typeof field === 'object' && field.stats) { + modelPlotCardinality += + modelPlotConfigFieldCount > 0 ? modelPlotConfigFieldCount : field.stats.cardinality!; + + if (isInvalid(field.stats.cardinality!)) { + messages.push({ + id: messageId || `cardinality_${type}_field`, + fieldName: uniqueFieldName, + }); + } + } else { + // only report uniqueFieldName as not aggregatable if it's not part + // of a valid categorization configuration and if it's not a scripted field. + if ( + !isValidCategorizationConfig(job, uniqueFieldName) && + !isScriptField(job, uniqueFieldName) + ) { + messages.push({ + id: 'field_not_aggregatable', + fieldName: uniqueFieldName, + }); + } + } + }); + } catch (e) { + // checkAggregatableFieldsExist may return an error if 'fielddata' is + // disabled for text fields (which is the default). If there was only + // one field we know it was the cause for the error. If there were more + // fields we cannot tell which field caused the error so we return a + // more generic message. + if (relevantDetectors.length === 1) { + messages.push({ + id: 'field_not_aggregatable', + fieldName: relevantDetectors[0][fieldName]!, + }); + } else { + messages.push({ id: 'fields_not_aggregatable' }); + } + // return here so an outer try/catch doesn't trigger to avoid a BOOM error + return { modelPlotCardinality, messages }; + } + } + + return { modelPlotCardinality, messages }; + }; +}; + +export async function validateCardinality( + callWithRequest: APICaller, + job?: CombinedJob +): Promise> | never { + const messages = []; + + if (!validateJobObject(job)) { + // required for TS type casting, validateJobObject throws an error internally. + throw new Error(); + } + + // find out if there are any relevant detector field names + // where cardinality checks could be run against. + const numDetectorsWithFieldNames = job.analysis_config.detectors.filter(d => { + return d.by_field_name || d.over_field_name || d.partition_field_name; + }); + if (numDetectorsWithFieldNames.length === 0) { + return []; + } + + // validate({ type, isInvalid }) asynchronously returns an array of validation messages + const validate = validateFactory(callWithRequest, job); + + const modelPlotEnabled = job.model_plot_config?.enabled ?? false; + + // check over fields (population analysis) + const validateOverFieldsLow = validate({ + type: 'over', + isInvalid: cardinality => cardinality < OVER_FIELD_CARDINALITY_THRESHOLD_LOW, + messageId: 'cardinality_over_field_low', + }); + const validateOverFieldsHigh = validate({ + type: 'over', + isInvalid: cardinality => cardinality > OVER_FIELD_CARDINALITY_THRESHOLD_HIGH, + messageId: 'cardinality_over_field_high', + }); + + // check partition/by fields (multi-metric analysis) + const validatePartitionFields = validate({ + type: 'partition', + isInvalid: cardinality => cardinality > PARTITION_FIELD_CARDINALITY_THRESHOLD, + }); + const validateByFields = validate({ + type: 'by', + isInvalid: cardinality => cardinality > BY_FIELD_CARDINALITY_THRESHOLD, + }); + + // we already called the validation functions above, + // but add "await" only here so they can be run in parallel. + const validations = [ + await validateByFields, + await validateOverFieldsLow, + await validateOverFieldsHigh, + await validatePartitionFields, + ]; + + // if model plot is enabled, check against the + // overall cardinality of all fields used in the detectors. + const modelPlotCardinality = validations.reduce((p, c) => { + return p + c.modelPlotCardinality; + }, 0); + + if (modelPlotEnabled && modelPlotCardinality > MODEL_PLOT_THRESHOLD_HIGH) { + messages.push({ + id: 'cardinality_model_plot_high', + modelPlotCardinality, + }); + } + + // add all messages returned from the individual cardinality checks + validations.forEach(v => messages.push(...v.messages)); + + if (messages.length === 0) { + messages.push({ id: 'success_cardinality' }); + } + + return messages; +} diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_job_object.ts b/x-pack/plugins/ml/server/models/job_validation/validate_job_object.ts index b0271fb5b4f45c..0d89656e051173 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_job_object.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_job_object.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; -export function validateJobObject(job: CombinedJob | null) { +export function validateJobObject(job: CombinedJob | null | undefined): job is CombinedJob | never { if (job === null || typeof job !== 'object') { throw new Error( i18n.translate('xpack.ml.models.jobValidation.validateJobObject.jobIsNotObjectErrorMessage', { @@ -93,4 +93,5 @@ export function validateJobObject(job: CombinedJob | null) { ) ); } + return true; } diff --git a/x-pack/plugins/painless_lab/public/styles/_index.scss b/x-pack/plugins/painless_lab/public/styles/_index.scss index df68a6450c1917..e6c9574161fd8b 100644 --- a/x-pack/plugins/painless_lab/public/styles/_index.scss +++ b/x-pack/plugins/painless_lab/public/styles/_index.scss @@ -5,7 +5,7 @@ * This is a very brittle way of preventing the editor and other content from disappearing * behind the bottom bar. */ -$bottomBarHeight: calc(#{$euiSize} * 3); +$bottomBarHeight: $euiSize * 3; .painlessLabBottomBarPlaceholder { height: $bottomBarHeight; @@ -40,8 +40,11 @@ $bottomBarHeight: calc(#{$euiSize} * 3); line-height: 0; } +// This value is calculated to static value using SCSS because calc in calc has issues in IE11 +$headerHeightOffset: $euiHeaderHeightCompensation * 2; + .painlessLabMainContainer { - height: calc(100vh - calc(#{$euiHeaderChildSize} * 2) - #{$bottomBarHeight}); + height: calc(100vh - #{$headerHeightOffset} - #{$bottomBarHeight}); } .painlessLabPanelsContainer { diff --git a/x-pack/plugins/searchprofiler/public/styles/_index.scss b/x-pack/plugins/searchprofiler/public/styles/_index.scss index 5c35e9a23b8a1a..e63042cf8fe2f7 100644 --- a/x-pack/plugins/searchprofiler/public/styles/_index.scss +++ b/x-pack/plugins/searchprofiler/public/styles/_index.scss @@ -47,8 +47,11 @@ } } +// This value is calculated to static value using SCSS because calc in calc has issues in IE11 +$headerHeightOffset: $euiHeaderHeightCompensation * 2; + .appRoot { - height: calc(100vh - calc(#{$euiHeaderChildSize} * 2)); + height: calc(100vh - #{$headerHeightOffset}); overflow: hidden; flex-shrink: 1; } diff --git a/x-pack/plugins/siem/common/detection_engine/ml_helpers.test.ts b/x-pack/plugins/siem/common/detection_engine/ml_helpers.test.ts deleted file mode 100644 index ba93b2e4b8a0d8..00000000000000 --- a/x-pack/plugins/siem/common/detection_engine/ml_helpers.test.ts +++ /dev/null @@ -1,57 +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 { isJobStarted, isJobLoading, isJobFailed } from './ml_helpers'; - -describe('isJobStarted', () => { - test('returns false if only jobState is enabled', () => { - expect(isJobStarted('started', 'closing')).toBe(false); - }); - - test('returns false if only datafeedState is enabled', () => { - expect(isJobStarted('stopping', 'opened')).toBe(false); - }); - - test('returns true if both enabled states are provided', () => { - expect(isJobStarted('started', 'opened')).toBe(true); - }); -}); - -describe('isJobLoading', () => { - test('returns true if both loading states are not provided', () => { - expect(isJobLoading('started', 'closing')).toBe(true); - }); - - test('returns true if only jobState is loading', () => { - expect(isJobLoading('starting', 'opened')).toBe(true); - }); - - test('returns true if only datafeedState is loading', () => { - expect(isJobLoading('started', 'opening')).toBe(true); - }); - - test('returns false if both disabling states are provided', () => { - expect(isJobLoading('stopping', 'closing')).toBe(true); - }); -}); - -describe('isJobFailed', () => { - test('returns true if only jobState is failure/deleted', () => { - expect(isJobFailed('failed', 'stopping')).toBe(true); - }); - - test('returns true if only dataFeed is failure/deleted', () => { - expect(isJobFailed('started', 'deleted')).toBe(true); - }); - - test('returns true if both enabled states are failure/deleted', () => { - expect(isJobFailed('failed', 'deleted')).toBe(true); - }); - - test('returns false only if both states are not failure/deleted', () => { - expect(isJobFailed('opened', 'stopping')).toBe(false); - }); -}); diff --git a/x-pack/plugins/siem/common/detection_engine/ml_helpers.ts b/x-pack/plugins/siem/common/detection_engine/ml_helpers.ts deleted file mode 100644 index e4158d08d448dc..00000000000000 --- a/x-pack/plugins/siem/common/detection_engine/ml_helpers.ts +++ /dev/null @@ -1,26 +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 { RuleType } from './types'; - -// Based on ML Job/Datafeed States from x-pack/legacy/plugins/ml/common/constants/states.js -const enabledStates = ['started', 'opened']; -const loadingStates = ['starting', 'stopping', 'opening', 'closing']; -const failureStates = ['deleted', 'failed']; - -export const isJobStarted = (jobState: string, datafeedState: string): boolean => { - return enabledStates.includes(jobState) && enabledStates.includes(datafeedState); -}; - -export const isJobLoading = (jobState: string, datafeedState: string): boolean => { - return loadingStates.includes(jobState) || loadingStates.includes(datafeedState); -}; - -export const isJobFailed = (jobState: string, datafeedState: string): boolean => { - return failureStates.includes(jobState) || failureStates.includes(datafeedState); -}; - -export const isMlRule = (ruleType: RuleType) => ruleType === 'machine_learning'; diff --git a/x-pack/plugins/siem/public/components/ml/empty_ml_capabilities.ts b/x-pack/plugins/siem/common/machine_learning/empty_ml_capabilities.ts similarity index 76% rename from x-pack/plugins/siem/public/components/ml/empty_ml_capabilities.ts rename to x-pack/plugins/siem/common/machine_learning/empty_ml_capabilities.ts index 9c8610ccd628c5..0d6a13c108b04f 100644 --- a/x-pack/plugins/siem/public/components/ml/empty_ml_capabilities.ts +++ b/x-pack/plugins/siem/common/machine_learning/empty_ml_capabilities.ts @@ -4,10 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MlCapabilities } from './types'; +import { MlCapabilitiesResponse } from '../../../ml/common/types/capabilities'; -export const emptyMlCapabilities: MlCapabilities = { +export const emptyMlCapabilities: MlCapabilitiesResponse = { capabilities: { + canAccessML: false, + canGetAnnotations: false, + canCreateAnnotation: false, + canDeleteAnnotation: false, canGetJobs: false, canCreateJob: false, canDeleteJob: false, @@ -26,11 +30,8 @@ export const emptyMlCapabilities: MlCapabilities = { canCreateFilter: false, canDeleteFilter: false, canFindFileStructure: false, - canGetDataFrame: false, - canDeleteDataFrame: false, - canPreviewDataFrame: false, - canCreateDataFrame: false, - canStartStopDataFrame: false, + canCreateDatafeed: false, + canDeleteDatafeed: false, canGetDataFrameAnalytics: false, canDeleteDataFrameAnalytics: false, canCreateDataFrameAnalytics: false, diff --git a/x-pack/plugins/siem/public/components/ml/permissions/has_ml_admin_permissions.test.ts b/x-pack/plugins/siem/common/machine_learning/has_ml_admin_permissions.test.ts similarity index 96% rename from x-pack/plugins/siem/public/components/ml/permissions/has_ml_admin_permissions.test.ts rename to x-pack/plugins/siem/common/machine_learning/has_ml_admin_permissions.test.ts index ee237b42bede99..9824ce1232cbe4 100644 --- a/x-pack/plugins/siem/public/components/ml/permissions/has_ml_admin_permissions.test.ts +++ b/x-pack/plugins/siem/common/machine_learning/has_ml_admin_permissions.test.ts @@ -6,7 +6,7 @@ import { hasMlAdminPermissions } from './has_ml_admin_permissions'; import { cloneDeep } from 'lodash/fp'; -import { emptyMlCapabilities } from '../empty_ml_capabilities'; +import { emptyMlCapabilities } from './empty_ml_capabilities'; describe('has_ml_admin_permissions', () => { let mlCapabilities = cloneDeep(emptyMlCapabilities); diff --git a/x-pack/plugins/siem/public/components/ml/permissions/has_ml_admin_permissions.ts b/x-pack/plugins/siem/common/machine_learning/has_ml_admin_permissions.ts similarity index 79% rename from x-pack/plugins/siem/public/components/ml/permissions/has_ml_admin_permissions.ts rename to x-pack/plugins/siem/common/machine_learning/has_ml_admin_permissions.ts index 6fe142cf8e5832..106e9aabbc711d 100644 --- a/x-pack/plugins/siem/public/components/ml/permissions/has_ml_admin_permissions.ts +++ b/x-pack/plugins/siem/common/machine_learning/has_ml_admin_permissions.ts @@ -4,21 +4,21 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MlCapabilities } from '../types'; +import { MlCapabilitiesResponse } from '../../../ml/common/types/capabilities'; -export const hasMlAdminPermissions = (capabilities: MlCapabilities): boolean => +export const hasMlAdminPermissions = (capabilities: MlCapabilitiesResponse): boolean => getDataFeedPermissions(capabilities) && getJobPermissions(capabilities) && getFilterPermissions(capabilities) && getCalendarPermissions(capabilities); -const getDataFeedPermissions = ({ capabilities }: MlCapabilities): boolean => +const getDataFeedPermissions = ({ capabilities }: MlCapabilitiesResponse): boolean => capabilities.canGetDatafeeds && capabilities.canStartStopDatafeed && capabilities.canUpdateDatafeed && capabilities.canPreviewDatafeed; -const getJobPermissions = ({ capabilities }: MlCapabilities): boolean => +const getJobPermissions = ({ capabilities }: MlCapabilitiesResponse): boolean => capabilities.canCreateJob && capabilities.canGetJobs && capabilities.canUpdateJob && @@ -27,8 +27,8 @@ const getJobPermissions = ({ capabilities }: MlCapabilities): boolean => capabilities.canCloseJob && capabilities.canForecastJob; -const getFilterPermissions = ({ capabilities }: MlCapabilities) => +const getFilterPermissions = ({ capabilities }: MlCapabilitiesResponse) => capabilities.canGetFilters && capabilities.canCreateFilter && capabilities.canDeleteFilter; -const getCalendarPermissions = ({ capabilities }: MlCapabilities) => +const getCalendarPermissions = ({ capabilities }: MlCapabilitiesResponse) => capabilities.canCreateCalendar && capabilities.canGetCalendars && capabilities.canDeleteCalendar; diff --git a/x-pack/plugins/siem/public/components/ml/permissions/has_ml_user_permissions.test.ts b/x-pack/plugins/siem/common/machine_learning/has_ml_user_permissions.test.ts similarity index 94% rename from x-pack/plugins/siem/public/components/ml/permissions/has_ml_user_permissions.test.ts rename to x-pack/plugins/siem/common/machine_learning/has_ml_user_permissions.test.ts index e3804055f2abbd..4d58cda81d71c5 100644 --- a/x-pack/plugins/siem/public/components/ml/permissions/has_ml_user_permissions.test.ts +++ b/x-pack/plugins/siem/common/machine_learning/has_ml_user_permissions.test.ts @@ -6,7 +6,7 @@ import { cloneDeep } from 'lodash/fp'; import { hasMlUserPermissions } from './has_ml_user_permissions'; -import { emptyMlCapabilities } from '../empty_ml_capabilities'; +import { emptyMlCapabilities } from './empty_ml_capabilities'; describe('has_ml_user_permissions', () => { let mlCapabilities = cloneDeep(emptyMlCapabilities); diff --git a/x-pack/plugins/siem/public/components/ml/permissions/has_ml_user_permissions.ts b/x-pack/plugins/siem/common/machine_learning/has_ml_user_permissions.ts similarity index 81% rename from x-pack/plugins/siem/public/components/ml/permissions/has_ml_user_permissions.ts rename to x-pack/plugins/siem/common/machine_learning/has_ml_user_permissions.ts index 2d55b7d74f93c5..dd746e4737bbc8 100644 --- a/x-pack/plugins/siem/public/components/ml/permissions/has_ml_user_permissions.ts +++ b/x-pack/plugins/siem/common/machine_learning/has_ml_user_permissions.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { MlCapabilities } from '../types'; +import { MlCapabilitiesResponse } from '../../../ml/common/types/capabilities'; -export const hasMlUserPermissions = (capabilities: MlCapabilities): boolean => +export const hasMlUserPermissions = (capabilities: MlCapabilitiesResponse): boolean => capabilities.capabilities.canGetJobs && capabilities.capabilities.canGetDatafeeds && capabilities.capabilities.canGetCalendars; diff --git a/x-pack/plugins/siem/common/machine_learning/helpers.test.ts b/x-pack/plugins/siem/common/machine_learning/helpers.test.ts new file mode 100644 index 00000000000000..ce343f75933dcf --- /dev/null +++ b/x-pack/plugins/siem/common/machine_learning/helpers.test.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isJobStarted, isJobLoading, isJobFailed } from './helpers'; + +describe('isJobStarted', () => { + test('returns false if only jobState is enabled', () => { + expect(isJobStarted('started', 'closing')).toBe(false); + }); + + test('returns false if only datafeedState is enabled', () => { + expect(isJobStarted('stopping', 'opened')).toBe(false); + }); + + test('returns true if both enabled states are provided', () => { + expect(isJobStarted('started', 'opened')).toBe(true); + }); +}); + +describe('isJobLoading', () => { + test('returns true if both loading states are not provided', () => { + expect(isJobLoading('started', 'closing')).toBe(true); + }); + + test('returns true if only jobState is loading', () => { + expect(isJobLoading('starting', 'opened')).toBe(true); + }); + + test('returns true if only datafeedState is loading', () => { + expect(isJobLoading('started', 'opening')).toBe(true); + }); + + test('returns false if both disabling states are provided', () => { + expect(isJobLoading('stopping', 'closing')).toBe(true); + }); +}); + +describe('isJobFailed', () => { + test('returns true if only jobState is failure/deleted', () => { + expect(isJobFailed('failed', 'stopping')).toBe(true); + }); + + test('returns true if only dataFeed is failure/deleted', () => { + expect(isJobFailed('started', 'deleted')).toBe(true); + }); + + test('returns true if both enabled states are failure/deleted', () => { + expect(isJobFailed('failed', 'deleted')).toBe(true); + }); + + test('returns false only if both states are not failure/deleted', () => { + expect(isJobFailed('opened', 'stopping')).toBe(false); + }); +}); diff --git a/x-pack/plugins/siem/common/machine_learning/helpers.ts b/x-pack/plugins/siem/common/machine_learning/helpers.ts new file mode 100644 index 00000000000000..fe3eb79a6f6109 --- /dev/null +++ b/x-pack/plugins/siem/common/machine_learning/helpers.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RuleType } from '../detection_engine/types'; + +// Based on ML Job/Datafeed States from x-pack/legacy/plugins/ml/common/constants/states.js +const enabledStates = ['started', 'opened']; +const loadingStates = ['starting', 'stopping', 'opening', 'closing']; +const failureStates = ['deleted', 'failed']; + +export const isJobStarted = (jobState: string, datafeedState: string): boolean => { + return enabledStates.includes(jobState) && enabledStates.includes(datafeedState); +}; + +export const isJobLoading = (jobState: string, datafeedState: string): boolean => { + return loadingStates.includes(jobState) || loadingStates.includes(datafeedState); +}; + +export const isJobFailed = (jobState: string, datafeedState: string): boolean => { + return failureStates.includes(jobState) || failureStates.includes(datafeedState); +}; + +export const isMlRule = (ruleType: RuleType) => ruleType === 'machine_learning'; diff --git a/x-pack/plugins/siem/public/alerts/components/activity_monitor/columns.tsx b/x-pack/plugins/siem/public/alerts/components/activity_monitor/columns.tsx new file mode 100644 index 00000000000000..51a5397637e7ca --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/activity_monitor/columns.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. + */ + +/* eslint-disable react/display-name */ + +import { + EuiIconTip, + EuiLink, + EuiTextColor, + EuiBasicTableColumn, + EuiTableActionsColumnType, +} from '@elastic/eui'; +import React from 'react'; +import { getEmptyTagValue } from '../../../common/components/empty_value'; +import { ColumnTypes } from './types'; + +const actions: EuiTableActionsColumnType['actions'] = [ + { + available: (item: ColumnTypes) => item.status === 'Running', + description: 'Stop', + icon: 'stop', + isPrimary: true, + name: 'Stop', + onClick: () => {}, + type: 'icon', + }, + { + available: (item: ColumnTypes) => item.status === 'Stopped', + description: 'Resume', + icon: 'play', + isPrimary: true, + name: 'Resume', + onClick: () => {}, + type: 'icon', + }, +]; + +// Michael: Are we able to do custom, in-table-header filters, as shown in my wireframes? +export const columns: Array> = [ + { + field: 'rule' as const, + name: 'Rule', + render: (value: ColumnTypes['rule'], _: ColumnTypes) => ( + {value.name} + ), + sortable: true, + truncateText: true, + }, + { + field: 'ran' as const, + name: 'Ran', + render: (value: ColumnTypes['ran'], _: ColumnTypes) => '--', + sortable: true, + truncateText: true, + }, + { + field: 'lookedBackTo' as const, + name: 'Looked back to', + render: (value: ColumnTypes['lookedBackTo'], _: ColumnTypes) => '--', + sortable: true, + truncateText: true, + }, + { + field: 'status' as const, + name: 'Status', + sortable: true, + truncateText: true, + }, + { + field: 'response' as const, + name: 'Response', + render: (value: ColumnTypes['response'], _: ColumnTypes) => { + return value === undefined ? ( + getEmptyTagValue() + ) : ( + <> + {value === 'Fail' ? ( + + {value} + + ) : ( + {value} + )} + + ); + }, + sortable: true, + truncateText: true, + }, + { + actions, + width: '40px', + }, +]; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/activity_monitor/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.test.tsx rename to x-pack/plugins/siem/public/alerts/components/activity_monitor/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/activity_monitor/index.tsx b/x-pack/plugins/siem/public/alerts/components/activity_monitor/index.tsx new file mode 100644 index 00000000000000..c2b6b0f025e1d6 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/activity_monitor/index.tsx @@ -0,0 +1,320 @@ +/* + * 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 { EuiBasicTable, EuiPanel, EuiSpacer } from '@elastic/eui'; +import React, { useState, useCallback } from 'react'; +import { HeaderSection } from '../../../common/components/header_section'; +import { + UtilityBar, + UtilityBarAction, + UtilityBarGroup, + UtilityBarSection, + UtilityBarText, +} from '../../../common/components/utility_bar'; +import { columns } from './columns'; +import { ColumnTypes, PageTypes, SortTypes } from './types'; + +export const ActivityMonitor = React.memo(() => { + const sampleTableData: ColumnTypes[] = [ + { + id: 1, + rule: { + href: '#/detections/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Running', + }, + { + id: 2, + rule: { + href: '#/detections/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Stopped', + }, + { + id: 3, + rule: { + href: '#/detections/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Fail', + }, + { + id: 4, + rule: { + href: '#/detections/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 5, + rule: { + href: '#/detections/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 6, + rule: { + href: '#/detections/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 7, + rule: { + href: '#/detections/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 8, + rule: { + href: '#/detections/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 9, + rule: { + href: '#/detections/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 10, + rule: { + href: '#/detections/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 11, + rule: { + href: '#/detections/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 12, + rule: { + href: '#/detections/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 13, + rule: { + href: '#/detections/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 14, + rule: { + href: '#/detections/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 15, + rule: { + href: '#/detections/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 16, + rule: { + href: '#/detections/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 17, + rule: { + href: '#/detections/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 18, + rule: { + href: '#/detections/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 19, + rule: { + href: '#/detections/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 20, + rule: { + href: '#/detections/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + { + id: 21, + rule: { + href: '#/detections/rules/rule-details', + name: 'Automated exfiltration', + }, + ran: '2019-12-28 00:00:00.000-05:00', + lookedBackTo: '2019-12-28 00:00:00.000-05:00', + status: 'Completed', + response: 'Success', + }, + ]; + + const [itemsTotalState] = useState(sampleTableData.length); + const [pageState, setPageState] = useState({ index: 0, size: 20 }); + // const [selectedState, setSelectedState] = useState([]); + const [sortState, setSortState] = useState({ field: 'ran', direction: 'desc' }); + + const handleChange = useCallback( + ({ page, sort }: { page?: PageTypes; sort?: SortTypes }) => { + setPageState(page!); + setSortState(sort!); + }, + [setPageState, setSortState] + ); + + return ( + <> + + + + + + + + {'Showing: 39 activites'} + + + + {'Selected: 2 activities'} + + {'Stop selected'} + + + + {'Clear 7 filters'} + + + + { + // @ts-ignore `Columns` interface differs from EUI's `column` type and is used all over this plugin, so ignore the differences instead of refactoring a lot of code + } + item.status !== 'Completed', + selectableMessage: (selectable: boolean) => + selectable ? '' : 'Completed runs cannot be acted upon', + onSelectionChange: (selectedItems: ColumnTypes[]) => { + // setSelectedState(selectedItems); + }, + }} + sorting={{ + sort: sortState, + }} + /> + + + ); +}); +ActivityMonitor.displayName = 'ActivityMonitor'; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/activity_monitor/types.ts b/x-pack/plugins/siem/public/alerts/components/activity_monitor/types.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/components/activity_monitor/types.ts rename to x-pack/plugins/siem/public/alerts/components/activity_monitor/types.ts diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/detection_engine_header_page/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/index.test.tsx rename to x-pack/plugins/siem/public/alerts/components/detection_engine_header_page/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/detection_engine_header_page/index.tsx b/x-pack/plugins/siem/public/alerts/components/detection_engine_header_page/index.tsx new file mode 100644 index 00000000000000..42a5afb600fba6 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/detection_engine_header_page/index.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { HeaderPage, HeaderPageProps } from '../../../common/components/header_page'; +import * as i18n from './translations'; + +const DetectionEngineHeaderPageComponent: React.FC = props => ( + +); + +DetectionEngineHeaderPageComponent.defaultProps = { + badgeOptions: { + beta: true, + text: i18n.PAGE_BADGE_LABEL, + tooltip: i18n.PAGE_BADGE_TOOLTIP, + }, +}; + +export const DetectionEngineHeaderPage = React.memo(DetectionEngineHeaderPageComponent); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/translations.ts b/x-pack/plugins/siem/public/alerts/components/detection_engine_header_page/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/translations.ts rename to x-pack/plugins/siem/public/alerts/components/detection_engine_header_page/translations.ts diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/no_api_integration_callout/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.test.tsx rename to x-pack/plugins/siem/public/alerts/components/no_api_integration_callout/index.test.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.tsx b/x-pack/plugins/siem/public/alerts/components/no_api_integration_callout/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/index.tsx rename to x-pack/plugins/siem/public/alerts/components/no_api_integration_callout/index.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/translations.ts b/x-pack/plugins/siem/public/alerts/components/no_api_integration_callout/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/components/no_api_integration_callout/translations.ts rename to x-pack/plugins/siem/public/alerts/components/no_api_integration_callout/translations.ts diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/no_write_signals_callout/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.test.tsx rename to x-pack/plugins/siem/public/alerts/components/no_write_signals_callout/index.test.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.tsx b/x-pack/plugins/siem/public/alerts/components/no_write_signals_callout/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/index.tsx rename to x-pack/plugins/siem/public/alerts/components/no_write_signals_callout/index.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/translations.ts b/x-pack/plugins/siem/public/alerts/components/no_write_signals_callout/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/components/no_write_signals_callout/translations.ts rename to x-pack/plugins/siem/public/alerts/components/no_write_signals_callout/translations.ts diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/accordion_title/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/rules/accordion_title/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/accordion_title/index.test.tsx rename to x-pack/plugins/siem/public/alerts/components/rules/accordion_title/index.test.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/accordion_title/index.tsx b/x-pack/plugins/siem/public/alerts/components/rules/accordion_title/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/accordion_title/index.tsx rename to x-pack/plugins/siem/public/alerts/components/rules/accordion_title/index.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/add_item_form/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/rules/add_item_form/index.test.tsx new file mode 100644 index 00000000000000..890e66c8767c4b --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/add_item_form/index.test.tsx @@ -0,0 +1,32 @@ +/* + * 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 { shallow } from 'enzyme'; + +import { AddItem } from './index'; +import { useFormFieldMock } from '../../../../common/mock/test_providers'; + +describe('AddItem', () => { + it('renders correctly', () => { + const Component = () => { + const field = useFormFieldMock(); + + return ( + + ); + }; + const wrapper = shallow(); + + expect(wrapper.dive().find('[iconType="plusInCircle"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/add_item_form/index.tsx b/x-pack/plugins/siem/public/alerts/components/rules/add_item_form/index.tsx new file mode 100644 index 00000000000000..d6c18078f8acd4 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/add_item_form/index.tsx @@ -0,0 +1,188 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiButtonEmpty, + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiFieldText, + EuiSpacer, +} from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; +import React, { ChangeEvent, useCallback, useEffect, useState, useRef } from 'react'; +import styled from 'styled-components'; + +import * as RuleI18n from '../../../pages/detection_engine/rules/translations'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../shared_imports'; + +interface AddItemProps { + addText: string; + field: FieldHook; + dataTestSubj: string; + idAria: string; + isDisabled: boolean; + validate?: (args: unknown) => boolean; +} + +const MyEuiFormRow = styled(EuiFormRow)` + .euiFormRow__labelWrapper { + .euiText { + padding-right: 32px; + } + } +`; + +export const MyAddItemButton = styled(EuiButtonEmpty)` + margin-top: 4px; + + &.euiButtonEmpty--xSmall { + font-size: 12px; + } + + .euiIcon { + width: 12px; + height: 12px; + } +`; + +MyAddItemButton.defaultProps = { + flush: 'left', + iconType: 'plusInCircle', + size: 'xs', +}; + +export const AddItem = ({ + addText, + dataTestSubj, + field, + idAria, + isDisabled, + validate, +}: AddItemProps) => { + const [showValidation, setShowValidation] = useState(false); + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + const [haveBeenKeyboardDeleted, setHaveBeenKeyboardDeleted] = useState(-1); + + const inputsRef = useRef([]); + + const removeItem = useCallback( + (index: number) => { + const values = field.value as string[]; + const newValues = [...values.slice(0, index), ...values.slice(index + 1)]; + field.setValue(newValues.length === 0 ? [''] : newValues); + inputsRef.current = [ + ...inputsRef.current.slice(0, index), + ...inputsRef.current.slice(index + 1), + ]; + inputsRef.current = inputsRef.current.map((ref, i) => { + if (i >= index && inputsRef.current[index] != null) { + ref.value = 're-render'; + } + return ref; + }); + }, + [field] + ); + + const addItem = useCallback(() => { + const values = field.value as string[]; + field.setValue([...values, '']); + }, [field]); + + const updateItem = useCallback( + (event: ChangeEvent, index: number) => { + event.persist(); + const values = field.value as string[]; + const value = event.target.value; + field.setValue([...values.slice(0, index), value, ...values.slice(index + 1)]); + }, + [field] + ); + + const handleLastInputRef = useCallback( + (index: number, element: HTMLInputElement | null) => { + if (element != null) { + inputsRef.current = [ + ...inputsRef.current.slice(0, index), + element, + ...inputsRef.current.slice(index + 1), + ]; + } + }, + [inputsRef] + ); + + useEffect(() => { + if ( + haveBeenKeyboardDeleted !== -1 && + !isEmpty(inputsRef.current) && + inputsRef.current[haveBeenKeyboardDeleted] != null + ) { + inputsRef.current[haveBeenKeyboardDeleted].focus(); + setHaveBeenKeyboardDeleted(-1); + } + }, [haveBeenKeyboardDeleted, inputsRef.current]); + + const values = field.value as string[]; + return ( + + <> + {values.map((item, index) => { + const euiFieldProps = { + disabled: isDisabled, + ...(index === values.length - 1 + ? { inputRef: handleLastInputRef.bind(null, index) } + : {}), + ...((inputsRef.current[index] != null && inputsRef.current[index].value !== item) || + inputsRef.current[index] == null + ? { value: item } + : {}), + isInvalid: validate == null ? false : showValidation && validate(item), + }; + return ( +
+ + + setShowValidation(true)} + onChange={e => updateItem(e, index)} + fullWidth + {...euiFieldProps} + /> + + + removeItem(index)} + aria-label={RuleI18n.DELETE} + /> + + + + {values.length - 1 !== index && } +
+ ); + })} + + + {addText} + + +
+ ); +}; diff --git a/x-pack/plugins/siem/public/alerts/components/rules/all_rules_tables/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/rules/all_rules_tables/index.test.tsx new file mode 100644 index 00000000000000..d841af69a7537d --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/all_rules_tables/index.test.tsx @@ -0,0 +1,120 @@ +/* + * 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, { useRef } from 'react'; +import { shallow } from 'enzyme'; + +import { AllRulesTables } from './index'; +import { AllRulesTabs } from '../../../pages/detection_engine/rules/all'; + +describe('AllRulesTables', () => { + it('renders correctly', () => { + const Component = () => { + const ref = useRef(); + + return ( + + ); + }; + const wrapper = shallow(); + + expect(wrapper.dive().find('[data-test-subj="rules-table"]')).toHaveLength(1); + }); + + it('renders rules tab when "selectedTab" is "rules"', () => { + const Component = () => { + const ref = useRef(); + + return ( + + ); + }; + const wrapper = shallow(); + + expect(wrapper.dive().find('[data-test-subj="rules-table"]')).toHaveLength(1); + expect(wrapper.dive().find('[data-test-subj="monitoring-table"]')).toHaveLength(0); + }); + + it('renders monitoring tab when "selectedTab" is "monitoring"', () => { + const Component = () => { + const ref = useRef(); + + return ( + + ); + }; + const wrapper = shallow(); + + expect(wrapper.dive().find('[data-test-subj="rules-table"]')).toHaveLength(0); + expect(wrapper.dive().find('[data-test-subj="monitoring-table"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/all_rules_tables/index.tsx b/x-pack/plugins/siem/public/alerts/components/rules/all_rules_tables/index.tsx new file mode 100644 index 00000000000000..8fd3f648bc812c --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/all_rules_tables/index.tsx @@ -0,0 +1,115 @@ +/* + * 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 { + EuiBasicTable, + EuiBasicTableColumn, + EuiEmptyPrompt, + Direction, + EuiTableSelectionType, +} from '@elastic/eui'; +import React, { useMemo, memo } from 'react'; +import styled from 'styled-components'; + +import { EuiBasicTableOnChange } from '../../../pages/detection_engine/rules/types'; +import * as i18n from '../../../pages/detection_engine/rules/translations'; +import { + RulesColumns, + RuleStatusRowItemType, +} from '../../../pages/detection_engine/rules/all/columns'; +import { Rule, Rules } from '../../../containers/detection_engine/rules/types'; +import { AllRulesTabs } from '../../../pages/detection_engine/rules/all'; + +// EuiBasicTable give me a hardtime with adding the ref attributes so I went the easy way +// after few hours of fight with typescript !!!! I lost :( +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const MyEuiBasicTable = styled(EuiBasicTable as any)`` as any; + +export interface SortingType { + sort: { + field: 'enabled'; + direction: Direction; + }; +} + +interface AllRulesTablesProps { + euiBasicTableSelectionProps: EuiTableSelectionType; + hasNoPermissions: boolean; + monitoringColumns: Array>; + pagination: { + pageIndex: number; + pageSize: number; + totalItemCount: number; + pageSizeOptions: number[]; + }; + rules: Rules; + rulesColumns: RulesColumns[]; + rulesStatuses: RuleStatusRowItemType[]; + sorting: { + sort: { + field: 'enabled'; + direction: Direction; + }; + }; + tableOnChangeCallback: ({ page, sort }: EuiBasicTableOnChange) => void; + tableRef?: React.MutableRefObject; + selectedTab: AllRulesTabs; +} + +export const AllRulesTablesComponent: React.FC = ({ + euiBasicTableSelectionProps, + hasNoPermissions, + monitoringColumns, + pagination, + rules, + rulesColumns, + rulesStatuses, + sorting, + tableOnChangeCallback, + tableRef, + selectedTab, +}) => { + const emptyPrompt = useMemo(() => { + return ( + {i18n.NO_RULES}} titleSize="xs" body={i18n.NO_RULES_BODY} /> + ); + }, []); + + return ( + <> + {selectedTab === AllRulesTabs.rules && ( + + )} + {selectedTab === AllRulesTabs.monitoring && ( + + )} + + ); +}; + +export const AllRulesTables = memo(AllRulesTablesComponent); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/anomaly_threshold_slider/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/rules/anomaly_threshold_slider/index.test.tsx new file mode 100644 index 00000000000000..5e65ff2fca59b3 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/anomaly_threshold_slider/index.test.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { AnomalyThresholdSlider } from './index'; +import { useFormFieldMock } from '../../../../common/mock'; + +describe('AnomalyThresholdSlider', () => { + it('renders correctly', () => { + const Component = () => { + const field = useFormFieldMock(); + + return ; + }; + const wrapper = shallow(); + + expect(wrapper.dive().find('EuiRange')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/anomaly_threshold_slider/index.tsx b/x-pack/plugins/siem/public/alerts/components/rules/anomaly_threshold_slider/index.tsx new file mode 100644 index 00000000000000..705626c77621c7 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/anomaly_threshold_slider/index.tsx @@ -0,0 +1,53 @@ +/* + * 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, { useCallback } from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiRange, EuiFormRow } from '@elastic/eui'; + +import { FieldHook } from '../../../../shared_imports'; + +interface AnomalyThresholdSliderProps { + describedByIds: string[]; + field: FieldHook; +} +type Event = React.ChangeEvent; +type EventArg = Event | React.MouseEvent; + +export const AnomalyThresholdSlider = ({ + describedByIds = [], + field, +}: AnomalyThresholdSliderProps) => { + const threshold = field.value as number; + const onThresholdChange = useCallback( + (event: EventArg) => { + const thresholdValue = Number((event as Event).target.value); + field.setValue(thresholdValue); + }, + [field] + ); + + return ( + + + + + + + + ); +}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/alerts/components/rules/description_step/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/alerts/components/rules/description_step/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/assets/list_tree_icon.svg b/x-pack/plugins/siem/public/alerts/components/rules/description_step/assets/list_tree_icon.svg similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/assets/list_tree_icon.svg rename to x-pack/plugins/siem/public/alerts/components/rules/description_step/assets/list_tree_icon.svg diff --git a/x-pack/plugins/siem/public/alerts/components/rules/description_step/helpers.test.tsx b/x-pack/plugins/siem/public/alerts/components/rules/description_step/helpers.test.tsx new file mode 100644 index 00000000000000..70de3d2a72dcc2 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/description_step/helpers.test.tsx @@ -0,0 +1,415 @@ +/* + * 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 { shallow } from 'enzyme'; +import { EuiLoadingSpinner } from '@elastic/eui'; + +import { coreMock } from '../../../../../../../../src/core/public/mocks'; +import { esFilters, FilterManager } from '../../../../../../../../src/plugins/data/public'; +import { SeverityBadge } from '../severity_badge'; + +import * as i18n from './translations'; +import { + isNotEmptyArray, + buildQueryBarDescription, + buildThreatDescription, + buildUnorderedListArrayDescription, + buildStringArrayDescription, + buildSeverityDescription, + buildUrlsDescription, + buildNoteDescription, + buildRuleTypeDescription, +} from './helpers'; +import { ListItems } from './types'; + +const setupMock = coreMock.createSetup(); +const uiSettingsMock = (pinnedByDefault: boolean) => (key: string) => { + switch (key) { + case 'filters:pinnedByDefault': + return pinnedByDefault; + default: + throw new Error(`Unexpected uiSettings key in FilterManager mock: ${key}`); + } +}; +setupMock.uiSettings.get.mockImplementation(uiSettingsMock(true)); +const mockFilterManager = new FilterManager(setupMock.uiSettings); + +const mockQueryBar = { + query: 'test query', + filters: [ + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ], + saved_id: 'test123', +}; + +describe('helpers', () => { + describe('isNotEmptyArray', () => { + test('returns false if empty array', () => { + const result = isNotEmptyArray([]); + expect(result).toBeFalsy(); + }); + + test('returns false if array of empty strings', () => { + const result = isNotEmptyArray(['', '']); + expect(result).toBeFalsy(); + }); + + test('returns true if array of string with space', () => { + const result = isNotEmptyArray([' ']); + expect(result).toBeTruthy(); + }); + + test('returns true if array with at least one non-empty string', () => { + const result = isNotEmptyArray(['', 'abc']); + expect(result).toBeTruthy(); + }); + }); + + describe('buildQueryBarDescription', () => { + test('returns empty array if no filters, query or savedId exist', () => { + const emptyMockQueryBar = { + query: '', + filters: [], + saved_id: '', + }; + const result: ListItems[] = buildQueryBarDescription({ + field: 'queryBar', + filters: emptyMockQueryBar.filters, + filterManager: mockFilterManager, + query: emptyMockQueryBar.query, + savedId: emptyMockQueryBar.saved_id, + }); + expect(result).toEqual([]); + }); + + test('returns expected array of ListItems when filters exists, but no indexPatterns passed in', () => { + const mockQueryBarWithFilters = { + ...mockQueryBar, + query: '', + saved_id: '', + }; + const result: ListItems[] = buildQueryBarDescription({ + field: 'queryBar', + filters: mockQueryBarWithFilters.filters, + filterManager: mockFilterManager, + query: mockQueryBarWithFilters.query, + savedId: mockQueryBarWithFilters.saved_id, + }); + const wrapper = shallow(result[0].description as React.ReactElement); + + expect(result[0].title).toEqual(<>{i18n.FILTERS_LABEL} ); + expect(wrapper.find(EuiLoadingSpinner).exists()).toBeTruthy(); + }); + + test('returns expected array of ListItems when filters AND indexPatterns exist', () => { + const mockQueryBarWithFilters = { + ...mockQueryBar, + query: '', + saved_id: '', + }; + const result: ListItems[] = buildQueryBarDescription({ + field: 'queryBar', + filters: mockQueryBarWithFilters.filters, + filterManager: mockFilterManager, + query: mockQueryBarWithFilters.query, + savedId: mockQueryBarWithFilters.saved_id, + indexPatterns: { fields: [{ name: 'test name', type: 'test type' }], title: 'test title' }, + }); + const wrapper = shallow(result[0].description as React.ReactElement); + const filterLabelComponent = wrapper.find(esFilters.FilterLabel).at(0); + + expect(result[0].title).toEqual(<>{i18n.FILTERS_LABEL} ); + expect(filterLabelComponent.prop('valueLabel')).toEqual('file'); + expect(filterLabelComponent.prop('filter')).toEqual(mockQueryBar.filters[0]); + }); + + test('returns expected array of ListItems when "query.query" exists', () => { + const mockQueryBarWithQuery = { + ...mockQueryBar, + filters: [], + saved_id: '', + }; + const result: ListItems[] = buildQueryBarDescription({ + field: 'queryBar', + filters: mockQueryBarWithQuery.filters, + filterManager: mockFilterManager, + query: mockQueryBarWithQuery.query, + savedId: mockQueryBarWithQuery.saved_id, + }); + expect(result[0].title).toEqual(<>{i18n.QUERY_LABEL} ); + expect(result[0].description).toEqual(<>{mockQueryBarWithQuery.query} ); + }); + + test('returns expected array of ListItems when "savedId" exists', () => { + const mockQueryBarWithSavedId = { + ...mockQueryBar, + query: '', + filters: [], + }; + const result: ListItems[] = buildQueryBarDescription({ + field: 'queryBar', + filters: mockQueryBarWithSavedId.filters, + filterManager: mockFilterManager, + query: mockQueryBarWithSavedId.query, + savedId: mockQueryBarWithSavedId.saved_id, + }); + expect(result[0].title).toEqual(<>{i18n.SAVED_ID_LABEL} ); + expect(result[0].description).toEqual(<>{mockQueryBarWithSavedId.saved_id} ); + }); + }); + + describe('buildThreatDescription', () => { + test('returns empty array if no threats', () => { + const result: ListItems[] = buildThreatDescription({ label: 'Mitre Attack', threat: [] }); + expect(result).toHaveLength(0); + }); + + test('returns empty tactic link if no corresponding tactic id found', () => { + const result: ListItems[] = buildThreatDescription({ + label: 'Mitre Attack', + threat: [ + { + framework: 'MITRE ATTACK', + technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }], + tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA000999' }, + }, + ], + }); + const wrapper = shallow(result[0].description as React.ReactElement); + expect(result[0].title).toEqual('Mitre Attack'); + expect(wrapper.find('[data-test-subj="threatTacticLink"]').text()).toEqual(''); + expect(wrapper.find('[data-test-subj="threatTechniqueLink"]').text()).toEqual( + 'Audio Capture (T1123)' + ); + }); + + test('returns empty technique link if no corresponding technique id found', () => { + const result: ListItems[] = buildThreatDescription({ + label: 'Mitre Attack', + threat: [ + { + framework: 'MITRE ATTACK', + technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123456' }], + tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA0009' }, + }, + ], + }); + const wrapper = shallow(result[0].description as React.ReactElement); + expect(result[0].title).toEqual('Mitre Attack'); + expect(wrapper.find('[data-test-subj="threatTacticLink"]').text()).toEqual( + 'Collection (TA0009)' + ); + expect(wrapper.find('[data-test-subj="threatTechniqueLink"]').text()).toEqual(''); + }); + + test('returns with corresponding tactic and technique link text', () => { + const result: ListItems[] = buildThreatDescription({ + label: 'Mitre Attack', + threat: [ + { + framework: 'MITRE ATTACK', + technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }], + tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA0009' }, + }, + ], + }); + const wrapper = shallow(result[0].description as React.ReactElement); + expect(result[0].title).toEqual('Mitre Attack'); + expect(wrapper.find('[data-test-subj="threatTacticLink"]').text()).toEqual( + 'Collection (TA0009)' + ); + expect(wrapper.find('[data-test-subj="threatTechniqueLink"]').text()).toEqual( + 'Audio Capture (T1123)' + ); + }); + + test('returns corresponding number of tactic and technique links', () => { + const result: ListItems[] = buildThreatDescription({ + label: 'Mitre Attack', + threat: [ + { + framework: 'MITRE ATTACK', + technique: [ + { reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }, + { reference: 'https://test.com', name: 'Clipboard Data', id: 'T1115' }, + ], + tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA0009' }, + }, + { + framework: 'MITRE ATTACK', + technique: [ + { reference: 'https://test.com', name: 'Automated Collection', id: 'T1119' }, + ], + tactic: { reference: 'https://test.com', name: 'Discovery', id: 'TA0007' }, + }, + ], + }); + const wrapper = shallow(result[0].description as React.ReactElement); + + expect(wrapper.find('[data-test-subj="threatTacticLink"]')).toHaveLength(2); + expect(wrapper.find('[data-test-subj="threatTechniqueLink"]')).toHaveLength(3); + }); + }); + + describe('buildUnorderedListArrayDescription', () => { + test('returns empty array if "values" is empty array', () => { + const result: ListItems[] = buildUnorderedListArrayDescription( + 'Test label', + 'falsePositives', + [] + ); + expect(result).toHaveLength(0); + }); + + test('returns ListItem with corresponding number of valid values items', () => { + const result: ListItems[] = buildUnorderedListArrayDescription( + 'Test label', + 'falsePositives', + ['', 'falsePositive1', 'falsePositive2'] + ); + const wrapper = shallow(result[0].description as React.ReactElement); + + expect(result[0].title).toEqual('Test label'); + expect(wrapper.find('[data-test-subj="unorderedListArrayDescriptionItem"]')).toHaveLength(2); + }); + }); + + describe('buildStringArrayDescription', () => { + test('returns empty array if "values" is empty array', () => { + const result: ListItems[] = buildStringArrayDescription('Test label', 'tags', []); + expect(result).toHaveLength(0); + }); + + test('returns ListItem with corresponding number of valid values items', () => { + const result: ListItems[] = buildStringArrayDescription('Test label', 'tags', [ + '', + 'tag1', + 'tag2', + ]); + const wrapper = shallow(result[0].description as React.ReactElement); + + expect(result[0].title).toEqual('Test label'); + expect(wrapper.find('[data-test-subj="stringArrayDescriptionBadgeItem"]')).toHaveLength(2); + expect( + wrapper + .find('[data-test-subj="stringArrayDescriptionBadgeItem"]') + .first() + .text() + ).toEqual('tag1'); + expect( + wrapper + .find('[data-test-subj="stringArrayDescriptionBadgeItem"]') + .at(1) + .text() + ).toEqual('tag2'); + }); + }); + + describe('buildSeverityDescription', () => { + test('returns ListItem with passed in label and SeverityBadge component', () => { + const result: ListItems[] = buildSeverityDescription('Test label', 'Test description value'); + + expect(result[0].title).toEqual('Test label'); + expect(result[0].description).toEqual(); + }); + }); + + describe('buildUrlsDescription', () => { + test('returns empty array if "values" is empty array', () => { + const result: ListItems[] = buildUrlsDescription('Test label', []); + expect(result).toHaveLength(0); + }); + + test('returns ListItem with corresponding number of valid values items', () => { + const result: ListItems[] = buildUrlsDescription('Test label', [ + 'www.test.com', + 'www.test2.com', + ]); + const wrapper = shallow(result[0].description as React.ReactElement); + + expect(result[0].title).toEqual('Test label'); + expect(wrapper.find('[data-test-subj="urlsDescriptionReferenceLinkItem"]')).toHaveLength(2); + expect( + wrapper + .find('[data-test-subj="urlsDescriptionReferenceLinkItem"]') + .first() + .text() + ).toEqual('www.test.com'); + expect( + wrapper + .find('[data-test-subj="urlsDescriptionReferenceLinkItem"]') + .at(1) + .text() + ).toEqual('www.test2.com'); + }); + }); + + describe('buildNoteDescription', () => { + test('returns ListItem with passed in label and note content', () => { + const noteSample = + 'Cras mattism. [Pellentesque](https://elastic.co). ### Malesuada adipiscing tristique'; + const result: ListItems[] = buildNoteDescription('Test label', noteSample); + const wrapper = shallow(result[0].description as React.ReactElement); + const noteElement = wrapper.find('[data-test-subj="noteDescriptionItem"]').at(0); + + expect(result[0].title).toEqual('Test label'); + expect(noteElement.exists()).toBeTruthy(); + expect(noteElement.text()).toEqual(noteSample); + }); + + test('returns empty array if passed in note is empty string', () => { + const result: ListItems[] = buildNoteDescription('Test label', ''); + + expect(result).toHaveLength(0); + }); + }); + + describe('buildRuleTypeDescription', () => { + it('returns the label for a machine_learning type', () => { + const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'machine_learning'); + + expect(result.title).toEqual('Test label'); + }); + + it('returns a humanized description for a machine_learning type', () => { + const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'machine_learning'); + + expect(result.description).toEqual('Machine Learning'); + }); + + it('returns the label for a query type', () => { + const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'query'); + + expect(result.title).toEqual('Test label'); + }); + + it('returns a humanized description for a query type', () => { + const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'query'); + + expect(result.description).toEqual('Query'); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/description_step/helpers.tsx b/x-pack/plugins/siem/public/alerts/components/rules/description_step/helpers.tsx new file mode 100644 index 00000000000000..ad3ed538c875b7 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/description_step/helpers.tsx @@ -0,0 +1,294 @@ +/* + * 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 { + EuiBadge, + EuiLoadingSpinner, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiSpacer, + EuiLink, + EuiText, +} from '@elastic/eui'; + +import { isEmpty } from 'lodash/fp'; +import React from 'react'; +import styled from 'styled-components'; + +import { RuleType } from '../../../../../common/detection_engine/types'; +import { esFilters } from '../../../../../../../../src/plugins/data/public'; + +import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_techniques'; + +import * as i18n from './translations'; +import { BuildQueryBarDescription, BuildThreatDescription, ListItems } from './types'; +import { SeverityBadge } from '../severity_badge'; +import ListTreeIcon from './assets/list_tree_icon.svg'; +import { assertUnreachable } from '../../../../common/lib/helpers'; + +const NoteDescriptionContainer = styled(EuiFlexItem)` + height: 105px; + overflow-y: hidden; +`; + +export const isNotEmptyArray = (values: string[]) => !isEmpty(values.join('')); + +const EuiBadgeWrap = (styled(EuiBadge)` + .euiBadge__text { + white-space: pre-wrap !important; + } +` as unknown) as typeof EuiBadge; + +export const buildQueryBarDescription = ({ + field, + filters, + filterManager, + query, + savedId, + indexPatterns, +}: BuildQueryBarDescription): ListItems[] => { + let items: ListItems[] = []; + if (!isEmpty(filters)) { + filterManager.setFilters(filters); + items = [ + ...items, + { + title: <>{i18n.FILTERS_LABEL} , + description: ( + + {filterManager.getFilters().map((filter, index) => ( + + + {indexPatterns != null ? ( + + ) : ( + + )} + + + ))} + + ), + }, + ]; + } + if (!isEmpty(query)) { + items = [ + ...items, + { + title: <>{i18n.QUERY_LABEL} , + description: <>{query} , + }, + ]; + } + if (!isEmpty(savedId)) { + items = [ + ...items, + { + title: <>{i18n.SAVED_ID_LABEL} , + description: <>{savedId} , + }, + ]; + } + return items; +}; + +const ThreatEuiFlexGroup = styled(EuiFlexGroup)` + .euiFlexItem { + margin-bottom: 0px; + } +`; + +const TechniqueLinkItem = styled(EuiButtonEmpty)` + .euiIcon { + width: 8px; + height: 8px; + } +`; + +export const buildThreatDescription = ({ label, threat }: BuildThreatDescription): ListItems[] => { + if (threat.length > 0) { + return [ + { + title: label, + description: ( + + {threat.map((singleThreat, index) => { + const tactic = tacticsOptions.find(t => t.id === singleThreat.tactic.id); + return ( + + + {tactic != null ? tactic.text : ''} + + + {singleThreat.technique.map(technique => { + const myTechnique = techniquesOptions.find(t => t.id === technique.id); + return ( + + + {myTechnique != null ? myTechnique.label : ''} + + + ); + })} + + + ); + })} + + + ), + }, + ]; + } + return []; +}; + +export const buildUnorderedListArrayDescription = ( + label: string, + field: string, + values: string[] +): ListItems[] => { + if (isNotEmptyArray(values)) { + return [ + { + title: label, + description: ( + +
    + {values.map(val => + isEmpty(val) ? null : ( +
  • + {val} +
  • + ) + )} +
+
+ ), + }, + ]; + } + return []; +}; + +export const buildStringArrayDescription = ( + label: string, + field: string, + values: string[] +): ListItems[] => { + if (isNotEmptyArray(values)) { + return [ + { + title: label, + description: ( + + {values.map((val: string) => + isEmpty(val) ? null : ( + + + {val} + + + ) + )} + + ), + }, + ]; + } + return []; +}; + +export const buildSeverityDescription = (label: string, value: string): ListItems[] => [ + { + title: label, + description: , + }, +]; + +export const buildUrlsDescription = (label: string, values: string[]): ListItems[] => { + if (isNotEmptyArray(values)) { + return [ + { + title: label, + description: ( + +
    + {values + .filter(v => !isEmpty(v)) + .map((val, index) => ( +
  • + + {val} + +
  • + ))} +
+
+ ), + }, + ]; + } + return []; +}; + +export const buildNoteDescription = (label: string, note: string): ListItems[] => { + if (note.trim() !== '') { + return [ + { + title: label, + description: ( + +
+ {note} +
+
+ ), + }, + ]; + } + return []; +}; + +export const buildRuleTypeDescription = (label: string, ruleType: RuleType): ListItems[] => { + switch (ruleType) { + case 'machine_learning': { + return [ + { + title: label, + description: i18n.ML_TYPE_DESCRIPTION, + }, + ]; + } + case 'query': + case 'saved_query': { + return [ + { + title: label, + description: i18n.QUERY_TYPE_DESCRIPTION, + }, + ]; + } + default: + return assertUnreachable(ruleType); + } +}; diff --git a/x-pack/plugins/siem/public/alerts/components/rules/description_step/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/rules/description_step/index.test.tsx new file mode 100644 index 00000000000000..0cd79f1db78a03 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/description_step/index.test.tsx @@ -0,0 +1,474 @@ +/* + * 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 { shallow } from 'enzyme'; + +import { + StepRuleDescriptionComponent, + addFilterStateIfNotThere, + buildListItems, + getDescriptionItem, +} from '.'; + +import { esFilters, Filter, FilterManager } from '../../../../../../../../src/plugins/data/public'; +import { + mockAboutStepRule, + mockDefineStepRule, +} from '../../../pages/detection_engine/rules/all/__mocks__/mock'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { coreMock } from '../../../../../../../../src/core/public/mocks'; +import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timeline/translations'; +import * as i18n from './translations'; + +import { schema } from '../step_about_rule/schema'; +import { ListItems } from './types'; +import { AboutStepRule } from '../../../pages/detection_engine/rules/types'; + +jest.mock('../../../../common/lib/kibana'); + +describe('description_step', () => { + const setupMock = coreMock.createSetup(); + const uiSettingsMock = (pinnedByDefault: boolean) => (key: string) => { + switch (key) { + case 'filters:pinnedByDefault': + return pinnedByDefault; + default: + throw new Error(`Unexpected uiSettings key in FilterManager mock: ${key}`); + } + }; + let mockFilterManager: FilterManager; + let mockAboutStep: AboutStepRule; + + beforeEach(() => { + setupMock.uiSettings.get.mockImplementation(uiSettingsMock(true)); + mockFilterManager = new FilterManager(setupMock.uiSettings); + mockAboutStep = mockAboutStepRule(); + }); + + describe('StepRuleDescriptionComponent', () => { + test('renders correctly against snapshot when columns is "multi"', () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(2); + }); + + test('renders correctly against snapshot when columns is "single"', () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(1); + }); + + test('renders correctly against snapshot when columns is "singleSplit', () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(1); + expect( + wrapper + .find('[data-test-subj="singleSplitStepRuleDescriptionList"]') + .at(0) + .prop('type') + ).toEqual('column'); + }); + }); + + describe('addFilterStateIfNotThere', () => { + test('it does not change the state if it is global', () => { + const filters: Filter[] = [ + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ]; + const output = addFilterStateIfNotThere(filters); + const expected: Filter[] = [ + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ]; + expect(output).toEqual(expected); + }); + + test('it adds the state if it does not exist as local', () => { + const filters: Filter[] = [ + { + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + { + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ]; + const output = addFilterStateIfNotThere(filters); + const expected: Filter[] = [ + { + $state: { + store: esFilters.FilterStateStore.APP_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + { + $state: { + store: esFilters.FilterStateStore.APP_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ]; + expect(output).toEqual(expected); + }); + }); + + describe('buildListItems', () => { + test('returns expected ListItems array when given valid inputs', () => { + const result: ListItems[] = buildListItems(mockAboutStep, schema, mockFilterManager); + + expect(result.length).toEqual(9); + }); + }); + + describe('getDescriptionItem', () => { + test('returns ListItem with all values enumerated when value[field] is an array', () => { + const result: ListItems[] = getDescriptionItem( + 'tags', + 'Tags label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Tags label'); + expect(typeof result[0].description).toEqual('object'); + }); + + test('returns ListItem with description of value[field] when value[field] is a string', () => { + const result: ListItems[] = getDescriptionItem( + 'description', + 'Description label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Description label'); + expect(result[0].description).toEqual('24/7'); + }); + + test('returns empty array when "value" is a non-existant property in "field"', () => { + const result: ListItems[] = getDescriptionItem( + 'jibberjabber', + 'JibberJabber label', + mockAboutStep, + mockFilterManager + ); + + expect(result.length).toEqual(0); + }); + + describe('queryBar', () => { + test('returns array of ListItems when queryBar exist', () => { + const mockQueryBar = { + isNew: false, + queryBar: { + query: { + query: 'user.name: root or user.name: admin', + language: 'kuery', + }, + filters: null, + saved_id: null, + }, + }; + const result: ListItems[] = getDescriptionItem( + 'queryBar', + 'Query bar label', + mockQueryBar, + mockFilterManager + ); + + expect(result[0].title).toEqual(<>{i18n.QUERY_LABEL} ); + expect(result[0].description).toEqual(<>{mockQueryBar.queryBar.query.query} ); + }); + }); + + describe('threat', () => { + test('returns array of ListItems when threat exist', () => { + const result: ListItems[] = getDescriptionItem( + 'threat', + 'Threat label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Threat label'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + }); + + test('filters out threats with tactic.name of "none"', () => { + const mockStep = { + ...mockAboutStep, + threat: [ + { + framework: 'mockFramework', + tactic: { + id: '1234', + name: 'none', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + }; + const result: ListItems[] = getDescriptionItem( + 'threat', + 'Threat label', + mockStep, + mockFilterManager + ); + + expect(result.length).toEqual(0); + }); + }); + + describe('references', () => { + test('returns array of ListItems when references exist', () => { + const result: ListItems[] = getDescriptionItem( + 'references', + 'Reference label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Reference label'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + }); + }); + + describe('falsePositives', () => { + test('returns array of ListItems when falsePositives exist', () => { + const result: ListItems[] = getDescriptionItem( + 'falsePositives', + 'False positives label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('False positives label'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + }); + }); + + describe('severity', () => { + test('returns array of ListItems when severity exist', () => { + const result: ListItems[] = getDescriptionItem( + 'severity', + 'Severity label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Severity label'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + }); + }); + + describe('riskScore', () => { + test('returns array of ListItems when riskScore exist', () => { + const result: ListItems[] = getDescriptionItem( + 'riskScore', + 'Risk score label', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Risk score label'); + expect(result[0].description).toEqual(21); + }); + }); + + describe('timeline', () => { + test('returns timeline title if one exists', () => { + const mockDefineStep = mockDefineStepRule(); + const result: ListItems[] = getDescriptionItem( + 'timeline', + 'Timeline label', + mockDefineStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Timeline label'); + expect(result[0].description).toEqual('Titled timeline'); + }); + + test('returns default timeline title if none exists', () => { + const mockStep = { + ...mockDefineStepRule(), + timeline: { + id: '12345', + }, + }; + const result: ListItems[] = getDescriptionItem( + 'timeline', + 'Timeline label', + mockStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Timeline label'); + expect(result[0].description).toEqual(DEFAULT_TIMELINE_TITLE); + }); + }); + + describe('note', () => { + test('returns default "note" description', () => { + const result: ListItems[] = getDescriptionItem( + 'note', + 'Investigation guide', + mockAboutStep, + mockFilterManager + ); + + expect(result[0].title).toEqual('Investigation guide'); + expect(React.isValidElement(result[0].description)).toBeTruthy(); + }); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/description_step/index.tsx b/x-pack/plugins/siem/public/alerts/components/rules/description_step/index.tsx new file mode 100644 index 00000000000000..86fe128b0a4fb7 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/description_step/index.tsx @@ -0,0 +1,205 @@ +/* + * 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 { EuiDescriptionList, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { isEmpty, chunk, get, pick, isNumber } from 'lodash/fp'; +import React, { memo, useState } from 'react'; +import styled from 'styled-components'; + +import { RuleType } from '../../../../../common/detection_engine/types'; +import { + IIndexPattern, + Filter, + esFilters, + FilterManager, +} from '../../../../../../../../src/plugins/data/public'; +import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timeline/translations'; +import { useKibana } from '../../../../common/lib/kibana'; +import { IMitreEnterpriseAttack } from '../../../pages/detection_engine/rules/types'; +import { FieldValueTimeline } from '../pick_timeline'; +import { FormSchema } from '../../../../shared_imports'; +import { ListItems } from './types'; +import { + buildQueryBarDescription, + buildSeverityDescription, + buildStringArrayDescription, + buildThreatDescription, + buildUnorderedListArrayDescription, + buildUrlsDescription, + buildNoteDescription, + buildRuleTypeDescription, +} from './helpers'; +import { useSiemJobs } from '../../../../common/components/ml_popover/hooks/use_siem_jobs'; +import { buildMlJobDescription } from './ml_job_description'; + +const DescriptionListContainer = styled(EuiDescriptionList)` + &.euiDescriptionList--column .euiDescriptionList__title { + width: 30%; + } + &.euiDescriptionList--column .euiDescriptionList__description { + width: 70%; + } +`; + +interface StepRuleDescriptionProps { + columns?: 'multi' | 'single' | 'singleSplit'; + data: unknown; + indexPatterns?: IIndexPattern; + schema: FormSchema; +} + +export const StepRuleDescriptionComponent: React.FC = ({ + data, + columns = 'multi', + indexPatterns, + schema, +}) => { + const kibana = useKibana(); + const [filterManager] = useState(new FilterManager(kibana.services.uiSettings)); + const [, siemJobs] = useSiemJobs(true); + + const keys = Object.keys(schema); + const listItems = keys.reduce((acc: ListItems[], key: string) => { + if (key === 'machineLearningJobId') { + return [ + ...acc, + buildMlJobDescription( + get(key, data) as string, + (get(key, schema) as { label: string }).label, + siemJobs + ), + ]; + } + return [...acc, ...buildListItems(data, pick(key, schema), filterManager, indexPatterns)]; + }, []); + + if (columns === 'multi') { + return ( + + {chunk(Math.ceil(listItems.length / 2), listItems).map((chunkListItems, index) => ( + + + + ))} + + ); + } + + return ( + + + {columns === 'single' ? ( + + ) : ( + + )} + + + ); +}; + +export const StepRuleDescription = memo(StepRuleDescriptionComponent); + +export const buildListItems = ( + data: unknown, + schema: FormSchema, + filterManager: FilterManager, + indexPatterns?: IIndexPattern +): ListItems[] => + Object.keys(schema).reduce( + (acc, field) => [ + ...acc, + ...getDescriptionItem( + field, + get([field, 'label'], schema), + data, + filterManager, + indexPatterns + ), + ], + [] + ); + +export const addFilterStateIfNotThere = (filters: Filter[]): Filter[] => { + return filters.map(filter => { + if (filter.$state == null) { + return { $state: { store: esFilters.FilterStateStore.APP_STATE }, ...filter }; + } else { + return filter; + } + }); +}; + +export const getDescriptionItem = ( + field: string, + label: string, + data: unknown, + filterManager: FilterManager, + indexPatterns?: IIndexPattern +): ListItems[] => { + if (field === 'queryBar') { + const filters = addFilterStateIfNotThere(get('queryBar.filters', data) ?? []); + const query = get('queryBar.query.query', data); + const savedId = get('queryBar.saved_id', data); + return buildQueryBarDescription({ + field, + filters, + filterManager, + query, + savedId, + indexPatterns, + }); + } else if (field === 'threat') { + const threat: IMitreEnterpriseAttack[] = get(field, data).filter( + (singleThreat: IMitreEnterpriseAttack) => singleThreat.tactic.name !== 'none' + ); + return buildThreatDescription({ label, threat }); + } else if (field === 'references') { + const urls: string[] = get(field, data); + return buildUrlsDescription(label, urls); + } else if (field === 'falsePositives') { + const values: string[] = get(field, data); + return buildUnorderedListArrayDescription(label, field, values); + } else if (Array.isArray(get(field, data))) { + const values: string[] = get(field, data); + return buildStringArrayDescription(label, field, values); + } else if (field === 'severity') { + const val: string = get(field, data); + return buildSeverityDescription(label, val); + } else if (field === 'timeline') { + const timeline = get(field, data) as FieldValueTimeline; + return [ + { + title: label, + description: timeline.title ?? DEFAULT_TIMELINE_TITLE, + }, + ]; + } else if (field === 'note') { + const val: string = get(field, data); + return buildNoteDescription(label, val); + } else if (field === 'ruleType') { + const ruleType: RuleType = get(field, data); + return buildRuleTypeDescription(label, ruleType); + } + + const description: string = get(field, data); + if (isNumber(description) || !isEmpty(description)) { + return [ + { + title: label, + description, + }, + ]; + } + return []; +}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.test.tsx b/x-pack/plugins/siem/public/alerts/components/rules/description_step/ml_job_description.test.tsx similarity index 97% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.test.tsx rename to x-pack/plugins/siem/public/alerts/components/rules/description_step/ml_job_description.test.tsx index 59231c31d15bb4..c82a465f08c3a7 100644 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.test.tsx +++ b/x-pack/plugins/siem/public/alerts/components/rules/description_step/ml_job_description.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { MlJobDescription, AuditIcon, JobStatusBadge } from './ml_job_description'; -jest.mock('../../../../../lib/kibana'); +jest.mock('../../../../common/lib/kibana'); const job = { moduleId: 'moduleId', diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.tsx b/x-pack/plugins/siem/public/alerts/components/rules/description_step/ml_job_description.tsx similarity index 92% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.tsx rename to x-pack/plugins/siem/public/alerts/components/rules/description_step/ml_job_description.tsx index 79993c37e549c3..c5df8b1a3db706 100644 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/ml_job_description.tsx +++ b/x-pack/plugins/siem/public/alerts/components/rules/description_step/ml_job_description.tsx @@ -8,9 +8,9 @@ import React from 'react'; import styled from 'styled-components'; import { EuiBadge, EuiIcon, EuiLink, EuiToolTip } from '@elastic/eui'; -import { isJobStarted } from '../../../../../../common/detection_engine/ml_helpers'; -import { useKibana } from '../../../../../lib/kibana'; -import { SiemJob } from '../../../../../components/ml_popover/types'; +import { isJobStarted } from '../../../../../common/machine_learning/helpers'; +import { useKibana } from '../../../../common/lib/kibana'; +import { SiemJob } from '../../../../common/components/ml_popover/types'; import { ListItems } from './types'; import { ML_JOB_STARTED, ML_JOB_STOPPED } from './translations'; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/translations.tsx b/x-pack/plugins/siem/public/alerts/components/rules/description_step/translations.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/translations.tsx rename to x-pack/plugins/siem/public/alerts/components/rules/description_step/translations.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/description_step/types.ts b/x-pack/plugins/siem/public/alerts/components/rules/description_step/types.ts new file mode 100644 index 00000000000000..bcda5ff67a9a62 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/description_step/types.ts @@ -0,0 +1,32 @@ +/* + * 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 { ReactNode } from 'react'; + +import { + IIndexPattern, + Filter, + FilterManager, +} from '../../../../../../../../src/plugins/data/public'; +import { IMitreEnterpriseAttack } from '../../../pages/detection_engine/rules/types'; + +export interface ListItems { + title: NonNullable; + description: NonNullable; +} + +export interface BuildQueryBarDescription { + field: string; + filters: Filter[]; + filterManager: FilterManager; + query: string; + savedId: string; + indexPatterns?: IIndexPattern; +} + +export interface BuildThreatDescription { + label: string; + threat: IMitreEnterpriseAttack[]; +} diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/mitre/helpers.test.tsx b/x-pack/plugins/siem/public/alerts/components/rules/mitre/helpers.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/mitre/helpers.test.tsx rename to x-pack/plugins/siem/public/alerts/components/rules/mitre/helpers.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/mitre/helpers.ts b/x-pack/plugins/siem/public/alerts/components/rules/mitre/helpers.ts new file mode 100644 index 00000000000000..2dc7a6d8f45e59 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/mitre/helpers.ts @@ -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. + */ +import { isEmpty } from 'lodash/fp'; + +import { IMitreAttack } from '../../../pages/detection_engine/rules/types'; + +export const isMitreAttackInvalid = ( + tacticName: string | null | undefined, + technique: IMitreAttack[] | null | undefined +) => { + if (isEmpty(tacticName) || (tacticName !== 'none' && isEmpty(technique))) { + return true; + } + return false; +}; diff --git a/x-pack/plugins/siem/public/alerts/components/rules/mitre/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/rules/mitre/index.test.tsx new file mode 100644 index 00000000000000..ecf1bda807b68c --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/mitre/index.test.tsx @@ -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 React from 'react'; +import { shallow } from 'enzyme'; + +import { AddMitreThreat } from './index'; +import { useFormFieldMock } from '../../../../common/mock'; + +describe('AddMitreThreat', () => { + it('renders correctly', () => { + const Component = () => { + const field = useFormFieldMock(); + + return ( + + ); + }; + const wrapper = shallow(); + + expect(wrapper.dive().find('[data-test-subj="addMitre"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/mitre/index.tsx b/x-pack/plugins/siem/public/alerts/components/rules/mitre/index.tsx new file mode 100644 index 00000000000000..4170ce5ebeabd7 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/mitre/index.tsx @@ -0,0 +1,218 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiButtonIcon, + EuiFormRow, + EuiSuperSelect, + EuiSpacer, + EuiFlexGroup, + EuiFlexItem, + EuiComboBox, + EuiText, +} from '@elastic/eui'; +import { isEmpty, kebabCase, camelCase } from 'lodash/fp'; +import React, { useCallback, useState } from 'react'; +import styled from 'styled-components'; + +import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_techniques'; +import * as Rulei18n from '../../../pages/detection_engine/rules/translations'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../shared_imports'; +import { threatDefault } from '../step_about_rule/default_value'; +import { IMitreEnterpriseAttack } from '../../../pages/detection_engine/rules/types'; +import { MyAddItemButton } from '../add_item_form'; +import { isMitreAttackInvalid } from './helpers'; +import * as i18n from './translations'; + +const MitreContainer = styled.div` + margin-top: 16px; +`; +const MyEuiSuperSelect = styled(EuiSuperSelect)` + width: 280px; +`; +interface AddItemProps { + field: FieldHook; + dataTestSubj: string; + idAria: string; + isDisabled: boolean; +} + +export const AddMitreThreat = ({ dataTestSubj, field, idAria, isDisabled }: AddItemProps) => { + const [showValidation, setShowValidation] = useState(false); + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + + const removeItem = useCallback( + (index: number) => { + const values = field.value as string[]; + const newValues = [...values.slice(0, index), ...values.slice(index + 1)]; + if (isEmpty(newValues)) { + field.setValue(threatDefault); + } else { + field.setValue(newValues); + } + }, + [field] + ); + + const addItem = useCallback(() => { + const values = field.value as IMitreEnterpriseAttack[]; + if (!isEmpty(values[values.length - 1])) { + field.setValue([ + ...values, + { tactic: { id: 'none', name: 'none', reference: 'none' }, technique: [] }, + ]); + } else { + field.setValue([{ tactic: { id: 'none', name: 'none', reference: 'none' }, technique: [] }]); + } + }, [field]); + + const updateTactic = useCallback( + (index: number, value: string) => { + const values = field.value as IMitreEnterpriseAttack[]; + const { id, reference, name } = tacticsOptions.find(t => t.value === value) || { + id: '', + name: '', + reference: '', + }; + field.setValue([ + ...values.slice(0, index), + { + ...values[index], + tactic: { id, reference, name }, + technique: [], + }, + ...values.slice(index + 1), + ]); + }, + [field] + ); + + const updateTechniques = useCallback( + (index: number, selectedOptions: unknown[]) => { + field.setValue([ + ...values.slice(0, index), + { + ...values[index], + technique: selectedOptions, + }, + ...values.slice(index + 1), + ]); + }, + [field] + ); + + const values = field.value as IMitreEnterpriseAttack[]; + + const getSelectTactic = (tacticName: string, index: number, disabled: boolean) => ( + {i18n.TACTIC_PLACEHOLDER}, + value: 'none', + disabled, + }, + ] + : []), + ...tacticsOptions.map(t => ({ + inputDisplay: <>{t.text}, + value: t.value, + disabled, + })), + ]} + aria-label="" + onChange={updateTactic.bind(null, index)} + fullWidth={false} + valueOfSelected={camelCase(tacticName)} + data-test-subj="mitreTactic" + /> + ); + + const getSelectTechniques = (item: IMitreEnterpriseAttack, index: number, disabled: boolean) => { + const invalid = isMitreAttackInvalid(item.tactic.name, item.technique); + const options = techniquesOptions.filter(t => t.tactics.includes(kebabCase(item.tactic.name))); + const selectedOptions = item.technique.map(technic => ({ + ...technic, + label: `${technic.name} (${technic.id})`, // API doesn't allow for label field + })); + + return ( + + + setShowValidation(true)} + /> + {showValidation && invalid && ( + +

{errorMessage}

+
+ )} +
+ + removeItem(index)} + aria-label={Rulei18n.DELETE} + /> + +
+ ); + }; + + return ( + + {values.map((item, index) => ( +
+ + + {index === 0 ? ( + + <>{getSelectTactic(item.tactic.name, index, isDisabled)} + + ) : ( + getSelectTactic(item.tactic.name, index, isDisabled) + )} + + + {index === 0 ? ( + + <>{getSelectTechniques(item, index, isDisabled)} + + ) : ( + getSelectTechniques(item, index, isDisabled) + )} + + + {values.length - 1 !== index && } +
+ ))} + + {i18n.ADD_MITRE_ATTACK} + +
+ ); +}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/mitre/translations.ts b/x-pack/plugins/siem/public/alerts/components/rules/mitre/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/mitre/translations.ts rename to x-pack/plugins/siem/public/alerts/components/rules/mitre/translations.ts diff --git a/x-pack/plugins/siem/public/alerts/components/rules/ml_job_select/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/rules/ml_job_select/index.test.tsx new file mode 100644 index 00000000000000..6f6581e4de1c37 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/ml_job_select/index.test.tsx @@ -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 React from 'react'; +import { shallow } from 'enzyme'; + +import { MlJobSelect } from './index'; +import { useSiemJobs } from '../../../../common/components/ml_popover/hooks/use_siem_jobs'; +import { useFormFieldMock } from '../../../../common/mock'; +jest.mock('../../../../common/components/ml_popover/hooks/use_siem_jobs'); +jest.mock('../../../../common/lib/kibana'); + +describe('MlJobSelect', () => { + beforeAll(() => { + (useSiemJobs as jest.Mock).mockReturnValue([false, []]); + }); + + it('renders correctly', () => { + const Component = () => { + const field = useFormFieldMock(); + + return ; + }; + const wrapper = shallow(); + + expect(wrapper.dive().find('[data-test-subj="mlJobSelect"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/ml_job_select/index.tsx b/x-pack/plugins/siem/public/alerts/components/rules/ml_job_select/index.tsx new file mode 100644 index 00000000000000..d3b6de6cf0c405 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/ml_job_select/index.tsx @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, useMemo } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiIcon, + EuiLink, + EuiSuperSelect, + EuiText, +} from '@elastic/eui'; + +import styled from 'styled-components'; +import { isJobStarted } from '../../../../../common/machine_learning/helpers'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../shared_imports'; +import { useSiemJobs } from '../../../../common/components/ml_popover/hooks/use_siem_jobs'; +import { useKibana } from '../../../../common/lib/kibana'; +import { + ML_JOB_SELECT_PLACEHOLDER_TEXT, + ENABLE_ML_JOB_WARNING, +} from '../step_define_rule/translations'; + +const HelpTextWarningContainer = styled.div` + margin-top: 10px; +`; + +const MlJobSelectEuiFlexGroup = styled(EuiFlexGroup)` + margin-bottom: 5px; +`; + +const HelpText: React.FC<{ href: string; showEnableWarning: boolean }> = ({ + href, + showEnableWarning = false, +}) => ( + <> + + + + ), + }} + /> + {showEnableWarning && ( + + + + {ENABLE_ML_JOB_WARNING} + + + )} + +); + +const JobDisplay: React.FC<{ title: string; description: string }> = ({ title, description }) => ( + <> + {title} + +

{description}

+
+ +); + +interface MlJobSelectProps { + describedByIds: string[]; + field: FieldHook; +} + +export const MlJobSelect: React.FC = ({ describedByIds = [], field }) => { + const jobId = field.value as string; + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + const [isLoading, siemJobs] = useSiemJobs(false); + const mlUrl = useKibana().services.application.getUrlForApp('ml'); + const handleJobChange = useCallback( + (machineLearningJobId: string) => { + field.setValue(machineLearningJobId); + }, + [field] + ); + const placeholderOption = { + value: 'placeholder', + inputDisplay: ML_JOB_SELECT_PLACEHOLDER_TEXT, + dropdownDisplay: ML_JOB_SELECT_PLACEHOLDER_TEXT, + disabled: true, + }; + + const jobOptions = siemJobs.map(job => ({ + value: job.id, + inputDisplay: job.id, + dropdownDisplay: , + })); + + const options = [placeholderOption, ...jobOptions]; + + const isJobRunning = useMemo(() => { + // If the selected job is not found in the list, it means the placeholder is selected + // and so we don't want to show the warning, thus isJobRunning will be true when 'job == null' + const job = siemJobs.find(j => j.id === jobId); + return job == null || isJobStarted(job.jobState, job.datafeedState); + }, [siemJobs, jobId]); + + return ( + + + } + isInvalid={isInvalid} + error={errorMessage} + data-test-subj="mlJobSelect" + describedByIds={describedByIds} + > + + + + + + + + + ); +}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/next_step/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/alerts/components/rules/next_step/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/next_step/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/alerts/components/rules/next_step/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/next_step/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/rules/next_step/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/next_step/index.test.tsx rename to x-pack/plugins/siem/public/alerts/components/rules/next_step/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/next_step/index.tsx b/x-pack/plugins/siem/public/alerts/components/rules/next_step/index.tsx new file mode 100644 index 00000000000000..d97c2b4c8c0aa4 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/next_step/index.tsx @@ -0,0 +1,32 @@ +/* + * 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 { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; +import * as RuleI18n from '../../../pages/detection_engine/rules/translations'; + +interface NextStepProps { + onClick: () => Promise; + isDisabled: boolean; + dataTestSubj?: string; +} + +export const NextStep = React.memo( + ({ onClick, isDisabled, dataTestSubj = 'nextStep-continue' }) => ( + <> + + + + + {RuleI18n.CONTINUE} + + + + + ) +); + +NextStep.displayName = 'NextStep'; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/optional_field_label/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/rules/optional_field_label/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/optional_field_label/index.test.tsx rename to x-pack/plugins/siem/public/alerts/components/rules/optional_field_label/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/optional_field_label/index.tsx b/x-pack/plugins/siem/public/alerts/components/rules/optional_field_label/index.tsx new file mode 100644 index 00000000000000..0d144e30cbaba9 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/optional_field_label/index.tsx @@ -0,0 +1,16 @@ +/* + * 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 { EuiText } from '@elastic/eui'; +import React from 'react'; + +import * as RuleI18n from '../../../pages/detection_engine/rules/translations'; + +export const OptionalFieldLabel = ( + + {RuleI18n.OPTIONAL_FIELD} + +); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/pick_timeline/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/rules/pick_timeline/index.test.tsx new file mode 100644 index 00000000000000..379a8a48e1ad7a --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/pick_timeline/index.test.tsx @@ -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 React from 'react'; +import { shallow } from 'enzyme'; + +import { PickTimeline } from './index'; +import { useFormFieldMock } from '../../../../common/mock'; + +describe('PickTimeline', () => { + it('renders correctly', () => { + const Component = () => { + const field = useFormFieldMock(); + + return ( + + ); + }; + const wrapper = shallow(); + + expect(wrapper.dive().find('[data-test-subj="pick-timeline"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/pick_timeline/index.tsx b/x-pack/plugins/siem/public/alerts/components/rules/pick_timeline/index.tsx new file mode 100644 index 00000000000000..0029e70e4edda8 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/pick_timeline/index.tsx @@ -0,0 +1,74 @@ +/* + * 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 { EuiFormRow } from '@elastic/eui'; +import React, { useCallback, useEffect, useState } from 'react'; + +import { SearchTimelineSuperSelect } from '../../../../timelines/components/timeline/search_super_select'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../shared_imports'; + +export interface FieldValueTimeline { + id: string | null; + title: string | null; +} + +interface QueryBarDefineRuleProps { + dataTestSubj: string; + field: FieldHook; + idAria: string; + isDisabled: boolean; +} + +export const PickTimeline = ({ + dataTestSubj, + field, + idAria, + isDisabled = false, +}: QueryBarDefineRuleProps) => { + const [timelineId, setTimelineId] = useState(null); + const [timelineTitle, setTimelineTitle] = useState(null); + + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + + useEffect(() => { + const { id, title } = field.value as FieldValueTimeline; + if (timelineTitle !== title && timelineId !== id) { + setTimelineId(id); + setTimelineTitle(title); + } + }, [field.value]); + + const handleOnTimelineChange = useCallback( + (title: string, id: string | null) => { + if (id === null) { + field.setValue({ id, title: null }); + } else if (timelineTitle !== title && timelineId !== id) { + field.setValue({ id, title }); + } + }, + [field] + ); + + return ( + + + + ); +}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.test.tsx b/x-pack/plugins/siem/public/alerts/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.test.tsx rename to x-pack/plugins/siem/public/alerts/components/rules/pre_packaged_rules/load_empty_prompt.test.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.tsx b/x-pack/plugins/siem/public/alerts/components/rules/pre_packaged_rules/load_empty_prompt.tsx similarity index 94% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.tsx rename to x-pack/plugins/siem/public/alerts/components/rules/pre_packaged_rules/load_empty_prompt.tsx index 5d136265ef1f23..cd88c4ce72af80 100644 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/load_empty_prompt.tsx +++ b/x-pack/plugins/siem/public/alerts/components/rules/pre_packaged_rules/load_empty_prompt.tsx @@ -8,7 +8,7 @@ import { EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/e import React, { memo, useCallback } from 'react'; import styled from 'styled-components'; -import { DETECTION_ENGINE_PAGE_NAME } from '../../../../../components/link_to/redirect_to_detection_engine'; +import { DETECTION_ENGINE_PAGE_NAME } from '../../../../common/components/link_to/redirect_to_detection_engine'; import * as i18n from './translations'; const EmptyPrompt = styled(EuiEmptyPrompt)` diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/translations.ts b/x-pack/plugins/siem/public/alerts/components/rules/pre_packaged_rules/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/translations.ts rename to x-pack/plugins/siem/public/alerts/components/rules/pre_packaged_rules/translations.ts diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/update_callout.test.tsx b/x-pack/plugins/siem/public/alerts/components/rules/pre_packaged_rules/update_callout.test.tsx similarity index 89% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/update_callout.test.tsx rename to x-pack/plugins/siem/public/alerts/components/rules/pre_packaged_rules/update_callout.test.tsx index 807da79fb7a1ab..b5dca70ad95758 100644 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/update_callout.test.tsx +++ b/x-pack/plugins/siem/public/alerts/components/rules/pre_packaged_rules/update_callout.test.tsx @@ -8,8 +8,8 @@ import React from 'react'; import { shallow } from 'enzyme'; import { UpdatePrePackagedRulesCallOut } from './update_callout'; -import { useKibana } from '../../../../../lib/kibana'; -jest.mock('../../../../../lib/kibana'); +import { useKibana } from '../../../../common/lib/kibana'; +jest.mock('../../../../common/lib/kibana'); describe('UpdatePrePackagedRulesCallOut', () => { beforeAll(() => { diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/update_callout.tsx b/x-pack/plugins/siem/public/alerts/components/rules/pre_packaged_rules/update_callout.tsx similarity index 95% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/update_callout.tsx rename to x-pack/plugins/siem/public/alerts/components/rules/pre_packaged_rules/update_callout.tsx index c2887508a9ae9a..0faf4074ed890d 100644 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/pre_packaged_rules/update_callout.tsx +++ b/x-pack/plugins/siem/public/alerts/components/rules/pre_packaged_rules/update_callout.tsx @@ -8,7 +8,7 @@ import React, { memo } from 'react'; import { EuiCallOut, EuiButton, EuiLink } from '@elastic/eui'; -import { useKibana } from '../../../../../lib/kibana'; +import { useKibana } from '../../../../common/lib/kibana'; import * as i18n from './translations'; interface UpdatePrePackagedRulesCallOutProps { diff --git a/x-pack/plugins/siem/public/alerts/components/rules/query_bar/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/rules/query_bar/index.test.tsx new file mode 100644 index 00000000000000..e22359edecd1a9 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/query_bar/index.test.tsx @@ -0,0 +1,37 @@ +/* + * 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 { shallow } from 'enzyme'; + +import { QueryBarDefineRule } from './index'; +import { useFormFieldMock } from '../../../../common/mock'; + +jest.mock('../../../../common/lib/kibana'); + +describe('QueryBarDefineRule', () => { + it('renders correctly', () => { + const Component = () => { + const field = useFormFieldMock(); + + return ( + + ); + }; + const wrapper = shallow(); + + expect(wrapper.dive().find('[data-test-subj="query-bar-define-rule"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/query_bar/index.tsx b/x-pack/plugins/siem/public/alerts/components/rules/query_bar/index.tsx new file mode 100644 index 00000000000000..1aa5ce66d371ef --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/query_bar/index.tsx @@ -0,0 +1,285 @@ +/* + * 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 { EuiFormRow, EuiMutationObserver } from '@elastic/eui'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { Subscription } from 'rxjs'; +import styled from 'styled-components'; +import deepEqual from 'fast-deep-equal'; + +import { + Filter, + IIndexPattern, + Query, + FilterManager, + SavedQuery, + SavedQueryTimeFilter, +} from '../../../../../../../../src/plugins/data/public'; + +import { BrowserFields } from '../../../../common/containers/source'; +import { OpenTimelineModal } from '../../../../timelines/components/open_timeline/open_timeline_modal'; +import { ActionTimelineToShow } from '../../../../timelines/components/open_timeline/types'; +import { QueryBar } from '../../../../common/components/query_bar'; +import { buildGlobalQuery } from '../../../../timelines/components/timeline/helpers'; +import { getDataProviderFilter } from '../../../../timelines/components/timeline/query_bar'; +import { convertKueryToElasticSearchQuery } from '../../../../common/lib/keury'; +import { useKibana } from '../../../../common/lib/kibana'; +import { TimelineModel } from '../../../../timelines/store/timeline/model'; +import { useSavedQueryServices } from '../../../../common/utils/saved_query_services'; +import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../shared_imports'; +import * as i18n from './translations'; + +export interface FieldValueQueryBar { + filters: Filter[]; + query: Query; + saved_id?: string; +} +interface QueryBarDefineRuleProps { + browserFields: BrowserFields; + dataTestSubj: string; + field: FieldHook; + idAria: string; + isLoading: boolean; + indexPattern: IIndexPattern; + onCloseTimelineSearch: () => void; + openTimelineSearch: boolean; + resizeParentContainer?: (height: number) => void; +} + +const StyledEuiFormRow = styled(EuiFormRow)` + .kbnTypeahead__items { + max-height: 45vh !important; + } + .globalQueryBar { + padding: 4px 0px 0px 0px; + .kbnQueryBar { + & > div:first-child { + margin: 0px 0px 0px 4px; + } + } + } +`; + +// TODO need to add disabled in the SearchBar + +export const QueryBarDefineRule = ({ + browserFields, + dataTestSubj, + field, + idAria, + indexPattern, + isLoading = false, + onCloseTimelineSearch, + openTimelineSearch = false, + resizeParentContainer, +}: QueryBarDefineRuleProps) => { + const [originalHeight, setOriginalHeight] = useState(-1); + const [loadingTimeline, setLoadingTimeline] = useState(false); + const [savedQuery, setSavedQuery] = useState(null); + const [queryDraft, setQueryDraft] = useState({ query: '', language: 'kuery' }); + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + + const kibana = useKibana(); + const [filterManager] = useState(new FilterManager(kibana.services.uiSettings)); + + const savedQueryServices = useSavedQueryServices(); + + useEffect(() => { + let isSubscribed = true; + const subscriptions = new Subscription(); + filterManager.setFilters([]); + + subscriptions.add( + filterManager.getUpdates$().subscribe({ + next: () => { + if (isSubscribed) { + const newFilters = filterManager.getFilters(); + const { filters } = field.value as FieldValueQueryBar; + + if (!deepEqual(filters, newFilters)) { + field.setValue({ ...(field.value as FieldValueQueryBar), filters: newFilters }); + } + } + }, + }) + ); + + return () => { + isSubscribed = false; + subscriptions.unsubscribe(); + }; + }, [field.value]); + + useEffect(() => { + let isSubscribed = true; + async function updateFilterQueryFromValue() { + const { filters, query, saved_id: savedId } = field.value as FieldValueQueryBar; + if (!deepEqual(query, queryDraft)) { + setQueryDraft(query); + } + if (!deepEqual(filters, filterManager.getFilters())) { + filterManager.setFilters(filters); + } + if ( + (savedId != null && savedQuery != null && savedId !== savedQuery.id) || + (savedId != null && savedQuery == null) + ) { + try { + const mySavedQuery = await savedQueryServices.getSavedQuery(savedId); + if (isSubscribed && mySavedQuery != null) { + setSavedQuery(mySavedQuery); + } + } catch { + setSavedQuery(null); + } + } else if (savedId == null && savedQuery != null) { + setSavedQuery(null); + } + } + updateFilterQueryFromValue(); + return () => { + isSubscribed = false; + }; + }, [field.value]); + + const onSubmitQuery = useCallback( + (newQuery: Query, timefilter?: SavedQueryTimeFilter) => { + const { query } = field.value as FieldValueQueryBar; + if (!deepEqual(query, newQuery)) { + field.setValue({ ...(field.value as FieldValueQueryBar), query: newQuery }); + } + }, + [field] + ); + + const onChangedQuery = useCallback( + (newQuery: Query) => { + const { query } = field.value as FieldValueQueryBar; + if (!deepEqual(query, newQuery)) { + field.setValue({ ...(field.value as FieldValueQueryBar), query: newQuery }); + } + }, + [field] + ); + + const onSavedQuery = useCallback( + (newSavedQuery: SavedQuery | null) => { + if (newSavedQuery != null) { + const { saved_id: savedId } = field.value as FieldValueQueryBar; + if (newSavedQuery.id !== savedId) { + setSavedQuery(newSavedQuery); + field.setValue({ + filters: newSavedQuery.attributes.filters, + query: newSavedQuery.attributes.query, + saved_id: newSavedQuery.id, + }); + } + } + }, + [field.value] + ); + + const onCloseTimelineModal = useCallback(() => { + setLoadingTimeline(true); + onCloseTimelineSearch(); + }, [onCloseTimelineSearch]); + + const onOpenTimeline = useCallback( + (timeline: TimelineModel) => { + setLoadingTimeline(false); + const newQuery = { + query: timeline.kqlQuery.filterQuery?.kuery?.expression ?? '', + language: timeline.kqlQuery.filterQuery?.kuery?.kind ?? 'kuery', + }; + const dataProvidersDsl = + timeline.dataProviders != null && timeline.dataProviders.length > 0 + ? convertKueryToElasticSearchQuery( + buildGlobalQuery(timeline.dataProviders, browserFields), + indexPattern + ) + : ''; + const newFilters = timeline.filters ?? []; + field.setValue({ + filters: + dataProvidersDsl !== '' + ? [...newFilters, getDataProviderFilter(dataProvidersDsl)] + : newFilters, + query: newQuery, + saved_id: '', + }); + }, + [browserFields, field, indexPattern] + ); + + const onMutation = (event: unknown, observer: unknown) => { + if (resizeParentContainer != null) { + const suggestionContainer = document.getElementById('kbnTypeahead__items'); + if (suggestionContainer != null) { + const box = suggestionContainer.getBoundingClientRect(); + const accordionContainer = document.getElementById('define-rule'); + if (accordionContainer != null) { + const accordionBox = accordionContainer.getBoundingClientRect(); + if (originalHeight === -1 || accordionBox.height < originalHeight + box.height) { + resizeParentContainer(originalHeight + box.height - 100); + } + if (originalHeight === -1) { + setOriginalHeight(accordionBox.height); + } + } + } else { + resizeParentContainer(-1); + } + } + }; + + const actionTimelineToHide = useMemo(() => ['duplicate'], []); + + return ( + <> + + + {mutationRef => ( +
+ +
+ )} +
+
+ {openTimelineSearch ? ( + + ) : null} + + ); +}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/query_bar/translations.tsx b/x-pack/plugins/siem/public/alerts/components/rules/query_bar/translations.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/query_bar/translations.tsx rename to x-pack/plugins/siem/public/alerts/components/rules/query_bar/translations.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/rules/read_only_callout/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.test.tsx rename to x-pack/plugins/siem/public/alerts/components/rules/read_only_callout/index.test.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.tsx b/x-pack/plugins/siem/public/alerts/components/rules/read_only_callout/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/index.tsx rename to x-pack/plugins/siem/public/alerts/components/rules/read_only_callout/index.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/translations.ts b/x-pack/plugins/siem/public/alerts/components/rules/read_only_callout/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/read_only_callout/translations.ts rename to x-pack/plugins/siem/public/alerts/components/rules/read_only_callout/translations.ts diff --git a/x-pack/plugins/siem/public/alerts/components/rules/rule_actions_field/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/rules/rule_actions_field/index.test.tsx new file mode 100644 index 00000000000000..579f8869c08b17 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/rule_actions_field/index.test.tsx @@ -0,0 +1,42 @@ +/* + * 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 { shallow } from 'enzyme'; + +import { RuleActionsField } from './index'; +import { useKibana } from '../../../../common/lib/kibana'; +import { useFormFieldMock } from '../../../../common/mock'; +jest.mock('../../../../common/lib/kibana'); + +describe('RuleActionsField', () => { + it('should not render ActionForm is no actions are supported', () => { + (useKibana as jest.Mock).mockReturnValue({ + services: { + triggers_actions_ui: { + actionTypeRegistry: {}, + }, + application: { + capabilities: { + actions: { + delete: true, + save: true, + show: true, + }, + }, + }, + }, + }); + const Component = () => { + const field = useFormFieldMock(); + + return ; + }; + const wrapper = shallow(); + + expect(wrapper.dive().find('ActionForm')).toHaveLength(0); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/rule_actions_field/index.tsx b/x-pack/plugins/siem/public/alerts/components/rules/rule_actions_field/index.tsx new file mode 100644 index 00000000000000..2e9a793bbdef2a --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/rule_actions_field/index.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback, useEffect, useState } from 'react'; +import deepMerge from 'deepmerge'; + +import { NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS } from '../../../../../common/constants'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { loadActionTypes } from '../../../../../../triggers_actions_ui/public/application/lib/action_connector_api'; +import { SelectField } from '../../../../shared_imports'; +import { ActionForm, ActionType } from '../../../../../../triggers_actions_ui/public'; +import { AlertAction } from '../../../../../../alerting/common'; +import { useKibana } from '../../../../common/lib/kibana'; + +type ThrottleSelectField = typeof SelectField; + +const DEFAULT_ACTION_GROUP_ID = 'default'; +const DEFAULT_ACTION_MESSAGE = + 'Rule {{context.rule.name}} generated {{state.signals_count}} signals'; + +export const RuleActionsField: ThrottleSelectField = ({ field, messageVariables }) => { + const [supportedActionTypes, setSupportedActionTypes] = useState(); + const { + http, + triggers_actions_ui: { actionTypeRegistry }, + notifications, + docLinks, + application: { capabilities }, + } = useKibana().services; + + const setActionIdByIndex = useCallback( + (id: string, index: number) => { + const updatedActions = [...(field.value as Array>)]; + updatedActions[index] = deepMerge(updatedActions[index], { id }); + field.setValue(updatedActions); + }, + [field] + ); + + const setAlertProperty = useCallback( + (updatedActions: AlertAction[]) => field.setValue(updatedActions), + [field] + ); + + const setActionParamsProperty = useCallback( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (key: string, value: any, index: number) => { + const updatedActions = [...(field.value as AlertAction[])]; + updatedActions[index].params[key] = value; + field.setValue(updatedActions); + }, + [field] + ); + + useEffect(() => { + (async function() { + const actionTypes = await loadActionTypes({ http }); + const supportedTypes = actionTypes.filter(actionType => + NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS.includes(actionType.id) + ); + setSupportedActionTypes(supportedTypes); + })(); + }, []); + + if (!supportedActionTypes) return <>; + + return ( + + ); +}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/alerts/components/rules/rule_actions_overflow/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/alerts/components/rules/rule_actions_overflow/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/alerts/components/rules/rule_actions_overflow/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/rules/rule_actions_overflow/index.test.tsx new file mode 100644 index 00000000000000..a648a1bdb9aa86 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/rule_actions_overflow/index.test.tsx @@ -0,0 +1,298 @@ +/* + * 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, mount } from 'enzyme'; +import React from 'react'; + +import { + deleteRulesAction, + duplicateRulesAction, +} from '../../../pages/detection_engine/rules/all/actions'; +import { RuleActionsOverflow } from './index'; +import { mockRule } from '../../../pages/detection_engine/rules/all/__mocks__/mock'; + +jest.mock('react-router-dom', () => ({ + useHistory: () => ({ + push: jest.fn(), + }), +})); + +jest.mock('../../../pages/detection_engine/rules/all/actions', () => ({ + deleteRulesAction: jest.fn(), + duplicateRulesAction: jest.fn(), +})); + +describe('RuleActionsOverflow', () => { + describe('snapshots', () => { + test('renders correctly against snapshot', () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); + }); + + describe('rules details menu panel', () => { + test('there is at least one item when there is a rule within the rules-details-menu-panel', () => { + const wrapper = mount( + + ); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + const items: unknown[] = wrapper + .find('[data-test-subj="rules-details-menu-panel"]') + .first() + .prop('items'); + + expect(items.length).toBeGreaterThan(0); + }); + + test('items are empty when there is a null rule within the rules-details-menu-panel', () => { + const wrapper = mount(); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + expect( + wrapper + .find('[data-test-subj="rules-details-menu-panel"]') + .first() + .prop('items') + ).toEqual([]); + }); + + test('items are empty when there is an undefined rule within the rules-details-menu-panel', () => { + const wrapper = mount(); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + expect( + wrapper + .find('[data-test-subj="rules-details-menu-panel"]') + .first() + .prop('items') + ).toEqual([]); + }); + + test('it opens the popover when rules-details-popover-button-icon is clicked', () => { + const wrapper = mount( + + ); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + expect( + wrapper + .find('[data-test-subj="rules-details-popover"]') + .first() + .prop('isOpen') + ).toEqual(true); + }); + }); + + describe('rules details pop over button icon', () => { + test('it does not open the popover when rules-details-popover-button-icon is clicked when the user does not have permission', () => { + const wrapper = mount( + + ); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + expect( + wrapper + .find('[data-test-subj="rules-details-popover"]') + .first() + .prop('isOpen') + ).toEqual(false); + }); + }); + + describe('rules details duplicate rule', () => { + test('it does not open the popover when rules-details-popover-button-icon is clicked and the user does not have permission', () => { + const rule = mockRule('id'); + const wrapper = mount(); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + expect(wrapper.find('[data-test-subj="rules-details-delete-rule"] button').exists()).toEqual( + false + ); + }); + + test('it opens the popover when rules-details-popover-button-icon is clicked', () => { + const wrapper = mount( + + ); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + expect( + wrapper + .find('[data-test-subj="rules-details-popover"]') + .first() + .prop('isOpen') + ).toEqual(true); + }); + + test('it closes the popover when rules-details-duplicate-rule is clicked', () => { + const wrapper = mount( + + ); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + wrapper.find('[data-test-subj="rules-details-duplicate-rule"] button').simulate('click'); + wrapper.update(); + expect( + wrapper + .find('[data-test-subj="rules-details-popover"]') + .first() + .prop('isOpen') + ).toEqual(false); + }); + + test('it calls duplicateRulesAction when rules-details-duplicate-rule is clicked', () => { + const wrapper = mount( + + ); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + wrapper.find('[data-test-subj="rules-details-duplicate-rule"] button').simulate('click'); + wrapper.update(); + expect(duplicateRulesAction).toHaveBeenCalled(); + }); + + test('it calls duplicateRulesAction with the rule and rule.id when rules-details-duplicate-rule is clicked', () => { + const rule = mockRule('id'); + const wrapper = mount(); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + wrapper.find('[data-test-subj="rules-details-duplicate-rule"] button').simulate('click'); + wrapper.update(); + expect(duplicateRulesAction).toHaveBeenCalledWith( + [rule], + [rule.id], + expect.anything(), + expect.anything() + ); + }); + }); + + describe('rules details export rule', () => { + test('it does not open the popover when rules-details-popover-button-icon is clicked and the user does not have permission', () => { + const rule = mockRule('id'); + const wrapper = mount(); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + expect(wrapper.find('[data-test-subj="rules-details-export-rule"] button').exists()).toEqual( + false + ); + }); + + test('it closes the popover when rules-details-export-rule is clicked', () => { + const wrapper = mount( + + ); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + wrapper.find('[data-test-subj="rules-details-export-rule"] button').simulate('click'); + wrapper.update(); + expect( + wrapper + .find('[data-test-subj="rules-details-popover"]') + .first() + .prop('isOpen') + ).toEqual(false); + }); + + test('it sets the rule.rule_id on the generic downloader when rules-details-export-rule is clicked', () => { + const rule = mockRule('id'); + const wrapper = mount(); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + wrapper.find('[data-test-subj="rules-details-export-rule"] button').simulate('click'); + wrapper.update(); + expect( + wrapper.find('[data-test-subj="rules-details-generic-downloader"]').prop('ids') + ).toEqual([rule.rule_id]); + }); + + test('it does not close the pop over on rules-details-export-rule when the rule is an immutable rule and the user does a click', () => { + const rule = mockRule('id'); + rule.immutable = true; + const wrapper = mount(); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + wrapper.find('[data-test-subj="rules-details-export-rule"] button').simulate('click'); + wrapper.update(); + expect( + wrapper + .find('[data-test-subj="rules-details-popover"]') + .first() + .prop('isOpen') + ).toEqual(true); + }); + + test('it does not set the rule.rule_id on rules-details-export-rule when the rule is an immutable rule', () => { + const rule = mockRule('id'); + rule.immutable = true; + const wrapper = mount(); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + wrapper.find('[data-test-subj="rules-details-export-rule"] button').simulate('click'); + wrapper.update(); + expect( + wrapper.find('[data-test-subj="rules-details-generic-downloader"]').prop('ids') + ).toEqual([]); + }); + }); + + describe('rules details delete rule', () => { + test('it does not open the popover when rules-details-popover-button-icon is clicked and the user does not have permission', () => { + const rule = mockRule('id'); + const wrapper = mount(); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + expect(wrapper.find('[data-test-subj="rules-details-delete-rule"] button').exists()).toEqual( + false + ); + }); + + test('it closes the popover when rules-details-delete-rule is clicked', () => { + const wrapper = mount( + + ); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + wrapper.find('[data-test-subj="rules-details-delete-rule"] button').simulate('click'); + wrapper.update(); + expect( + wrapper + .find('[data-test-subj="rules-details-popover"]') + .first() + .prop('isOpen') + ).toEqual(false); + }); + + test('it calls deleteRulesAction when rules-details-delete-rule is clicked', () => { + const wrapper = mount( + + ); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + wrapper.find('[data-test-subj="rules-details-delete-rule"] button').simulate('click'); + wrapper.update(); + expect(deleteRulesAction).toHaveBeenCalled(); + }); + + test('it calls deleteRulesAction with the rule.id when rules-details-delete-rule is clicked', () => { + const rule = mockRule('id'); + const wrapper = mount(); + wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); + wrapper.update(); + wrapper.find('[data-test-subj="rules-details-delete-rule"] button').simulate('click'); + wrapper.update(); + expect(deleteRulesAction).toHaveBeenCalledWith( + [rule.id], + expect.anything(), + expect.anything(), + expect.anything() + ); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/rule_actions_overflow/index.tsx b/x-pack/plugins/siem/public/alerts/components/rules/rule_actions_overflow/index.tsx new file mode 100644 index 00000000000000..2d8e6bef8ee90d --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/rule_actions_overflow/index.tsx @@ -0,0 +1,158 @@ +/* + * 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 { + EuiButtonIcon, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiPopover, + EuiToolTip, +} from '@elastic/eui'; +import React, { useCallback, useMemo, useState } from 'react'; +import styled from 'styled-components'; + +import { noop } from 'lodash/fp'; +import { useHistory } from 'react-router-dom'; +import { Rule, exportRules } from '../../../../alerts/containers/detection_engine/rules'; +import * as i18n from './translations'; +import * as i18nActions from '../../../pages/detection_engine/rules/translations'; +import { displaySuccessToast, useStateToaster } from '../../../../common/components/toasters'; +import { + deleteRulesAction, + duplicateRulesAction, +} from '../../../pages/detection_engine/rules/all/actions'; +import { GenericDownloader } from '../../../../common/components/generic_downloader'; +import { DETECTION_ENGINE_PAGE_NAME } from '../../../../common/components/link_to/redirect_to_detection_engine'; + +const MyEuiButtonIcon = styled(EuiButtonIcon)` + &.euiButtonIcon { + svg { + transform: rotate(90deg); + } + border: 1px solid  ${({ theme }) => theme.euiColorPrimary}; + width: 40px; + height: 40px; + } +`; + +interface RuleActionsOverflowComponentProps { + rule: Rule | null; + userHasNoPermissions: boolean; +} + +/** + * Overflow Actions for a Rule + */ +const RuleActionsOverflowComponent = ({ + rule, + userHasNoPermissions, +}: RuleActionsOverflowComponentProps) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [rulesToExport, setRulesToExport] = useState([]); + const history = useHistory(); + const [, dispatchToaster] = useStateToaster(); + + const onRuleDeletedCallback = useCallback(() => { + history.push(`/${DETECTION_ENGINE_PAGE_NAME}/rules`); + }, [history]); + + const actions = useMemo( + () => + rule != null + ? [ + { + setIsPopoverOpen(false); + await duplicateRulesAction([rule], [rule.id], noop, dispatchToaster); + }} + > + {i18nActions.DUPLICATE_RULE} + , + { + setIsPopoverOpen(false); + setRulesToExport([rule.rule_id]); + }} + > + {i18nActions.EXPORT_RULE} + , + { + setIsPopoverOpen(false); + await deleteRulesAction([rule.id], noop, dispatchToaster, onRuleDeletedCallback); + }} + > + {i18nActions.DELETE_RULE} + , + ] + : [], + [rule, userHasNoPermissions] + ); + + const handlePopoverOpen = useCallback(() => { + setIsPopoverOpen(!isPopoverOpen); + }, [setIsPopoverOpen, isPopoverOpen]); + + const button = useMemo( + () => ( + + + + ), + [handlePopoverOpen, userHasNoPermissions] + ); + + return ( + <> + setIsPopoverOpen(false)} + id="ruleActionsOverflow" + isOpen={isPopoverOpen} + data-test-subj="rules-details-popover" + ownFocus={true} + panelPaddingSize="none" + > + + + { + displaySuccessToast( + i18nActions.SUCCESSFULLY_EXPORTED_RULES(exportCount), + dispatchToaster + ); + }} + /> + + ); +}; + +export const RuleActionsOverflow = React.memo(RuleActionsOverflowComponent); + +RuleActionsOverflow.displayName = 'RuleActionsOverflow'; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/translations.ts b/x-pack/plugins/siem/public/alerts/components/rules/rule_actions_overflow/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/translations.ts rename to x-pack/plugins/siem/public/alerts/components/rules/rule_actions_overflow/translations.ts diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.test.tsx b/x-pack/plugins/siem/public/alerts/components/rules/rule_status/helpers.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.test.tsx rename to x-pack/plugins/siem/public/alerts/components/rules/rule_status/helpers.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/rule_status/helpers.ts b/x-pack/plugins/siem/public/alerts/components/rules/rule_status/helpers.ts new file mode 100644 index 00000000000000..88fca3d95604e7 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/rule_status/helpers.ts @@ -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. + */ + +import { RuleStatusType } from '../../../../alerts/containers/detection_engine/rules'; + +export const getStatusColor = (status: RuleStatusType | string | null) => + status == null + ? 'subdued' + : status === 'succeeded' + ? 'success' + : status === 'failed' + ? 'danger' + : status === 'executing' || status === 'going to run' + ? 'warning' + : 'subdued'; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/rules/rule_status/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.test.tsx rename to x-pack/plugins/siem/public/alerts/components/rules/rule_status/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/rule_status/index.tsx b/x-pack/plugins/siem/public/alerts/components/rules/rule_status/index.tsx new file mode 100644 index 00000000000000..53be48bc988509 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/rule_status/index.tsx @@ -0,0 +1,102 @@ +/* + * 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 { + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiHealth, + EuiLoadingSpinner, + EuiText, +} from '@elastic/eui'; +import React, { memo, useCallback, useEffect, useState } from 'react'; +import deepEqual from 'fast-deep-equal'; + +import { + useRuleStatus, + RuleInfoStatus, +} from '../../../../alerts/containers/detection_engine/rules'; +import { FormattedDate } from '../../../../common/components/formatted_date'; +import { getEmptyTagValue } from '../../../../common/components/empty_value'; +import { getStatusColor } from './helpers'; +import * as i18n from './translations'; + +interface RuleStatusProps { + ruleId: string | null; + ruleEnabled?: boolean | null; +} + +const RuleStatusComponent: React.FC = ({ ruleId, ruleEnabled }) => { + const [loading, ruleStatus, fetchRuleStatus] = useRuleStatus(ruleId); + const [myRuleEnabled, setMyRuleEnabled] = useState(ruleEnabled ?? null); + const [currentStatus, setCurrentStatus] = useState( + ruleStatus?.current_status ?? null + ); + + useEffect(() => { + if (myRuleEnabled !== ruleEnabled && fetchRuleStatus != null && ruleId != null) { + fetchRuleStatus(ruleId); + if (myRuleEnabled !== ruleEnabled) { + setMyRuleEnabled(ruleEnabled ?? null); + } + } + }, [fetchRuleStatus, myRuleEnabled, ruleId, ruleEnabled, setMyRuleEnabled]); + + useEffect(() => { + if (!deepEqual(currentStatus, ruleStatus?.current_status)) { + setCurrentStatus(ruleStatus?.current_status ?? null); + } + }, [currentStatus, ruleStatus, setCurrentStatus]); + + const handleRefresh = useCallback(() => { + if (fetchRuleStatus != null && ruleId != null) { + fetchRuleStatus(ruleId); + } + }, [fetchRuleStatus, ruleId]); + + return ( + + + {i18n.STATUS} + {':'} + + {loading && ( + + + + )} + {!loading && ( + <> + + + {currentStatus?.status ?? getEmptyTagValue()} + + + {currentStatus?.status_date != null && currentStatus?.status != null && ( + <> + + <>{i18n.STATUS_AT} + + + + + + )} + + + + + )} + + ); +}; + +export const RuleStatus = memo(RuleStatusComponent); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_status/translations.ts b/x-pack/plugins/siem/public/alerts/components/rules/rule_status/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_status/translations.ts rename to x-pack/plugins/siem/public/alerts/components/rules/rule_status/translations.ts diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/alerts/components/rules/rule_switch/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/alerts/components/rules/rule_switch/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/rules/rule_switch/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.test.tsx rename to x-pack/plugins/siem/public/alerts/components/rules/rule_switch/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/rule_switch/index.tsx b/x-pack/plugins/siem/public/alerts/components/rules/rule_switch/index.tsx new file mode 100644 index 00000000000000..2f718818ff2e73 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/rule_switch/index.tsx @@ -0,0 +1,134 @@ +/* + * 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 { + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiSwitch, + EuiSwitchEvent, +} from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; +import styled from 'styled-components'; +import React, { useCallback, useState, useEffect } from 'react'; + +import * as i18n from '../../../pages/detection_engine/rules/translations'; +import { enableRules } from '../../../../alerts/containers/detection_engine/rules'; +import { enableRulesAction } from '../../../pages/detection_engine/rules/all/actions'; +import { Action } from '../../../pages/detection_engine/rules/all/reducer'; +import { useStateToaster, displayErrorToast } from '../../../../common/components/toasters'; +import { bucketRulesResponse } from '../../../pages/detection_engine/rules/all/helpers'; + +const StaticSwitch = styled(EuiSwitch)` + .euiSwitch__thumb, + .euiSwitch__icon { + transition: none; + } +`; + +StaticSwitch.displayName = 'StaticSwitch'; + +export interface RuleSwitchProps { + dispatch?: React.Dispatch; + id: string; + enabled: boolean; + isDisabled?: boolean; + isLoading?: boolean; + optionLabel?: string; + onChange?: (enabled: boolean) => void; +} + +/** + * Basic switch component for displaying loader when enabled/disabled + */ +export const RuleSwitchComponent = ({ + dispatch, + id, + isDisabled, + isLoading, + enabled, + optionLabel, + onChange, +}: RuleSwitchProps) => { + const [myIsLoading, setMyIsLoading] = useState(false); + const [myEnabled, setMyEnabled] = useState(enabled ?? false); + const [, dispatchToaster] = useStateToaster(); + + const onRuleStateChange = useCallback( + async (event: EuiSwitchEvent) => { + setMyIsLoading(true); + if (dispatch != null) { + await enableRulesAction([id], event.target.checked!, dispatch, dispatchToaster); + } else { + try { + const enabling = event.target.checked!; + const response = await enableRules({ + ids: [id], + enabled: enabling, + }); + const { rules, errors } = bucketRulesResponse(response); + + if (errors.length > 0) { + setMyIsLoading(false); + const title = enabling + ? i18n.BATCH_ACTION_ACTIVATE_SELECTED_ERROR(1) + : i18n.BATCH_ACTION_DEACTIVATE_SELECTED_ERROR(1); + displayErrorToast( + title, + errors.map(e => e.error.message), + dispatchToaster + ); + } else { + const [rule] = rules; + setMyEnabled(rule.enabled); + if (onChange != null) { + onChange(rule.enabled); + } + } + } catch { + setMyIsLoading(false); + } + } + setMyIsLoading(false); + }, + [dispatch, id] + ); + + useEffect(() => { + if (myEnabled !== enabled) { + setMyEnabled(enabled); + } + }, [enabled]); + + useEffect(() => { + if (myIsLoading !== isLoading) { + setMyIsLoading(isLoading ?? false); + } + }, [isLoading]); + + return ( + + + {myIsLoading ? ( + + ) : ( + + )} + + + ); +}; + +export const RuleSwitch = React.memo(RuleSwitchComponent); + +RuleSwitch.displayName = 'RuleSwitch'; diff --git a/x-pack/plugins/siem/public/alerts/components/rules/schedule_item_form/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/rules/schedule_item_form/index.test.tsx new file mode 100644 index 00000000000000..9dddd9e6c40851 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/schedule_item_form/index.test.tsx @@ -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 React from 'react'; +import { shallow } from 'enzyme'; + +import { ScheduleItem } from './index'; +import { useFormFieldMock } from '../../../../common/mock'; + +describe('ScheduleItem', () => { + it('renders correctly', () => { + const Component = () => { + const field = useFormFieldMock(); + + return ( + + ); + }; + const wrapper = shallow(); + + expect(wrapper.dive().find('[data-test-subj="schedule-item"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/schedule_item_form/index.tsx b/x-pack/plugins/siem/public/alerts/components/rules/schedule_item_form/index.tsx new file mode 100644 index 00000000000000..ffb6c4eda32438 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/schedule_item_form/index.tsx @@ -0,0 +1,163 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiFlexGroup, + EuiFlexItem, + EuiFieldNumber, + EuiFormRow, + EuiSelect, + EuiFormControlLayout, +} from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import styled from 'styled-components'; + +import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../shared_imports'; + +import * as I18n from './translations'; + +interface ScheduleItemProps { + field: FieldHook; + dataTestSubj: string; + idAria: string; + isDisabled: boolean; + minimumValue?: number; +} + +const timeTypeOptions = [ + { value: 's', text: I18n.SECONDS }, + { value: 'm', text: I18n.MINUTES }, + { value: 'h', text: I18n.HOURS }, +]; + +// move optional label to the end of input +const StyledLabelAppend = styled(EuiFlexItem)` + &.euiFlexItem.euiFlexItem--flexGrowZero { + margin-left: 31px; + } +`; + +const StyledEuiFormRow = styled(EuiFormRow)` + max-width: none; + + .euiFormControlLayout { + max-width: 200px !important; + } + + .euiFormControlLayout__childrenWrapper > *:first-child { + box-shadow: none; + height: 38px; + } + + .euiFormControlLayout:not(:first-child) { + border-left: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; + } +`; + +const MyEuiSelect = styled(EuiSelect)` + width: auto; +`; + +export const ScheduleItem = ({ + dataTestSubj, + field, + idAria, + isDisabled, + minimumValue = 0, +}: ScheduleItemProps) => { + const [timeType, setTimeType] = useState('s'); + const [timeVal, setTimeVal] = useState(0); + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + + const onChangeTimeType = useCallback( + e => { + setTimeType(e.target.value); + field.setValue(`${timeVal}${e.target.value}`); + }, + [timeVal] + ); + + const onChangeTimeVal = useCallback( + e => { + const sanitizedValue: number = parseInt(e.target.value, 10); + setTimeVal(sanitizedValue); + field.setValue(`${sanitizedValue}${timeType}`); + }, + [timeType] + ); + + useEffect(() => { + if (field.value !== `${timeVal}${timeType}`) { + const filterTimeVal = (field.value as string).match(/\d+/g); + const filterTimeType = (field.value as string).match(/[a-zA-Z]+/g); + if ( + !isEmpty(filterTimeVal) && + filterTimeVal != null && + !isNaN(Number(filterTimeVal[0])) && + Number(filterTimeVal[0]) !== Number(timeVal) + ) { + setTimeVal(Number(filterTimeVal[0])); + } + if ( + !isEmpty(filterTimeType) && + filterTimeType != null && + ['s', 'm', 'h'].includes(filterTimeType[0]) && + filterTimeType[0] !== timeType + ) { + setTimeType(filterTimeType[0]); + } + } + }, [field.value]); + + // EUI missing some props + const rest = { disabled: isDisabled }; + const label = useMemo( + () => ( + + + {field.label} + + + {field.labelAppend} + + + ), + [field.label, field.labelAppend] + ); + + return ( + + + } + > + + + + ); +}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/translations.ts b/x-pack/plugins/siem/public/alerts/components/rules/schedule_item_form/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/translations.ts rename to x-pack/plugins/siem/public/alerts/components/rules/schedule_item_form/translations.ts diff --git a/x-pack/plugins/siem/public/alerts/components/rules/select_rule_type/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/rules/select_rule_type/index.test.tsx new file mode 100644 index 00000000000000..87401408c3cc18 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/select_rule_type/index.test.tsx @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { SelectRuleType } from './index'; +import { useFormFieldMock } from '../../../../common/mock'; +jest.mock('../../../../common/lib/kibana'); + +describe('SelectRuleType', () => { + it('renders correctly', () => { + const Component = () => { + const field = useFormFieldMock(); + + return ; + }; + const wrapper = shallow(); + + expect(wrapper.dive().find('[data-test-subj="selectRuleType"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/select_rule_type/index.tsx b/x-pack/plugins/siem/public/alerts/components/rules/select_rule_type/index.tsx new file mode 100644 index 00000000000000..58112732bea3b1 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/select_rule_type/index.tsx @@ -0,0 +1,123 @@ +/* + * 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, { useCallback } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiCard, + EuiFlexGrid, + EuiFlexItem, + EuiFormRow, + EuiIcon, + EuiLink, + EuiText, +} from '@elastic/eui'; + +import { isMlRule } from '../../../../../common/machine_learning/helpers'; +import { RuleType } from '../../../../../common/detection_engine/types'; +import { FieldHook } from '../../../../shared_imports'; +import { useKibana } from '../../../../common/lib/kibana'; +import * as i18n from './translations'; + +const MlCardDescription = ({ + subscriptionUrl, + hasValidLicense = false, +}: { + subscriptionUrl: string; + hasValidLicense?: boolean; +}) => ( + + {hasValidLicense ? ( + i18n.ML_TYPE_DESCRIPTION + ) : ( + + + + ), + }} + /> + )} + +); + +interface SelectRuleTypeProps { + describedByIds?: string[]; + field: FieldHook; + hasValidLicense?: boolean; + isMlAdmin?: boolean; + isReadOnly?: boolean; +} + +export const SelectRuleType: React.FC = ({ + describedByIds = [], + field, + isReadOnly = false, + hasValidLicense = false, + isMlAdmin = false, +}) => { + const ruleType = field.value as RuleType; + const setType = useCallback( + (type: RuleType) => { + field.setValue(type); + }, + [field] + ); + const setMl = useCallback(() => setType('machine_learning'), [setType]); + const setQuery = useCallback(() => setType('query'), [setType]); + const mlCardDisabled = isReadOnly || !hasValidLicense || !isMlAdmin; + const licensingUrl = useKibana().services.application.getUrlForApp('kibana', { + path: '#/management/elasticsearch/license_management', + }); + + return ( + + + + } + selectable={{ + isDisabled: isReadOnly, + onClick: setQuery, + isSelected: !isMlRule(ruleType), + }} + /> + + + + } + icon={} + isDisabled={mlCardDisabled} + selectable={{ + isDisabled: mlCardDisabled, + onClick: setMl, + isSelected: isMlRule(ruleType), + }} + /> + + + + ); +}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/translations.ts b/x-pack/plugins/siem/public/alerts/components/rules/select_rule_type/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/translations.ts rename to x-pack/plugins/siem/public/alerts/components/rules/select_rule_type/translations.ts diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/severity_badge/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/rules/severity_badge/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/severity_badge/index.test.tsx rename to x-pack/plugins/siem/public/alerts/components/rules/severity_badge/index.test.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/severity_badge/index.tsx b/x-pack/plugins/siem/public/alerts/components/rules/severity_badge/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/severity_badge/index.tsx rename to x-pack/plugins/siem/public/alerts/components/rules/severity_badge/index.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/status_icon/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/rules/status_icon/index.test.tsx new file mode 100644 index 00000000000000..6a16008e9d616d --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/status_icon/index.test.tsx @@ -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 React from 'react'; +import { shallow } from 'enzyme'; + +import { TestProviders } from '../../../../common/mock'; +import { RuleStatusIcon } from './index'; +jest.mock('../../../../common/lib/kibana'); + +describe('RuleStatusIcon', () => { + it('renders correctly', () => { + const wrapper = shallow(, { + wrappingComponent: TestProviders, + }); + + expect(wrapper.find('EuiAvatar')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/status_icon/index.tsx b/x-pack/plugins/siem/public/alerts/components/rules/status_icon/index.tsx new file mode 100644 index 00000000000000..443ee3e1811f62 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/status_icon/index.tsx @@ -0,0 +1,39 @@ +/* + * 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 { EuiAvatar, EuiIcon } from '@elastic/eui'; +import React, { memo } from 'react'; +import styled from 'styled-components'; + +import { useEuiTheme } from '../../../../common/lib/theme/use_eui_theme'; +import { RuleStatusType } from '../../../pages/detection_engine/rules/types'; + +export interface RuleStatusIconProps { + name: string; + type: RuleStatusType; +} + +const RuleStatusIconStyled = styled.div` + position: relative; + svg { + position: absolute; + top: 8px; + left: 9px; + } +`; + +const RuleStatusIconComponent: React.FC = ({ name, type }) => { + const theme = useEuiTheme(); + const color = type === 'passive' ? theme.euiColorLightestShade : theme.euiColorPrimary; + return ( + + + {type === 'valid' ? : null} + + ); +}; + +export const RuleStatusIcon = memo(RuleStatusIconComponent); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/data.tsx b/x-pack/plugins/siem/public/alerts/components/rules/step_about_rule/data.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/data.tsx rename to x-pack/plugins/siem/public/alerts/components/rules/step_about_rule/data.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts b/x-pack/plugins/siem/public/alerts/components/rules/step_about_rule/default_value.ts similarity index 89% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts rename to x-pack/plugins/siem/public/alerts/components/rules/step_about_rule/default_value.ts index 52b0038507b594..977769158481e0 100644 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/default_value.ts +++ b/x-pack/plugins/siem/public/alerts/components/rules/step_about_rule/default_value.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AboutStepRule } from '../../types'; +import { AboutStepRule } from '../../../pages/detection_engine/rules/types'; export const threatDefault = [ { diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/helpers.test.ts b/x-pack/plugins/siem/public/alerts/components/rules/step_about_rule/helpers.test.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/helpers.test.ts rename to x-pack/plugins/siem/public/alerts/components/rules/step_about_rule/helpers.test.ts diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/helpers.ts b/x-pack/plugins/siem/public/alerts/components/rules/step_about_rule/helpers.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/helpers.ts rename to x-pack/plugins/siem/public/alerts/components/rules/step_about_rule/helpers.ts diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_about_rule/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/rules/step_about_rule/index.test.tsx new file mode 100644 index 00000000000000..13a62cee047ba9 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/step_about_rule/index.test.tsx @@ -0,0 +1,167 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { mount, shallow } from 'enzyme'; +import { ThemeProvider } from 'styled-components'; +import euiDarkVars from '@elastic/eui/dist/eui_theme_light.json'; + +import { StepAboutRule } from '.'; + +import { mockAboutStepRule } from '../../../pages/detection_engine/rules/all/__mocks__/mock'; +import { StepRuleDescription } from '../description_step'; +import { stepAboutDefaultValue } from './default_value'; + +const theme = () => ({ eui: euiDarkVars, darkMode: true }); + +/* eslint-disable no-console */ +// Silence until enzyme fixed to use ReactTestUtils.act() +const originalError = console.error; +beforeAll(() => { + console.error = jest.fn(); +}); +afterAll(() => { + console.error = originalError; +}); +/* eslint-enable no-console */ + +describe('StepAboutRuleComponent', () => { + test('it renders StepRuleDescription if isReadOnlyView is true and "name" property exists', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(StepRuleDescription).exists()).toBeTruthy(); + }); + + test('it prevents user from clicking continue if no "description" defined', () => { + const wrapper = mount( + + + + ); + + const nameInput = wrapper + .find('input[aria-describedby="detectionEngineStepAboutRuleName"]') + .at(0); + nameInput.simulate('change', { target: { value: 'Test name text' } }); + + const descriptionInput = wrapper + .find('textarea[aria-describedby="detectionEngineStepAboutRuleDescription"]') + .at(0); + const nextButton = wrapper.find('button[data-test-subj="about-continue"]').at(0); + nextButton.simulate('click'); + + expect( + wrapper + .find('input[aria-describedby="detectionEngineStepAboutRuleName"]') + .at(0) + .props().value + ).toEqual('Test name text'); + expect(descriptionInput.props().value).toEqual(''); + expect( + wrapper + .find('EuiFormRow[data-test-subj="detectionEngineStepAboutRuleDescription"] label') + .at(0) + .hasClass('euiFormLabel-isInvalid') + ).toBeTruthy(); + expect( + wrapper + .find('EuiFormRow[data-test-subj="detectionEngineStepAboutRuleDescription"] EuiTextArea') + .at(0) + .prop('isInvalid') + ).toBeTruthy(); + }); + + test('it prevents user from clicking continue if no "name" defined', () => { + const wrapper = mount( + + + + ); + + const descriptionInput = wrapper + .find('textarea[aria-describedby="detectionEngineStepAboutRuleDescription"]') + .at(0); + descriptionInput.simulate('change', { target: { value: 'Test description text' } }); + + const nameInput = wrapper + .find('input[aria-describedby="detectionEngineStepAboutRuleName"]') + .at(0); + const nextButton = wrapper.find('button[data-test-subj="about-continue"]').at(0); + nextButton.simulate('click'); + + expect( + wrapper + .find('textarea[aria-describedby="detectionEngineStepAboutRuleDescription"]') + .at(0) + .props().value + ).toEqual('Test description text'); + expect(nameInput.props().value).toEqual(''); + expect( + wrapper + .find('EuiFormRow[data-test-subj="detectionEngineStepAboutRuleName"] label') + .at(0) + .hasClass('euiFormLabel-isInvalid') + ).toBeTruthy(); + expect( + wrapper + .find('EuiFormRow[data-test-subj="detectionEngineStepAboutRuleName"] EuiFieldText') + .at(0) + .prop('isInvalid') + ).toBeTruthy(); + }); + + test('it allows user to click continue if "name" and "description" are defined', () => { + const wrapper = mount( + + + + ); + + const descriptionInput = wrapper + .find('textarea[aria-describedby="detectionEngineStepAboutRuleDescription"]') + .at(0); + descriptionInput.simulate('change', { target: { value: 'Test description text' } }); + + const nameInput = wrapper + .find('input[aria-describedby="detectionEngineStepAboutRuleName"]') + .at(0); + nameInput.simulate('change', { target: { value: 'Test name text' } }); + + const nextButton = wrapper.find('button[data-test-subj="about-continue"]').at(0); + nextButton.simulate('click'); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_about_rule/index.tsx b/x-pack/plugins/siem/public/alerts/components/rules/step_about_rule/index.tsx new file mode 100644 index 00000000000000..78ae3e44705c70 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/step_about_rule/index.tsx @@ -0,0 +1,283 @@ +/* + * 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 { EuiAccordion, EuiFlexItem, EuiSpacer, EuiButtonEmpty } from '@elastic/eui'; +import React, { FC, memo, useCallback, useEffect, useState } from 'react'; +import styled from 'styled-components'; +import deepEqual from 'fast-deep-equal'; + +import { + RuleStepProps, + RuleStep, + AboutStepRule, +} from '../../../pages/detection_engine/rules/types'; +import { AddItem } from '../add_item_form'; +import { StepRuleDescription } from '../description_step'; +import { AddMitreThreat } from '../mitre'; +import { + Field, + Form, + FormDataProvider, + getUseField, + UseField, + useForm, +} from '../../../../shared_imports'; + +import { defaultRiskScoreBySeverity, severityOptions, SeverityValue } from './data'; +import { stepAboutDefaultValue } from './default_value'; +import { isUrlInvalid } from './helpers'; +import { schema } from './schema'; +import * as I18n from './translations'; +import { StepContentWrapper } from '../step_content_wrapper'; +import { NextStep } from '../next_step'; +import { MarkdownEditorForm } from '../../../../common/components/markdown_editor/form'; +import { setFieldValue } from '../../../pages/detection_engine/rules/helpers'; + +const CommonUseField = getUseField({ component: Field }); + +interface StepAboutRuleProps extends RuleStepProps { + defaultValues?: AboutStepRule | null; +} + +const ThreeQuartersContainer = styled.div` + max-width: 740px; +`; + +ThreeQuartersContainer.displayName = 'ThreeQuartersContainer'; + +const TagContainer = styled.div` + margin-top: 16px; +`; + +TagContainer.displayName = 'TagContainer'; + +const AdvancedSettingsAccordion = styled(EuiAccordion)` + .euiAccordion__iconWrapper { + display: none; + } + + .euiAccordion__childWrapper { + transition-duration: 1ms; /* hack to fire Step accordion to set proper content's height */ + } + + &.euiAccordion-isOpen .euiButtonEmpty__content > svg { + transform: rotate(90deg); + } +`; + +const AdvancedSettingsAccordionButton = ( + + {I18n.ADVANCED_SETTINGS} + +); + +const StepAboutRuleComponent: FC = ({ + addPadding = false, + defaultValues, + descriptionColumns = 'singleSplit', + isReadOnlyView, + isUpdateView = false, + isLoading, + setForm, + setStepData, +}) => { + const [myStepData, setMyStepData] = useState(stepAboutDefaultValue); + + const { form } = useForm({ + defaultValue: myStepData, + options: { stripEmptyFields: false }, + schema, + }); + + const onSubmit = useCallback(async () => { + if (setStepData) { + setStepData(RuleStep.aboutRule, null, false); + const { isValid, data } = await form.submit(); + if (isValid) { + setStepData(RuleStep.aboutRule, data, isValid); + setMyStepData({ ...data, isNew: false } as AboutStepRule); + } + } + }, [form]); + + useEffect(() => { + const { isNew, ...initDefaultValue } = myStepData; + if (defaultValues != null && !deepEqual(initDefaultValue, defaultValues)) { + const myDefaultValues = { + ...defaultValues, + isNew: false, + }; + setMyStepData(myDefaultValues); + setFieldValue(form, schema, myDefaultValues); + } + }, [defaultValues]); + + useEffect(() => { + if (setForm != null) { + setForm(RuleStep.aboutRule, form); + } + }, [form]); + + return isReadOnlyView && myStepData.name != null ? ( + + + + ) : ( + <> + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {({ severity }) => { + const newRiskScore = defaultRiskScoreBySeverity[severity as SeverityValue]; + const severityField = form.getFields().severity; + const riskScoreField = form.getFields().riskScore; + if ( + severityField.value !== severity && + newRiskScore != null && + riskScoreField.value !== newRiskScore + ) { + riskScoreField.setValue(newRiskScore); + } + return null; + }} + + +
+ + {!isUpdateView && ( + + )} + + ); +}; + +export const StepAboutRule = memo(StepAboutRuleComponent); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_about_rule/schema.tsx b/x-pack/plugins/siem/public/alerts/components/rules/step_about_rule/schema.tsx new file mode 100644 index 00000000000000..3cb5e9a0dd5f01 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/step_about_rule/schema.tsx @@ -0,0 +1,190 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +import { + FIELD_TYPES, + fieldValidators, + FormSchema, + ValidationFunc, + ERROR_CODE, +} from '../../../../shared_imports'; +import { IMitreEnterpriseAttack } from '../../../pages/detection_engine/rules/types'; +import { isMitreAttackInvalid } from '../mitre/helpers'; +import { OptionalFieldLabel } from '../optional_field_label'; +import { isUrlInvalid } from './helpers'; +import * as I18n from './translations'; + +const { emptyField } = fieldValidators; + +export const schema: FormSchema = { + name: { + type: FIELD_TYPES.TEXT, + label: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.fieldNameLabel', { + defaultMessage: 'Name', + }), + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.nameFieldRequiredError', + { + defaultMessage: 'A name is required.', + } + ) + ), + }, + ], + }, + description: { + type: FIELD_TYPES.TEXTAREA, + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldDescriptionLabel', + { + defaultMessage: 'Description', + } + ), + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.descriptionFieldRequiredError', + { + defaultMessage: 'A description is required.', + } + ) + ), + }, + ], + }, + severity: { + type: FIELD_TYPES.SUPER_SELECT, + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldSeverityLabel', + { + defaultMessage: 'Severity', + } + ), + validations: [ + { + validator: emptyField( + i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.severityFieldRequiredError', + { + defaultMessage: 'A severity is required.', + } + ) + ), + }, + ], + }, + riskScore: { + type: FIELD_TYPES.RANGE, + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldRiskScoreLabel', + { + defaultMessage: 'Risk score', + } + ), + }, + references: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldReferenceUrlsLabel', + { + defaultMessage: 'Reference URLs', + } + ), + labelAppend: OptionalFieldLabel, + validations: [ + { + validator: ( + ...args: Parameters + ): ReturnType> | undefined => { + const [{ value, path }] = args; + let hasError = false; + (value as string[]).forEach(url => { + if (isUrlInvalid(url)) { + hasError = true; + } + }); + return hasError + ? { + code: 'ERR_FIELD_FORMAT', + path, + message: I18n.URL_FORMAT_INVALID, + } + : undefined; + }, + }, + ], + }, + falsePositives: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldFalsePositiveLabel', + { + defaultMessage: 'False positive examples', + } + ), + labelAppend: OptionalFieldLabel, + }, + threat: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldMitreThreatLabel', + { + defaultMessage: 'MITRE ATT&CK\\u2122', + } + ), + labelAppend: OptionalFieldLabel, + validations: [ + { + validator: ( + ...args: Parameters + ): ReturnType> | undefined => { + const [{ value, path }] = args; + let hasError = false; + (value as IMitreEnterpriseAttack[]).forEach(v => { + if (isMitreAttackInvalid(v.tactic.name, v.technique)) { + hasError = true; + } + }); + return hasError + ? { + code: 'ERR_FIELD_MISSING', + path, + message: I18n.CUSTOM_MITRE_ATTACK_TECHNIQUES_REQUIRED, + } + : undefined; + }, + }, + ], + }, + tags: { + type: FIELD_TYPES.COMBO_BOX, + label: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTagsLabel', { + defaultMessage: 'Tags', + }), + helpText: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTagsHelpText', + { + defaultMessage: + 'Type one or more custom identifying tags for this rule. Press enter after each tag to begin a new one.', + } + ), + labelAppend: OptionalFieldLabel, + }, + note: { + type: FIELD_TYPES.TEXTAREA, + label: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.guideLabel', { + defaultMessage: 'Investigation guide', + }), + helpText: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.guideHelpText', { + defaultMessage: + 'Provide helpful information for analysts that are performing a signal investigation. This guide will appear on both the rule details page and in timelines created from signals generated by this rule.', + }), + labelAppend: OptionalFieldLabel, + }, +}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts b/x-pack/plugins/siem/public/alerts/components/rules/step_about_rule/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/translations.ts rename to x-pack/plugins/siem/public/alerts/components/rules/step_about_rule/translations.ts diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_about_rule_details/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/rules/step_about_rule_details/index.test.tsx new file mode 100644 index 00000000000000..cd499c60b12337 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/step_about_rule_details/index.test.tsx @@ -0,0 +1,170 @@ +/* + * 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, shallow } from 'enzyme'; +import { EuiProgress, EuiButtonGroup } from '@elastic/eui'; +import { ThemeProvider } from 'styled-components'; +import euiDarkVars from '@elastic/eui/dist/eui_theme_light.json'; + +import { StepAboutRuleToggleDetails } from '.'; +import { mockAboutStepRule } from '../../../pages/detection_engine/rules/all/__mocks__/mock'; +import { HeaderSection } from '../../../../common/components/header_section'; +import { StepAboutRule } from '../step_about_rule'; +import { AboutStepRule } from '../../../pages/detection_engine/rules/types'; + +jest.mock('../../../../common/lib/kibana'); + +const theme = () => ({ eui: euiDarkVars, darkMode: true }); + +describe('StepAboutRuleToggleDetails', () => { + let mockRule: AboutStepRule; + + beforeEach(() => { + mockRule = mockAboutStepRule(); + }); + + test('it renders loading component when "loading" is true', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(EuiProgress).exists()).toBeTruthy(); + expect(wrapper.find(HeaderSection).exists()).toBeTruthy(); + }); + + test('it does not render details if stepDataDetails is null', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(StepAboutRule).exists()).toBeFalsy(); + }); + + test('it does not render details if stepData is null', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(StepAboutRule).exists()).toBeFalsy(); + }); + + describe('note value is empty string', () => { + test('it does not render toggle buttons', () => { + const mockAboutStepWithoutNote = { + ...mockRule, + note: '', + }; + const wrapper = shallow( + + ); + + expect(wrapper.find('[data-test-subj="stepAboutDetailsToggle"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="stepAboutDetailsNoteContent"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="stepAboutDetailsContent"]').exists()).toBeTruthy(); + }); + }); + + describe('note value does exist', () => { + test('it renders toggle buttons, defaulted to "details"', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(EuiButtonGroup).exists()).toBeTruthy(); + expect( + wrapper + .find('EuiButtonToggle[id="details"]') + .at(0) + .prop('isSelected') + ).toBeTruthy(); + expect( + wrapper + .find('EuiButtonToggle[id="notes"]') + .at(0) + .prop('isSelected') + ).toBeFalsy(); + }); + + test('it allows users to toggle between "details" and "note"', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('EuiButtonGroup[idSelected="details"]').exists()).toBeTruthy(); + expect(wrapper.find('EuiButtonGroup[idSelected="notes"]').exists()).toBeFalsy(); + + wrapper + .find('input[title="Investigation guide"]') + .at(0) + .simulate('change', { target: { value: 'notes' } }); + + expect(wrapper.find('EuiButtonGroup[idSelected="details"]').exists()).toBeFalsy(); + expect(wrapper.find('EuiButtonGroup[idSelected="notes"]').exists()).toBeTruthy(); + }); + + test('it displays notes markdown when user toggles to "notes"', () => { + const wrapper = mount( + + + + ); + + wrapper + .find('input[title="Investigation guide"]') + .at(0) + .simulate('change', { target: { value: 'notes' } }); + + expect(wrapper.find('EuiButtonGroup[idSelected="notes"]').exists()).toBeTruthy(); + expect(wrapper.find('Markdown h1').text()).toEqual('this is some markdown documentation'); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_about_rule_details/index.tsx b/x-pack/plugins/siem/public/alerts/components/rules/step_about_rule_details/index.tsx new file mode 100644 index 00000000000000..2163ea80f673ae --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/step_about_rule_details/index.tsx @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiPanel, + EuiProgress, + EuiButtonGroup, + EuiButtonGroupOption, + EuiSpacer, + EuiFlexItem, + EuiText, + EuiFlexGroup, + EuiResizeObserver, +} from '@elastic/eui'; +import React, { memo, useCallback, useState } from 'react'; +import styled from 'styled-components'; +import { isEmpty } from 'lodash/fp'; + +import { HeaderSection } from '../../../../common/components/header_section'; +import { Markdown } from '../../../../common/components/markdown'; +import { AboutStepRule, AboutStepRuleDetails } from '../../../pages/detection_engine/rules/types'; +import * as i18n from './translations'; +import { StepAboutRule } from '../step_about_rule'; + +const MyPanel = styled(EuiPanel)` + position: relative; +`; + +const FlexGroupFullHeight = styled(EuiFlexGroup)` + height: 100%; +`; + +const VerticalOverflowContainer = styled.div((props: { maxHeight: number }) => ({ + 'max-height': `${props.maxHeight}px`, + 'overflow-y': 'hidden', +})); + +const VerticalOverflowContent = styled.div((props: { maxHeight: number }) => ({ + 'max-height': `${props.maxHeight}px`, +})); + +const AboutContent = styled.div` + height: 100%; +`; + +const toggleOptions: EuiButtonGroupOption[] = [ + { + id: 'details', + label: i18n.ABOUT_PANEL_DETAILS_TAB, + }, + { + id: 'notes', + label: i18n.ABOUT_PANEL_NOTES_TAB, + }, +]; + +interface StepPanelProps { + stepData: AboutStepRule | null; + stepDataDetails: AboutStepRuleDetails | null; + loading: boolean; +} + +const StepAboutRuleToggleDetailsComponent: React.FC = ({ + stepData, + stepDataDetails, + loading, +}) => { + const [selectedToggleOption, setToggleOption] = useState('details'); + const [aboutPanelHeight, setAboutPanelHeight] = useState(0); + + const onResize = useCallback( + (e: { height: number; width: number }) => { + setAboutPanelHeight(e.height); + }, + [setAboutPanelHeight] + ); + + return ( + + {loading && ( + <> + + + + )} + {stepData != null && stepDataDetails != null && ( + + + + {!isEmpty(stepDataDetails.note) && stepDataDetails.note.trim() !== '' && ( + { + setToggleOption(val); + }} + data-test-subj="stepAboutDetailsToggle" + /> + )} + + + + {selectedToggleOption === 'details' ? ( + + {resizeRef => ( + + + + + {stepDataDetails.description} + + + + + + + )} + + ) : ( + + + + + + )} + + + )} + + ); +}; + +export const StepAboutRuleToggleDetails = memo(StepAboutRuleToggleDetailsComponent); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/translations.ts b/x-pack/plugins/siem/public/alerts/components/rules/step_about_rule_details/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/translations.ts rename to x-pack/plugins/siem/public/alerts/components/rules/step_about_rule_details/translations.ts diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/rules/step_content_wrapper/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.test.tsx rename to x-pack/plugins/siem/public/alerts/components/rules/step_content_wrapper/index.test.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.tsx b/x-pack/plugins/siem/public/alerts/components/rules/step_content_wrapper/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_content_wrapper/index.tsx rename to x-pack/plugins/siem/public/alerts/components/rules/step_content_wrapper/index.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_define_rule/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/rules/step_define_rule/index.test.tsx new file mode 100644 index 00000000000000..6831548992ff10 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/step_define_rule/index.test.tsx @@ -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 React from 'react'; +import { shallow } from 'enzyme'; + +import { StepDefineRule } from './index'; + +jest.mock('../../../../common/lib/kibana'); + +describe('StepDefineRule', () => { + it('renders correctly', () => { + const wrapper = shallow(); + + expect(wrapper.find('Form[data-test-subj="stepDefineRule"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_define_rule/index.tsx b/x-pack/plugins/siem/public/alerts/components/rules/step_define_rule/index.tsx new file mode 100644 index 00000000000000..119f851ecdfe45 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/step_define_rule/index.tsx @@ -0,0 +1,283 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButtonEmpty, EuiFormRow } from '@elastic/eui'; +import React, { FC, memo, useCallback, useState, useEffect } from 'react'; +import styled from 'styled-components'; +import deepEqual from 'fast-deep-equal'; + +import { DEFAULT_INDEX_KEY } from '../../../../../common/constants'; +import { isMlRule } from '../../../../../common/machine_learning/helpers'; +import { hasMlAdminPermissions } from '../../../../../common/machine_learning/has_ml_admin_permissions'; +import { IIndexPattern } from '../../../../../../../../src/plugins/data/public'; +import { useFetchIndexPatterns } from '../../../../alerts/containers/detection_engine/rules'; +import { DEFAULT_TIMELINE_TITLE } from '../../../../timelines/components/timeline/translations'; +import { useMlCapabilities } from '../../../../common/components/ml_popover/hooks/use_ml_capabilities'; +import { useUiSetting$ } from '../../../../common/lib/kibana'; +import { setFieldValue } from '../../../pages/detection_engine/rules/helpers'; +import { + filterRuleFieldsForType, + RuleFields, +} from '../../../pages/detection_engine/rules/create/helpers'; +import { + DefineStepRule, + RuleStep, + RuleStepProps, +} from '../../../pages/detection_engine/rules/types'; +import { StepRuleDescription } from '../description_step'; +import { QueryBarDefineRule } from '../query_bar'; +import { SelectRuleType } from '../select_rule_type'; +import { AnomalyThresholdSlider } from '../anomaly_threshold_slider'; +import { MlJobSelect } from '../ml_job_select'; +import { PickTimeline } from '../pick_timeline'; +import { StepContentWrapper } from '../step_content_wrapper'; +import { NextStep } from '../next_step'; +import { + Field, + Form, + FormDataProvider, + getUseField, + UseField, + useForm, + FormSchema, +} from '../../../../shared_imports'; +import { schema } from './schema'; +import * as i18n from './translations'; + +const CommonUseField = getUseField({ component: Field }); + +interface StepDefineRuleProps extends RuleStepProps { + defaultValues?: DefineStepRule | null; +} + +const stepDefineDefaultValue: DefineStepRule = { + anomalyThreshold: 50, + index: [], + isNew: true, + machineLearningJobId: '', + ruleType: 'query', + queryBar: { + query: { query: '', language: 'kuery' }, + filters: [], + saved_id: undefined, + }, + timeline: { + id: null, + title: DEFAULT_TIMELINE_TITLE, + }, +}; + +const MyLabelButton = styled(EuiButtonEmpty)` + height: 18px; + font-size: 12px; + + .euiIcon { + width: 14px; + height: 14px; + } +`; + +MyLabelButton.defaultProps = { + flush: 'right', +}; + +const StepDefineRuleComponent: FC = ({ + addPadding = false, + defaultValues, + descriptionColumns = 'singleSplit', + isReadOnlyView, + isLoading, + isUpdateView = false, + setForm, + setStepData, +}) => { + const mlCapabilities = useMlCapabilities(); + const [openTimelineSearch, setOpenTimelineSearch] = useState(false); + const [indexModified, setIndexModified] = useState(false); + const [localIsMlRule, setIsMlRule] = useState(false); + const [indicesConfig] = useUiSetting$(DEFAULT_INDEX_KEY); + const [myStepData, setMyStepData] = useState({ + ...stepDefineDefaultValue, + index: indicesConfig ?? [], + }); + const [ + { browserFields, indexPatterns: indexPatternQueryBar, isLoading: indexPatternLoadingQueryBar }, + ] = useFetchIndexPatterns(myStepData.index); + + const { form } = useForm({ + defaultValue: myStepData, + options: { stripEmptyFields: false }, + schema, + }); + const clearErrors = useCallback(() => form.reset({ resetValues: false }), [form]); + + const onSubmit = useCallback(async () => { + if (setStepData) { + setStepData(RuleStep.defineRule, null, false); + const { isValid, data } = await form.submit(); + if (isValid && setStepData) { + setStepData(RuleStep.defineRule, data, isValid); + setMyStepData({ ...data, isNew: false } as DefineStepRule); + } + } + }, [form]); + + useEffect(() => { + const { isNew, ...values } = myStepData; + if (defaultValues != null && !deepEqual(values, defaultValues)) { + const newValues = { ...values, ...defaultValues, isNew: false }; + setMyStepData(newValues); + setFieldValue(form, schema, newValues); + } + }, [defaultValues, setMyStepData, setFieldValue]); + + useEffect(() => { + if (setForm != null) { + setForm(RuleStep.defineRule, form); + } + }, [form]); + + const handleResetIndices = useCallback(() => { + const indexField = form.getFields().index; + indexField.setValue(indicesConfig); + }, [form, indicesConfig]); + + const handleOpenTimelineSearch = useCallback(() => { + setOpenTimelineSearch(true); + }, []); + + const handleCloseTimelineSearch = useCallback(() => { + setOpenTimelineSearch(false); + }, []); + + return isReadOnlyView ? ( + + + + ) : ( + <> + +
+ + + <> + + {i18n.RESET_DEFAULT_INDEX} + + ) : null, + }} + componentProps={{ + idAria: 'detectionEngineStepDefineRuleIndices', + 'data-test-subj': 'detectionEngineStepDefineRuleIndices', + euiFieldProps: { + fullWidth: true, + isDisabled: isLoading, + placeholder: '', + }, + }} + /> + + {i18n.IMPORT_TIMELINE_QUERY} + + ), + }} + component={QueryBarDefineRule} + componentProps={{ + browserFields, + idAria: 'detectionEngineStepDefineRuleQueryBar', + indexPattern: indexPatternQueryBar, + isDisabled: isLoading, + isLoading: indexPatternLoadingQueryBar, + dataTestSubj: 'detectionEngineStepDefineRuleQueryBar', + openTimelineSearch, + onCloseTimelineSearch: handleCloseTimelineSearch, + }} + /> + + + + <> + + + + + + + {({ index, ruleType }) => { + if (index != null) { + if (deepEqual(index, indicesConfig) && indexModified) { + setIndexModified(false); + } else if (!deepEqual(index, indicesConfig) && !indexModified) { + setIndexModified(true); + } + } + + if (isMlRule(ruleType) && !localIsMlRule) { + setIsMlRule(true); + clearErrors(); + } else if (!isMlRule(ruleType) && localIsMlRule) { + setIsMlRule(false); + clearErrors(); + } + + return null; + }} + + +
+ + {!isUpdateView && ( + + )} + + ); +}; + +export const StepDefineRule = memo(StepDefineRuleComponent); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_define_rule/schema.tsx b/x-pack/plugins/siem/public/alerts/components/rules/step_define_rule/schema.tsx new file mode 100644 index 00000000000000..babfebb2c6ca7c --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/step_define_rule/schema.tsx @@ -0,0 +1,176 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { EuiText } from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; +import React from 'react'; + +import { isMlRule } from '../../../../../common/machine_learning/helpers'; +import { esKuery } from '../../../../../../../../src/plugins/data/public'; +import { FieldValueQueryBar } from '../query_bar'; +import { + ERROR_CODE, + FIELD_TYPES, + fieldValidators, + FormSchema, + ValidationFunc, +} from '../../../../shared_imports'; +import { CUSTOM_QUERY_REQUIRED, INVALID_CUSTOM_QUERY, INDEX_HELPER_TEXT } from './translations'; + +export const schema: FormSchema = { + index: { + type: FIELD_TYPES.COMBO_BOX, + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.fiedIndexPatternsLabel', + { + defaultMessage: 'Index patterns', + } + ), + helpText: {INDEX_HELPER_TEXT}, + validations: [ + { + validator: ( + ...args: Parameters + ): ReturnType> | undefined => { + const [{ formData }] = args; + const needsValidation = !isMlRule(formData.ruleType); + + if (!needsValidation) { + return; + } + + return fieldValidators.emptyField( + i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.outputIndiceNameFieldRequiredError', + { + defaultMessage: 'A minimum of one index pattern is required.', + } + ) + )(...args); + }, + }, + ], + }, + queryBar: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldQuerBarLabel', + { + defaultMessage: 'Custom query', + } + ), + validations: [ + { + validator: ( + ...args: Parameters + ): ReturnType> | undefined => { + const [{ value, path, formData }] = args; + const { query, filters } = value as FieldValueQueryBar; + const needsValidation = !isMlRule(formData.ruleType); + if (!needsValidation) { + return; + } + + return isEmpty(query.query as string) && isEmpty(filters) + ? { + code: 'ERR_FIELD_MISSING', + path, + message: CUSTOM_QUERY_REQUIRED, + } + : undefined; + }, + }, + { + validator: ( + ...args: Parameters + ): ReturnType> | undefined => { + const [{ value, path, formData }] = args; + const { query } = value as FieldValueQueryBar; + const needsValidation = !isMlRule(formData.ruleType); + if (!needsValidation) { + return; + } + + if (!isEmpty(query.query as string) && query.language === 'kuery') { + try { + esKuery.fromKueryExpression(query.query); + } catch (err) { + return { + code: 'ERR_FIELD_FORMAT', + path, + message: INVALID_CUSTOM_QUERY, + }; + } + } + }, + }, + ], + }, + ruleType: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldRuleTypeLabel', + { + defaultMessage: 'Rule type', + } + ), + validations: [], + }, + anomalyThreshold: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldAnomalyThresholdLabel', + { + defaultMessage: 'Anomaly score threshold', + } + ), + validations: [], + }, + machineLearningJobId: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldMachineLearningJobIdLabel', + { + defaultMessage: 'Machine Learning job', + } + ), + validations: [ + { + validator: ( + ...args: Parameters + ): ReturnType> | undefined => { + const [{ formData }] = args; + const needsValidation = isMlRule(formData.ruleType); + + if (!needsValidation) { + return; + } + + return fieldValidators.emptyField( + i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepDefineRule.machineLearningJobIdRequired', + { + defaultMessage: 'A Machine Learning job is required.', + } + ) + )(...args); + }, + }, + ], + }, + timeline: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTimelineTemplateLabel', + { + defaultMessage: 'Timeline template', + } + ), + helpText: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTimelineTemplateHelpText', + { + defaultMessage: + 'Select an existing timeline to use as a template when investigating generated signals.', + } + ), + }, +}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/translations.tsx b/x-pack/plugins/siem/public/alerts/components/rules/step_define_rule/translations.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/translations.tsx rename to x-pack/plugins/siem/public/alerts/components/rules/step_define_rule/translations.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/types.ts b/x-pack/plugins/siem/public/alerts/components/rules/step_define_rule/types.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/types.ts rename to x-pack/plugins/siem/public/alerts/components/rules/step_define_rule/types.ts diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/rules/step_panel/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.test.tsx rename to x-pack/plugins/siem/public/alerts/components/rules/step_panel/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_panel/index.tsx b/x-pack/plugins/siem/public/alerts/components/rules/step_panel/index.tsx new file mode 100644 index 00000000000000..ef8cfc99a3b11f --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/step_panel/index.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiPanel, EuiProgress } from '@elastic/eui'; +import React, { memo } from 'react'; +import styled from 'styled-components'; + +import { HeaderSection } from '../../../../common/components/header_section'; + +interface StepPanelProps { + children: React.ReactNode; + loading: boolean; + title: string; +} + +const MyPanel = styled(EuiPanel)` + position: relative; +`; + +MyPanel.displayName = 'MyPanel'; + +const StepPanelComponent: React.FC = ({ children, loading, title }) => ( + + {loading && } + + {children} + +); + +export const StepPanel = memo(StepPanelComponent); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_rule_actions/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/rules/step_rule_actions/index.test.tsx new file mode 100644 index 00000000000000..712aacd3e3e822 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/step_rule_actions/index.test.tsx @@ -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 React from 'react'; +import { shallow } from 'enzyme'; + +import { StepRuleActions } from './index'; + +jest.mock('../../../../common/lib/kibana'); + +describe('StepRuleActions', () => { + it('renders correctly', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find('[data-test-subj="stepRuleActions"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_rule_actions/index.tsx b/x-pack/plugins/siem/public/alerts/components/rules/step_rule_actions/index.tsx new file mode 100644 index 00000000000000..2d22f0a3437f1c --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/step_rule_actions/index.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 { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton, EuiSpacer } from '@elastic/eui'; +import React, { FC, memo, useCallback, useEffect, useMemo, useState } from 'react'; +import deepEqual from 'fast-deep-equal'; + +import { setFieldValue } from '../../../pages/detection_engine/rules/helpers'; +import { + RuleStep, + RuleStepProps, + ActionsStepRule, +} from '../../../pages/detection_engine/rules/types'; +import { StepRuleDescription } from '../description_step'; +import { Form, UseField, useForm } from '../../../../shared_imports'; +import { StepContentWrapper } from '../step_content_wrapper'; +import { ThrottleSelectField, THROTTLE_OPTIONS } from '../throttle_select_field'; +import { RuleActionsField } from '../rule_actions_field'; +import { useKibana } from '../../../../common/lib/kibana'; +import { schema } from './schema'; +import * as I18n from './translations'; + +interface StepRuleActionsProps extends RuleStepProps { + defaultValues?: ActionsStepRule | null; + actionMessageParams: string[]; +} + +const stepActionsDefaultValue = { + enabled: true, + isNew: true, + actions: [], + kibanaSiemAppUrl: '', + throttle: THROTTLE_OPTIONS[0].value, +}; + +const GhostFormField = () => <>; + +const StepRuleActionsComponent: FC = ({ + addPadding = false, + defaultValues, + isReadOnlyView, + isLoading, + isUpdateView = false, + setStepData, + setForm, + actionMessageParams, +}) => { + const [myStepData, setMyStepData] = useState(stepActionsDefaultValue); + const { + services: { application }, + } = useKibana(); + + const { form } = useForm({ + defaultValue: myStepData, + options: { stripEmptyFields: false }, + schema, + }); + + const kibanaAbsoluteUrl = useMemo(() => application.getUrlForApp('siem', { absolute: true }), [ + application, + ]); + + const onSubmit = useCallback( + async (enabled: boolean) => { + if (setStepData) { + setStepData(RuleStep.ruleActions, null, false); + const { isValid: newIsValid, data } = await form.submit(); + if (newIsValid) { + setStepData(RuleStep.ruleActions, { ...data, enabled }, newIsValid); + setMyStepData({ ...data, isNew: false } as ActionsStepRule); + } + } + }, + [form] + ); + + useEffect(() => { + const { isNew, ...initDefaultValue } = myStepData; + if (defaultValues != null && !deepEqual(initDefaultValue, defaultValues)) { + const myDefaultValues = { + ...defaultValues, + isNew: false, + }; + setMyStepData(myDefaultValues); + setFieldValue(form, schema, myDefaultValues); + } + }, [defaultValues]); + + useEffect(() => { + if (setForm != null) { + setForm(RuleStep.ruleActions, form); + } + }, [form]); + + const updateThrottle = useCallback(throttle => setMyStepData({ ...myStepData, throttle }), [ + myStepData, + setMyStepData, + ]); + + const throttleFieldComponentProps = useMemo( + () => ({ + idAria: 'detectionEngineStepRuleActionsThrottle', + isDisabled: isLoading, + dataTestSubj: 'detectionEngineStepRuleActionsThrottle', + hasNoInitialSelection: false, + handleChange: updateThrottle, + euiFieldProps: { + options: THROTTLE_OPTIONS, + }, + }), + [isLoading, updateThrottle] + ); + + return isReadOnlyView && myStepData != null ? ( + + + + ) : ( + <> + +
+ + {myStepData.throttle !== stepActionsDefaultValue.throttle && ( + <> + + + + + )} + + +
+ + {!isUpdateView && ( + <> + + + + + {I18n.COMPLETE_WITHOUT_ACTIVATING} + + + + + {I18n.COMPLETE_WITH_ACTIVATING} + + + + + )} + + ); +}; + +export const StepRuleActions = memo(StepRuleActionsComponent); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_rule_actions/schema.tsx b/x-pack/plugins/siem/public/alerts/components/rules/step_rule_actions/schema.tsx new file mode 100644 index 00000000000000..a978e038985f64 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/step_rule_actions/schema.tsx @@ -0,0 +1,32 @@ +/* + * 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. + */ + +/* istanbul ignore file */ + +import { i18n } from '@kbn/i18n'; + +import { FormSchema } from '../../../../shared_imports'; + +export const schema: FormSchema = { + actions: {}, + enabled: {}, + kibanaSiemAppUrl: {}, + throttle: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepRuleActions.fieldThrottleLabel', + { + defaultMessage: 'Actions frequency', + } + ), + helpText: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepRuleActions.fieldThrottleHelpText', + { + defaultMessage: + 'Select when automated actions should be performed if a rule evaluates as true.', + } + ), + }, +}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/translations.tsx b/x-pack/plugins/siem/public/alerts/components/rules/step_rule_actions/translations.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/translations.tsx rename to x-pack/plugins/siem/public/alerts/components/rules/step_rule_actions/translations.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_schedule_rule/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/rules/step_schedule_rule/index.test.tsx new file mode 100644 index 00000000000000..6f5eddfe051a13 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/step_schedule_rule/index.test.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow, mount } from 'enzyme'; + +import { TestProviders } from '../../../../common/mock'; +import { StepScheduleRule } from './index'; + +describe('StepScheduleRule', () => { + it('renders correctly', () => { + const wrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect(wrapper.find('Form[data-test-subj="stepScheduleRule"]')).toHaveLength(1); + }); + + it('renders correctly if isReadOnlyView', () => { + const wrapper = shallow(); + + expect(wrapper.find('StepContentWrapper')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_schedule_rule/index.tsx b/x-pack/plugins/siem/public/alerts/components/rules/step_schedule_rule/index.tsx new file mode 100644 index 00000000000000..fa49637a0c830d --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/step_schedule_rule/index.tsx @@ -0,0 +1,126 @@ +/* + * 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, memo, useCallback, useEffect, useState } from 'react'; +import deepEqual from 'fast-deep-equal'; +import styled from 'styled-components'; + +import { setFieldValue } from '../../../pages/detection_engine/rules/helpers'; +import { + RuleStep, + RuleStepProps, + ScheduleStepRule, +} from '../../../pages/detection_engine/rules/types'; +import { StepRuleDescription } from '../description_step'; +import { ScheduleItem } from '../schedule_item_form'; +import { Form, UseField, useForm } from '../../../../shared_imports'; +import { StepContentWrapper } from '../step_content_wrapper'; +import { NextStep } from '../next_step'; +import { schema } from './schema'; + +interface StepScheduleRuleProps extends RuleStepProps { + defaultValues?: ScheduleStepRule | null; +} + +const RestrictedWidthContainer = styled.div` + max-width: 300px; +`; + +const stepScheduleDefaultValue = { + interval: '5m', + isNew: true, + from: '1m', +}; + +const StepScheduleRuleComponent: FC = ({ + addPadding = false, + defaultValues, + descriptionColumns = 'singleSplit', + isReadOnlyView, + isLoading, + isUpdateView = false, + setStepData, + setForm, +}) => { + const [myStepData, setMyStepData] = useState(stepScheduleDefaultValue); + + const { form } = useForm({ + defaultValue: myStepData, + options: { stripEmptyFields: false }, + schema, + }); + + const onSubmit = useCallback(async () => { + if (setStepData) { + setStepData(RuleStep.scheduleRule, null, false); + const { isValid: newIsValid, data } = await form.submit(); + if (newIsValid) { + setStepData(RuleStep.scheduleRule, { ...data }, newIsValid); + setMyStepData({ ...data, isNew: false } as ScheduleStepRule); + } + } + }, [form]); + + useEffect(() => { + const { isNew, ...initDefaultValue } = myStepData; + if (defaultValues != null && !deepEqual(initDefaultValue, defaultValues)) { + const myDefaultValues = { + ...defaultValues, + isNew: false, + }; + setMyStepData(myDefaultValues); + setFieldValue(form, schema, myDefaultValues); + } + }, [defaultValues]); + + useEffect(() => { + if (setForm != null) { + setForm(RuleStep.scheduleRule, form); + } + }, [form]); + + return isReadOnlyView && myStepData != null ? ( + + + + ) : ( + <> + +
+ + + + + + +
+
+ + {!isUpdateView && ( + + )} + + ); +}; + +export const StepScheduleRule = memo(StepScheduleRuleComponent); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/step_schedule_rule/schema.tsx b/x-pack/plugins/siem/public/alerts/components/rules/step_schedule_rule/schema.tsx new file mode 100644 index 00000000000000..99ff8a67273727 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/step_schedule_rule/schema.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* istanbul ignore file */ + +import { i18n } from '@kbn/i18n'; + +import { OptionalFieldLabel } from '../optional_field_label'; +import { FormSchema } from '../../../../shared_imports'; + +export const schema: FormSchema = { + interval: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldIntervalLabel', + { + defaultMessage: 'Runs every', + } + ), + helpText: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldIntervalHelpText', + { + defaultMessage: + 'Rules run periodically and detect signals within the specified time frame.', + } + ), + }, + from: { + label: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldAdditionalLookBackLabel', + { + defaultMessage: 'Additional look-back time', + } + ), + labelAppend: OptionalFieldLabel, + helpText: i18n.translate( + 'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldAdditionalLookBackHelpText', + { + defaultMessage: 'Adds time to the look-back period to prevent missed signals.', + } + ), + }, +}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/translations.tsx b/x-pack/plugins/siem/public/alerts/components/rules/step_schedule_rule/translations.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/translations.tsx rename to x-pack/plugins/siem/public/alerts/components/rules/step_schedule_rule/translations.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/rules/throttle_select_field/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/rules/throttle_select_field/index.test.tsx new file mode 100644 index 00000000000000..2a13c40a0dd170 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/throttle_select_field/index.test.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { ThrottleSelectField } from './index'; +import { useFormFieldMock } from '../../../../common/mock'; + +describe('ThrottleSelectField', () => { + it('renders correctly', () => { + const Component = () => { + const field = useFormFieldMock(); + + return ; + }; + const wrapper = shallow(); + + expect(wrapper.dive().find('SelectField')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/components/rules/throttle_select_field/index.tsx b/x-pack/plugins/siem/public/alerts/components/rules/throttle_select_field/index.tsx new file mode 100644 index 00000000000000..281c45b19ece58 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/rules/throttle_select_field/index.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback } from 'react'; + +import { + NOTIFICATION_THROTTLE_RULE, + NOTIFICATION_THROTTLE_NO_ACTIONS, +} from '../../../../../common/constants'; +import { SelectField } from '../../../../shared_imports'; + +export const THROTTLE_OPTIONS = [ + { value: NOTIFICATION_THROTTLE_NO_ACTIONS, text: 'Perform no actions' }, + { value: NOTIFICATION_THROTTLE_RULE, text: 'On each rule execution' }, + { value: '1h', text: 'Hourly' }, + { value: '1d', text: 'Daily' }, + { value: '7d', text: 'Weekly' }, +]; + +type ThrottleSelectField = typeof SelectField; + +export const ThrottleSelectField: ThrottleSelectField = props => { + const onChange = useCallback( + e => { + const throttle = e.target.value; + props.field.setValue(throttle); + props.handleChange(throttle); + }, + [props.field.setValue, props.handleChange] + ); + const newEuiFieldProps = { ...props.euiFieldProps, onChange }; + return ; +}; diff --git a/x-pack/plugins/siem/public/alerts/components/signals/actions.test.tsx b/x-pack/plugins/siem/public/alerts/components/signals/actions.test.tsx new file mode 100644 index 00000000000000..d3be87ce7c39cc --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/signals/actions.test.tsx @@ -0,0 +1,384 @@ +/* + * 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 sinon from 'sinon'; +import moment from 'moment'; + +import { sendSignalToTimelineAction, determineToAndFrom } from './actions'; +import { + mockEcsDataWithSignal, + defaultTimelineProps, + apolloClient, + mockTimelineApolloResult, +} from '../../../common/mock/'; +import { CreateTimeline, UpdateTimelineLoading } from './types'; +import { Ecs } from '../../../graphql/types'; +import { TimelineType } from '../../../../common/types/timeline'; + +jest.mock('apollo-client'); + +describe('signals actions', () => { + const anchor = '2020-03-01T17:59:46.349Z'; + const unix = moment(anchor).valueOf(); + let createTimeline: CreateTimeline; + let updateTimelineIsLoading: UpdateTimelineLoading; + let clock: sinon.SinonFakeTimers; + + beforeEach(() => { + // jest carries state between mocked implementations when using + // spyOn. So now we're doing all three of these. + // https://github.com/facebook/jest/issues/7136#issuecomment-565976599 + jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); + + createTimeline = jest.fn() as jest.Mocked; + updateTimelineIsLoading = jest.fn() as jest.Mocked; + + jest.spyOn(apolloClient, 'query').mockResolvedValue(mockTimelineApolloResult); + + clock = sinon.useFakeTimers(unix); + }); + + afterEach(() => { + clock.restore(); + }); + + describe('sendSignalToTimelineAction', () => { + describe('timeline id is NOT empty string and apollo client exists', () => { + test('it invokes updateTimelineIsLoading to set to true', async () => { + await sendSignalToTimelineAction({ + apolloClient, + createTimeline, + ecsData: mockEcsDataWithSignal, + updateTimelineIsLoading, + }); + + expect(updateTimelineIsLoading).toHaveBeenCalledTimes(1); + expect(updateTimelineIsLoading).toHaveBeenCalledWith({ id: 'timeline-1', isLoading: true }); + }); + + test('it invokes createTimeline with designated timeline template if "timelineTemplate" exists', async () => { + await sendSignalToTimelineAction({ + apolloClient, + createTimeline, + ecsData: mockEcsDataWithSignal, + updateTimelineIsLoading, + }); + const expected = { + from: 1541444305937, + timeline: { + columns: [ + { + aggregatable: undefined, + category: undefined, + columnHeaderType: 'not-filtered', + description: undefined, + example: undefined, + id: '@timestamp', + placeholder: undefined, + type: undefined, + width: 190, + }, + { + aggregatable: undefined, + category: undefined, + columnHeaderType: 'not-filtered', + description: undefined, + example: undefined, + id: 'message', + placeholder: undefined, + type: undefined, + width: 180, + }, + { + aggregatable: undefined, + category: undefined, + columnHeaderType: 'not-filtered', + description: undefined, + example: undefined, + id: 'event.category', + placeholder: undefined, + type: undefined, + width: 180, + }, + { + aggregatable: undefined, + category: undefined, + columnHeaderType: 'not-filtered', + description: undefined, + example: undefined, + id: 'host.name', + placeholder: undefined, + type: undefined, + width: 180, + }, + { + aggregatable: undefined, + category: undefined, + columnHeaderType: 'not-filtered', + description: undefined, + example: undefined, + id: 'source.ip', + placeholder: undefined, + type: undefined, + width: 180, + }, + { + aggregatable: undefined, + category: undefined, + columnHeaderType: 'not-filtered', + description: undefined, + example: undefined, + id: 'destination.ip', + placeholder: undefined, + type: undefined, + width: 180, + }, + { + aggregatable: undefined, + category: undefined, + columnHeaderType: 'not-filtered', + description: undefined, + example: undefined, + id: 'user.name', + placeholder: undefined, + type: undefined, + width: 180, + }, + ], + dataProviders: [], + dateRange: { + end: 1541444605937, + start: 1541444305937, + }, + deletedEventIds: [], + description: 'This is a sample rule description', + eventIdToNoteIds: {}, + eventType: 'all', + filters: [ + { + $state: { + store: 'appState', + }, + meta: { + key: 'host.name', + negate: false, + params: { + query: 'apache', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'host.name': 'apache', + }, + }, + }, + ], + highlightedDropAndProviderId: '', + historyIds: [], + id: '', + isFavorite: false, + isLive: false, + isLoading: false, + isSaving: false, + isSelectAllChecked: false, + itemsPerPage: 25, + itemsPerPageOptions: [10, 25, 50, 100], + kqlMode: 'filter', + kqlQuery: { + filterQuery: { + kuery: { + expression: '', + kind: 'kuery', + }, + serializedQuery: '', + }, + filterQueryDraft: { + expression: '', + kind: 'kuery', + }, + }, + loadingEventIds: [], + noteIds: [], + pinnedEventIds: {}, + pinnedEventsSaveObject: {}, + savedObjectId: null, + selectedEventIds: {}, + show: true, + showCheckboxes: false, + showRowRenderers: true, + sort: { + columnId: '@timestamp', + sortDirection: 'desc', + }, + title: '', + timelineType: TimelineType.default, + templateTimelineId: null, + templateTimelineVersion: null, + version: null, + width: 1100, + }, + to: 1541444605937, + ruleNote: '# this is some markdown documentation', + }; + + expect(createTimeline).toHaveBeenCalledWith(expected); + }); + + test('it invokes createTimeline with kqlQuery.filterQuery.kuery.kind as "kuery" if not specified in returned timeline template', async () => { + const mockTimelineApolloResultModified = { + ...mockTimelineApolloResult, + kqlQuery: { + filterQuery: { + kuery: { + expression: [''], + }, + }, + filterQueryDraft: { + expression: [''], + }, + }, + }; + jest.spyOn(apolloClient, 'query').mockResolvedValue(mockTimelineApolloResultModified); + + await sendSignalToTimelineAction({ + apolloClient, + createTimeline, + ecsData: mockEcsDataWithSignal, + updateTimelineIsLoading, + }); + // @ts-ignore + const createTimelineArg = createTimeline.mock.calls[0][0]; + + expect(createTimeline).toHaveBeenCalledTimes(1); + expect(createTimelineArg.timeline.kqlQuery.filterQuery.kuery.kind).toEqual('kuery'); + }); + + test('it invokes createTimeline with kqlQuery.filterQueryDraft.kuery.kind as "kuery" if not specified in returned timeline template', async () => { + const mockTimelineApolloResultModified = { + ...mockTimelineApolloResult, + kqlQuery: { + filterQuery: { + kuery: { + expression: [''], + }, + }, + filterQueryDraft: { + expression: [''], + }, + }, + }; + jest.spyOn(apolloClient, 'query').mockResolvedValue(mockTimelineApolloResultModified); + + await sendSignalToTimelineAction({ + apolloClient, + createTimeline, + ecsData: mockEcsDataWithSignal, + updateTimelineIsLoading, + }); + // @ts-ignore + const createTimelineArg = createTimeline.mock.calls[0][0]; + + expect(createTimeline).toHaveBeenCalledTimes(1); + expect(createTimelineArg.timeline.kqlQuery.filterQueryDraft.kind).toEqual('kuery'); + }); + + test('it invokes createTimeline with default timeline if apolloClient throws', async () => { + jest.spyOn(apolloClient, 'query').mockImplementation(() => { + throw new Error('Test error'); + }); + + await sendSignalToTimelineAction({ + apolloClient, + createTimeline, + ecsData: mockEcsDataWithSignal, + updateTimelineIsLoading, + }); + + expect(updateTimelineIsLoading).toHaveBeenCalledWith({ id: 'timeline-1', isLoading: true }); + expect(updateTimelineIsLoading).toHaveBeenCalledWith({ + id: 'timeline-1', + isLoading: false, + }); + expect(createTimeline).toHaveBeenCalledTimes(1); + expect(createTimeline).toHaveBeenCalledWith(defaultTimelineProps); + }); + }); + + describe('timelineId is empty string', () => { + test('it invokes createTimeline with timelineDefaults', async () => { + const ecsDataMock: Ecs = { + ...mockEcsDataWithSignal, + signal: { + rule: { + ...mockEcsDataWithSignal.signal?.rule!, + timeline_id: null, + }, + }, + }; + + await sendSignalToTimelineAction({ + apolloClient, + createTimeline, + ecsData: ecsDataMock, + updateTimelineIsLoading, + }); + + expect(updateTimelineIsLoading).not.toHaveBeenCalled(); + expect(createTimeline).toHaveBeenCalledTimes(1); + expect(createTimeline).toHaveBeenCalledWith(defaultTimelineProps); + }); + }); + + describe('apolloClient is not defined', () => { + test('it invokes createTimeline with timelineDefaults', async () => { + const ecsDataMock: Ecs = { + ...mockEcsDataWithSignal, + signal: { + rule: { + ...mockEcsDataWithSignal.signal?.rule!, + timeline_id: [''], + }, + }, + }; + + await sendSignalToTimelineAction({ + createTimeline, + ecsData: ecsDataMock, + updateTimelineIsLoading, + }); + + expect(updateTimelineIsLoading).not.toHaveBeenCalled(); + expect(createTimeline).toHaveBeenCalledTimes(1); + expect(createTimeline).toHaveBeenCalledWith(defaultTimelineProps); + }); + }); + }); + + describe('determineToAndFrom', () => { + test('it uses ecs.Data.timestamp if one is provided', () => { + const ecsDataMock: Ecs = { + ...mockEcsDataWithSignal, + timestamp: '2020-03-20T17:59:46.349Z', + }; + const result = determineToAndFrom({ ecsData: ecsDataMock }); + + expect(result.from).toEqual(1584726886349); + expect(result.to).toEqual(1584727186349); + }); + + test('it uses current time timestamp if ecsData.timestamp is not provided', () => { + const { timestamp, ...ecsDataMock } = { + ...mockEcsDataWithSignal, + }; + const result = determineToAndFrom({ ecsData: ecsDataMock }); + + expect(result.from).toEqual(1583085286349); + expect(result.to).toEqual(1583085586349); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/components/signals/actions.tsx b/x-pack/plugins/siem/public/alerts/components/signals/actions.tsx new file mode 100644 index 00000000000000..044633da62f610 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/signals/actions.tsx @@ -0,0 +1,210 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import dateMath from '@elastic/datemath'; +import { getOr, isEmpty } from 'lodash/fp'; +import moment from 'moment'; + +import { updateSignalStatus } from '../../containers/detection_engine/signals/api'; +import { SendSignalToTimelineActionProps, UpdateSignalStatusActionProps } from './types'; +import { TimelineNonEcsData, GetOneTimeline, TimelineResult, Ecs } from '../../../graphql/types'; +import { oneTimelineQuery } from '../../../timelines/containers/one/index.gql_query'; +import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; +import { + omitTypenameInTimeline, + formatTimelineResultToModel, +} from '../../../timelines/components/open_timeline/helpers'; +import { convertKueryToElasticSearchQuery } from '../../../common/lib/keury'; +import { + replaceTemplateFieldFromQuery, + replaceTemplateFieldFromMatchFilters, + replaceTemplateFieldFromDataProviders, +} from './helpers'; + +export const getUpdateSignalsQuery = (eventIds: Readonly) => { + return { + query: { + bool: { + filter: { + terms: { + _id: [...eventIds], + }, + }, + }, + }, + }; +}; + +export const getFilterAndRuleBounds = ( + data: TimelineNonEcsData[][] +): [string[], number, number] => { + const stringFilter = data?.[0].filter(d => d.field === 'signal.rule.filters')?.[0]?.value ?? []; + + const eventTimes = data + .flatMap(signal => signal.filter(d => d.field === 'signal.original_time')?.[0]?.value ?? []) + .map(d => moment(d)); + + return [stringFilter, moment.min(eventTimes).valueOf(), moment.max(eventTimes).valueOf()]; +}; + +export const updateSignalStatusAction = async ({ + query, + signalIds, + status, + setEventsLoading, + setEventsDeleted, +}: UpdateSignalStatusActionProps) => { + try { + setEventsLoading({ eventIds: signalIds, isLoading: true }); + + const queryObject = query ? { query: JSON.parse(query) } : getUpdateSignalsQuery(signalIds); + + await updateSignalStatus({ query: queryObject, status }); + // TODO: Only delete those that were successfully updated from updatedRules + setEventsDeleted({ eventIds: signalIds, isDeleted: true }); + } catch (e) { + // TODO: Show error toasts + } finally { + setEventsLoading({ eventIds: signalIds, isLoading: false }); + } +}; + +export const determineToAndFrom = ({ ecsData }: { ecsData: Ecs }) => { + const ellapsedTimeRule = moment.duration( + moment().diff( + dateMath.parse(ecsData.signal?.rule?.from != null ? ecsData.signal?.rule?.from[0] : 'now-0s') + ) + ); + + const from = moment(ecsData.timestamp ?? new Date()) + .subtract(ellapsedTimeRule) + .valueOf(); + const to = moment(ecsData.timestamp ?? new Date()).valueOf(); + + return { to, from }; +}; + +export const sendSignalToTimelineAction = async ({ + apolloClient, + createTimeline, + ecsData, + updateTimelineIsLoading, +}: SendSignalToTimelineActionProps) => { + let openSignalInBasicTimeline = true; + const noteContent = ecsData.signal?.rule?.note != null ? ecsData.signal?.rule?.note[0] : ''; + const timelineId = + ecsData.signal?.rule?.timeline_id != null ? ecsData.signal?.rule?.timeline_id[0] : ''; + const { to, from } = determineToAndFrom({ ecsData }); + + if (timelineId !== '' && apolloClient != null) { + try { + updateTimelineIsLoading({ id: 'timeline-1', isLoading: true }); + const responseTimeline = await apolloClient.query< + GetOneTimeline.Query, + GetOneTimeline.Variables + >({ + query: oneTimelineQuery, + fetchPolicy: 'no-cache', + variables: { + id: timelineId, + }, + }); + const resultingTimeline: TimelineResult = getOr({}, 'data.getOneTimeline', responseTimeline); + + if (!isEmpty(resultingTimeline)) { + const timelineTemplate: TimelineResult = omitTypenameInTimeline(resultingTimeline); + openSignalInBasicTimeline = false; + const { timeline } = formatTimelineResultToModel(timelineTemplate, true); + const query = replaceTemplateFieldFromQuery( + timeline.kqlQuery?.filterQuery?.kuery?.expression ?? '', + ecsData + ); + const filters = replaceTemplateFieldFromMatchFilters(timeline.filters ?? [], ecsData); + const dataProviders = replaceTemplateFieldFromDataProviders( + timeline.dataProviders ?? [], + ecsData + ); + createTimeline({ + from, + timeline: { + ...timeline, + dataProviders, + eventType: 'all', + filters, + dateRange: { + start: from, + end: to, + }, + kqlQuery: { + filterQuery: { + kuery: { + kind: timeline.kqlQuery?.filterQuery?.kuery?.kind ?? 'kuery', + expression: query, + }, + serializedQuery: convertKueryToElasticSearchQuery(query), + }, + filterQueryDraft: { + kind: timeline.kqlQuery?.filterQuery?.kuery?.kind ?? 'kuery', + expression: query, + }, + }, + show: true, + }, + to, + ruleNote: noteContent, + }); + } + } catch { + openSignalInBasicTimeline = true; + updateTimelineIsLoading({ id: 'timeline-1', isLoading: false }); + } + } + + if (openSignalInBasicTimeline) { + createTimeline({ + from, + timeline: { + ...timelineDefaults, + dataProviders: [ + { + and: [], + id: `send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-signal-id-${ecsData._id}`, + name: ecsData._id, + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: '_id', + value: ecsData._id, + operator: ':', + }, + }, + ], + id: 'timeline-1', + dateRange: { + start: from, + end: to, + }, + eventType: 'all', + kqlQuery: { + filterQuery: { + kuery: { + kind: 'kuery', + expression: '', + }, + serializedQuery: '', + }, + filterQueryDraft: { + kind: 'kuery', + expression: '', + }, + }, + }, + to, + ruleNote: noteContent, + }); + } +}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/default_config.test.tsx b/x-pack/plugins/siem/public/alerts/components/signals/default_config.test.tsx similarity index 96% rename from x-pack/plugins/siem/public/pages/detection_engine/components/signals/default_config.test.tsx rename to x-pack/plugins/siem/public/alerts/components/signals/default_config.test.tsx index 5428b9932fbde7..71da68108da7ee 100644 --- a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/default_config.test.tsx +++ b/x-pack/plugins/siem/public/alerts/components/signals/default_config.test.tsx @@ -7,8 +7,8 @@ import React from 'react'; import { mount, ReactWrapper } from 'enzyme'; import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import { Filter } from '../../../../../../../../src/plugins/data/common/es_query'; -import { TimelineAction } from '../../../../components/timeline/body/actions'; +import { Filter } from '../../../../../../../src/plugins/data/common/es_query'; +import { TimelineAction } from '../../../timelines/components/timeline/body/actions'; import { buildSignalsRuleIdFilter, getSignalsActions } from './default_config'; import { CreateTimeline, @@ -16,7 +16,7 @@ import { SetEventsLoadingProps, UpdateTimelineLoading, } from './types'; -import { mockEcsDataWithSignal } from '../../../../mock/mock_ecs'; +import { mockEcsDataWithSignal } from '../../../common/mock/mock_ecs'; import { sendSignalToTimelineAction, updateSignalStatusAction } from './actions'; import * as i18n from './translations'; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx b/x-pack/plugins/siem/public/alerts/components/signals/default_config.tsx similarity index 92% rename from x-pack/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx rename to x-pack/plugins/siem/public/alerts/components/signals/default_config.tsx index 81b643b7894dfc..05e0baba66d0a2 100644 --- a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/default_config.tsx +++ b/x-pack/plugins/siem/public/alerts/components/signals/default_config.tsx @@ -10,15 +10,18 @@ import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import ApolloClient from 'apollo-client'; import React from 'react'; -import { Filter } from '../../../../../../../../src/plugins/data/common/es_query'; -import { TimelineAction, TimelineActionProps } from '../../../../components/timeline/body/actions'; -import { defaultColumnHeaderType } from '../../../../components/timeline/body/column_headers/default_headers'; +import { Filter } from '../../../../../../../src/plugins/data/common/es_query'; +import { + TimelineAction, + TimelineActionProps, +} from '../../../timelines/components/timeline/body/actions'; +import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers'; import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH, -} from '../../../../components/timeline/body/constants'; -import { ColumnHeaderOptions, SubsetTimelineModel } from '../../../../store/timeline/model'; -import { timelineDefaults } from '../../../../store/timeline/defaults'; +} from '../../../timelines/components/timeline/body/constants'; +import { ColumnHeaderOptions, SubsetTimelineModel } from '../../../timelines/store/timeline/model'; +import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; import { FILTER_OPEN } from './signals_filter_group'; import { sendSignalToTimelineAction, updateSignalStatusAction } from './actions'; diff --git a/x-pack/plugins/siem/public/alerts/components/signals/helpers.test.ts b/x-pack/plugins/siem/public/alerts/components/signals/helpers.test.ts new file mode 100644 index 00000000000000..ad4f5cf8b4aa88 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/signals/helpers.test.ts @@ -0,0 +1,274 @@ +/* + * 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 { cloneDeep } from 'lodash/fp'; + +import { mockEcsData } from '../../../common/mock/mock_ecs'; +import { Filter } from '../../../../../../../src/plugins/data/public'; +import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider'; +import { mockDataProviders } from '../../../timelines/components/timeline/data_providers/mock/mock_data_providers'; + +import { + getStringArray, + replaceTemplateFieldFromQuery, + replaceTemplateFieldFromMatchFilters, + reformatDataProviderWithNewValue, +} from './helpers'; + +describe('helpers', () => { + let mockEcsDataClone = cloneDeep(mockEcsData); + beforeEach(() => { + mockEcsDataClone = cloneDeep(mockEcsData); + }); + describe('getStringOrStringArray', () => { + test('it should correctly return a string array', () => { + const value = getStringArray('x', { + x: 'The nickname of the developer we all :heart:', + }); + expect(value).toEqual(['The nickname of the developer we all :heart:']); + }); + + test('it should correctly return a string array with a single element', () => { + const value = getStringArray('x', { + x: ['The nickname of the developer we all :heart:'], + }); + expect(value).toEqual(['The nickname of the developer we all :heart:']); + }); + + test('it should correctly return a string array with two elements of strings', () => { + const value = getStringArray('x', { + x: ['The nickname of the developer we all :heart:', 'We are all made of stars'], + }); + expect(value).toEqual([ + 'The nickname of the developer we all :heart:', + 'We are all made of stars', + ]); + }); + + test('it should correctly return a string array with deep elements', () => { + const value = getStringArray('x.y.z', { + x: { y: { z: 'zed' } }, + }); + expect(value).toEqual(['zed']); + }); + + test('it should correctly return a string array with a non-existent value', () => { + const value = getStringArray('non.existent', { + x: { y: { z: 'zed' } }, + }); + expect(value).toEqual([]); + }); + + test('it should trace an error if the value is not a string', () => { + const mockConsole: Console = ({ trace: jest.fn() } as unknown) as Console; + const value = getStringArray('a', { a: 5 }, mockConsole); + expect(value).toEqual([]); + expect( + mockConsole.trace + ).toHaveBeenCalledWith( + 'Data type that is not a string or string array detected:', + 5, + 'when trying to access field:', + 'a', + 'from data object of:', + { a: 5 } + ); + }); + + test('it should trace an error if the value is an array of mixed values', () => { + const mockConsole: Console = ({ trace: jest.fn() } as unknown) as Console; + const value = getStringArray('a', { a: ['hi', 5] }, mockConsole); + expect(value).toEqual([]); + expect( + mockConsole.trace + ).toHaveBeenCalledWith( + 'Data type that is not a string or string array detected:', + ['hi', 5], + 'when trying to access field:', + 'a', + 'from data object of:', + { a: ['hi', 5] } + ); + }); + }); + + describe('replaceTemplateFieldFromQuery', () => { + test('given an empty query string this returns an empty query string', () => { + const replacement = replaceTemplateFieldFromQuery('', mockEcsDataClone[0]); + expect(replacement).toEqual(''); + }); + + test('given a query string with spaces this returns an empty query string', () => { + const replacement = replaceTemplateFieldFromQuery(' ', mockEcsDataClone[0]); + expect(replacement).toEqual(''); + }); + + test('it should replace a query with a template value such as apache from a mock template', () => { + const replacement = replaceTemplateFieldFromQuery( + 'host.name: placeholdertext', + mockEcsDataClone[0] + ); + expect(replacement).toEqual('host.name: apache'); + }); + + test('it should replace a template field with an ECS value that is not an array', () => { + mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case + const replacement = replaceTemplateFieldFromQuery('host.name: *', mockEcsDataClone[0]); + expect(replacement).toEqual('host.name: *'); + }); + + test('it should NOT replace a query with a template value that is not part of the template fields array', () => { + const replacement = replaceTemplateFieldFromQuery( + 'user.id: placeholdertext', + mockEcsDataClone[0] + ); + expect(replacement).toEqual('user.id: placeholdertext'); + }); + }); + + describe('replaceTemplateFieldFromMatchFilters', () => { + test('given an empty query filter this will return an empty filter', () => { + const replacement = replaceTemplateFieldFromMatchFilters([], mockEcsDataClone[0]); + expect(replacement).toEqual([]); + }); + + test('given a query filter this will return that filter with the placeholder replaced', () => { + const filters: Filter[] = [ + { + meta: { + type: 'phrase', + key: 'host.name', + alias: 'alias', + disabled: false, + negate: false, + params: { query: 'Braden' }, + }, + query: { match_phrase: { 'host.name': 'Braden' } }, + }, + ]; + const replacement = replaceTemplateFieldFromMatchFilters(filters, mockEcsDataClone[0]); + const expected: Filter[] = [ + { + meta: { + type: 'phrase', + key: 'host.name', + alias: 'alias', + disabled: false, + negate: false, + params: { query: 'apache' }, + }, + query: { match_phrase: { 'host.name': 'apache' } }, + }, + ]; + expect(replacement).toEqual(expected); + }); + + test('given a query filter with a value not in the templateFields, this will NOT replace the placeholder value', () => { + const filters: Filter[] = [ + { + meta: { + type: 'phrase', + key: 'user.id', + alias: 'alias', + disabled: false, + negate: false, + params: { query: 'Evan' }, + }, + query: { match_phrase: { 'user.id': 'Evan' } }, + }, + ]; + const replacement = replaceTemplateFieldFromMatchFilters(filters, mockEcsDataClone[0]); + const expected: Filter[] = [ + { + meta: { + type: 'phrase', + key: 'user.id', + alias: 'alias', + disabled: false, + negate: false, + params: { query: 'Evan' }, + }, + query: { match_phrase: { 'user.id': 'Evan' } }, + }, + ]; + expect(replacement).toEqual(expected); + }); + }); + + describe('reformatDataProviderWithNewValue', () => { + test('it should replace a query with a template value such as apache from a mock data provider', () => { + const mockDataProvider: DataProvider = mockDataProviders[0]; + mockDataProvider.queryMatch.field = 'host.name'; + mockDataProvider.id = 'Braden'; + mockDataProvider.name = 'Braden'; + mockDataProvider.queryMatch.value = 'Braden'; + const replacement = reformatDataProviderWithNewValue(mockDataProvider, mockEcsDataClone[0]); + expect(replacement).toEqual({ + id: 'apache', + name: 'apache', + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'host.name', + value: 'apache', + operator: ':', + displayField: undefined, + displayValue: undefined, + }, + and: [], + }); + }); + + test('it should replace a query with a template value such as apache from a mock data provider using a string in the data provider', () => { + mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case + const mockDataProvider: DataProvider = mockDataProviders[0]; + mockDataProvider.queryMatch.field = 'host.name'; + mockDataProvider.id = 'Braden'; + mockDataProvider.name = 'Braden'; + mockDataProvider.queryMatch.value = 'Braden'; + const replacement = reformatDataProviderWithNewValue(mockDataProvider, mockEcsDataClone[0]); + expect(replacement).toEqual({ + id: 'apache', + name: 'apache', + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'host.name', + value: 'apache', + operator: ':', + displayField: undefined, + displayValue: undefined, + }, + and: [], + }); + }); + + test('it should NOT replace a query with a template value that is not part of a template such as user.id', () => { + const mockDataProvider: DataProvider = mockDataProviders[0]; + mockDataProvider.queryMatch.field = 'user.id'; + mockDataProvider.id = 'my-id'; + mockDataProvider.name = 'Rebecca'; + mockDataProvider.queryMatch.value = 'Rebecca'; + const replacement = reformatDataProviderWithNewValue(mockDataProvider, mockEcsDataClone[0]); + expect(replacement).toEqual({ + id: 'my-id', + name: 'Rebecca', + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'user.id', + value: 'Rebecca', + operator: ':', + displayField: undefined, + displayValue: undefined, + }, + and: [], + }); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/components/signals/helpers.ts b/x-pack/plugins/siem/public/alerts/components/signals/helpers.ts new file mode 100644 index 00000000000000..5099d61254caa7 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/signals/helpers.ts @@ -0,0 +1,165 @@ +/* + * 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 { get, isEmpty } from 'lodash/fp'; +import { Filter, esKuery, KueryNode } from '../../../../../../../src/plugins/data/public'; +import { + DataProvider, + DataProvidersAnd, +} from '../../../timelines/components/timeline/data_providers/data_provider'; +import { Ecs } from '../../../graphql/types'; + +interface FindValueToChangeInQuery { + field: string; + valueToChange: string; +} + +/** + * Fields that will be replaced with the template strings from a a saved timeline template. + * This is used for the signals detection engine feature when you save a timeline template + * and are the fields you can replace when creating a template. + */ +const templateFields = [ + 'host.name', + 'host.hostname', + 'host.domain', + 'host.id', + 'host.ip', + 'client.ip', + 'destination.ip', + 'server.ip', + 'source.ip', + 'network.community_id', + 'user.name', + 'process.name', +]; + +/** + * This will return an unknown as a string array if it exists from an unknown data type and a string + * that represents the path within the data object the same as lodash's "get". If the value is non-existent + * we will return an empty array. If it is a non string value then this will log a trace to the console + * that it encountered an error and return an empty array. + * @param field string of the field to access + * @param data The unknown data that is typically a ECS value to get the value + * @param localConsole The local console which can be sent in to make this pure (for tests) or use the default console + */ +export const getStringArray = (field: string, data: unknown, localConsole = console): string[] => { + const value: unknown | undefined = get(field, data); + if (value == null) { + return []; + } else if (typeof value === 'string') { + return [value]; + } else if (Array.isArray(value) && value.every(element => typeof element === 'string')) { + return value; + } else { + localConsole.trace( + 'Data type that is not a string or string array detected:', + value, + 'when trying to access field:', + field, + 'from data object of:', + data + ); + return []; + } +}; + +export const findValueToChangeInQuery = ( + kueryNode: KueryNode, + valueToChange: FindValueToChangeInQuery[] = [] +): FindValueToChangeInQuery[] => { + let localValueToChange = valueToChange; + if (kueryNode.function === 'is' && templateFields.includes(kueryNode.arguments[0].value)) { + localValueToChange = [ + ...localValueToChange, + { + field: kueryNode.arguments[0].value, + valueToChange: kueryNode.arguments[1].value, + }, + ]; + } + return kueryNode.arguments.reduce( + (addValueToChange: FindValueToChangeInQuery[], ast: KueryNode) => { + if (ast.function === 'is' && templateFields.includes(ast.arguments[0].value)) { + return [ + ...addValueToChange, + { + field: ast.arguments[0].value, + valueToChange: ast.arguments[1].value, + }, + ]; + } + if (ast.arguments) { + return findValueToChangeInQuery(ast, addValueToChange); + } + return addValueToChange; + }, + localValueToChange + ); +}; + +export const replaceTemplateFieldFromQuery = (query: string, ecsData: Ecs): string => { + if (query.trim() !== '') { + const valueToChange = findValueToChangeInQuery(esKuery.fromKueryExpression(query)); + return valueToChange.reduce((newQuery, vtc) => { + const newValue = getStringArray(vtc.field, ecsData); + if (newValue.length) { + return newQuery.replace(vtc.valueToChange, newValue[0]); + } else { + return newQuery; + } + }, query); + } else { + return ''; + } +}; + +export const replaceTemplateFieldFromMatchFilters = (filters: Filter[], ecsData: Ecs): Filter[] => + filters.map(filter => { + if ( + filter.meta.type === 'phrase' && + filter.meta.key != null && + templateFields.includes(filter.meta.key) + ) { + const newValue = getStringArray(filter.meta.key, ecsData); + if (newValue.length) { + filter.meta.params = { query: newValue[0] }; + filter.query = { match_phrase: { [filter.meta.key]: newValue[0] } }; + } + } + return filter; + }); + +export const reformatDataProviderWithNewValue = ( + dataProvider: T, + ecsData: Ecs +): T => { + if (templateFields.includes(dataProvider.queryMatch.field)) { + const newValue = getStringArray(dataProvider.queryMatch.field, ecsData); + if (newValue.length) { + dataProvider.id = dataProvider.id.replace(dataProvider.name, newValue[0]); + dataProvider.name = newValue[0]; + dataProvider.queryMatch.value = newValue[0]; + dataProvider.queryMatch.displayField = undefined; + dataProvider.queryMatch.displayValue = undefined; + } + } + return dataProvider; +}; + +export const replaceTemplateFieldFromDataProviders = ( + dataProviders: DataProvider[], + ecsData: Ecs +): DataProvider[] => + dataProviders.map(dataProvider => { + const newDataProvider = reformatDataProviderWithNewValue(dataProvider, ecsData); + if (newDataProvider.and != null && !isEmpty(newDataProvider.and)) { + newDataProvider.and = newDataProvider.and.map(andDataProvider => + reformatDataProviderWithNewValue(andDataProvider, ecsData) + ); + } + return newDataProvider; + }); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/signals/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/components/signals/index.test.tsx rename to x-pack/plugins/siem/public/alerts/components/signals/index.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/signals/index.tsx b/x-pack/plugins/siem/public/alerts/components/signals/index.tsx new file mode 100644 index 00000000000000..eb19cfea97324f --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/signals/index.tsx @@ -0,0 +1,369 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiPanel, EuiLoadingContent } from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import { Dispatch } from 'redux'; + +import { Filter, esQuery } from '../../../../../../../src/plugins/data/public'; +import { useFetchIndexPatterns } from '../../../alerts/containers/detection_engine/rules/fetch_index_patterns'; +import { StatefulEventsViewer } from '../../../common/components/events_viewer'; +import { HeaderSection } from '../../../common/components/header_section'; +import { combineQueries } from '../../../timelines/components/timeline/helpers'; +import { useKibana } from '../../../common/lib/kibana'; +import { inputsSelectors, State, inputsModel } from '../../../common/store'; +import { timelineActions, timelineSelectors } from '../../../timelines/store/timeline'; +import { TimelineModel } from '../../../timelines/store/timeline/model'; +import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; +import { useApolloClient } from '../../../common/utils/apollo_context'; + +import { updateSignalStatusAction } from './actions'; +import { + getSignalsActions, + requiredFieldsForActions, + signalsClosedFilters, + signalsDefaultModel, + signalsOpenFilters, +} from './default_config'; +import { + FILTER_CLOSED, + FILTER_OPEN, + SignalFilterOption, + SignalsTableFilterGroup, +} from './signals_filter_group'; +import { SignalsUtilityBar } from './signals_utility_bar'; +import * as i18n from './translations'; +import { + CreateTimelineProps, + SetEventsDeletedProps, + SetEventsLoadingProps, + UpdateSignalsStatusCallback, + UpdateSignalsStatusProps, +} from './types'; +import { dispatchUpdateTimeline } from '../../../timelines/components/open_timeline/helpers'; + +export const SIGNALS_PAGE_TIMELINE_ID = 'signals-page'; + +interface OwnProps { + canUserCRUD: boolean; + defaultFilters?: Filter[]; + hasIndexWrite: boolean; + from: number; + loading: boolean; + signalsIndex: string; + to: number; +} + +type SignalsTableComponentProps = OwnProps & PropsFromRedux; + +export const SignalsTableComponent: React.FC = ({ + canUserCRUD, + clearEventsDeleted, + clearEventsLoading, + clearSelected, + defaultFilters, + from, + globalFilters, + globalQuery, + hasIndexWrite, + isSelectAllChecked, + loading, + loadingEventIds, + selectedEventIds, + setEventsDeleted, + setEventsLoading, + signalsIndex, + to, + updateTimeline, + updateTimelineIsLoading, +}) => { + const [selectAll, setSelectAll] = useState(false); + const apolloClient = useApolloClient(); + + const [showClearSelectionAction, setShowClearSelectionAction] = useState(false); + const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); + const [{ browserFields, indexPatterns }] = useFetchIndexPatterns( + signalsIndex !== '' ? [signalsIndex] : [] + ); + const kibana = useKibana(); + + const getGlobalQuery = useCallback(() => { + if (browserFields != null && indexPatterns != null) { + return combineQueries({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + dataProviders: [], + indexPattern: indexPatterns, + browserFields, + filters: isEmpty(defaultFilters) + ? globalFilters + : [...(defaultFilters ?? []), ...globalFilters], + kqlQuery: globalQuery, + kqlMode: globalQuery.language, + start: from, + end: to, + isEventViewer: true, + }); + } + return null; + }, [browserFields, globalFilters, globalQuery, indexPatterns, kibana, to, from]); + + // Callback for creating a new timeline -- utilized by row/batch actions + const createTimelineCallback = useCallback( + ({ from: fromTimeline, timeline, to: toTimeline, ruleNote }: CreateTimelineProps) => { + updateTimelineIsLoading({ id: 'timeline-1', isLoading: false }); + updateTimeline({ + duplicate: true, + from: fromTimeline, + id: 'timeline-1', + notes: [], + timeline: { + ...timeline, + show: true, + }, + to: toTimeline, + ruleNote, + })(); + }, + [updateTimeline, updateTimelineIsLoading] + ); + + const setEventsLoadingCallback = useCallback( + ({ eventIds, isLoading }: SetEventsLoadingProps) => { + setEventsLoading!({ id: SIGNALS_PAGE_TIMELINE_ID, eventIds, isLoading }); + }, + [setEventsLoading, SIGNALS_PAGE_TIMELINE_ID] + ); + + const setEventsDeletedCallback = useCallback( + ({ eventIds, isDeleted }: SetEventsDeletedProps) => { + setEventsDeleted!({ id: SIGNALS_PAGE_TIMELINE_ID, eventIds, isDeleted }); + }, + [setEventsDeleted, SIGNALS_PAGE_TIMELINE_ID] + ); + + // Catches state change isSelectAllChecked->false upon user selection change to reset utility bar + useEffect(() => { + if (!isSelectAllChecked) { + setShowClearSelectionAction(false); + } else { + setSelectAll(false); + } + }, [isSelectAllChecked]); + + // Callback for when open/closed filter changes + const onFilterGroupChangedCallback = useCallback( + (newFilterGroup: SignalFilterOption) => { + clearEventsLoading!({ id: SIGNALS_PAGE_TIMELINE_ID }); + clearEventsDeleted!({ id: SIGNALS_PAGE_TIMELINE_ID }); + clearSelected!({ id: SIGNALS_PAGE_TIMELINE_ID }); + setFilterGroup(newFilterGroup); + }, + [clearEventsLoading, clearEventsDeleted, clearSelected, setFilterGroup] + ); + + // Callback for clearing entire selection from utility bar + const clearSelectionCallback = useCallback(() => { + clearSelected!({ id: SIGNALS_PAGE_TIMELINE_ID }); + setSelectAll(false); + setShowClearSelectionAction(false); + }, [clearSelected, setSelectAll, setShowClearSelectionAction]); + + // Callback for selecting all events on all pages from utility bar + // Dispatches to stateful_body's selectAll via TimelineTypeContext props + // as scope of response data required to actually set selectedEvents + const selectAllCallback = useCallback(() => { + setSelectAll(true); + setShowClearSelectionAction(true); + }, [setSelectAll, setShowClearSelectionAction]); + + const updateSignalsStatusCallback: UpdateSignalsStatusCallback = useCallback( + async (refetchQuery: inputsModel.Refetch, { signalIds, status }: UpdateSignalsStatusProps) => { + await updateSignalStatusAction({ + query: showClearSelectionAction ? getGlobalQuery()?.filterQuery : undefined, + signalIds: Object.keys(selectedEventIds), + status, + setEventsDeleted: setEventsDeletedCallback, + setEventsLoading: setEventsLoadingCallback, + }); + refetchQuery(); + }, + [ + getGlobalQuery, + selectedEventIds, + setEventsDeletedCallback, + setEventsLoadingCallback, + showClearSelectionAction, + ] + ); + + // Callback for creating the SignalUtilityBar which receives totalCount from EventsViewer component + const utilityBarCallback = useCallback( + (refetchQuery: inputsModel.Refetch, totalCount: number) => { + return ( + 0} + clearSelection={clearSelectionCallback} + hasIndexWrite={hasIndexWrite} + isFilteredToOpen={filterGroup === FILTER_OPEN} + selectAll={selectAllCallback} + selectedEventIds={selectedEventIds} + showClearSelection={showClearSelectionAction} + totalCount={totalCount} + updateSignalsStatus={updateSignalsStatusCallback.bind(null, refetchQuery)} + /> + ); + }, + [ + canUserCRUD, + hasIndexWrite, + clearSelectionCallback, + filterGroup, + loadingEventIds.length, + selectAllCallback, + selectedEventIds, + showClearSelectionAction, + updateSignalsStatusCallback, + ] + ); + + // Send to Timeline / Update Signal Status Actions for each table row + const additionalActions = useMemo( + () => + getSignalsActions({ + apolloClient, + canUserCRUD, + hasIndexWrite, + createTimeline: createTimelineCallback, + setEventsLoading: setEventsLoadingCallback, + setEventsDeleted: setEventsDeletedCallback, + status: filterGroup === FILTER_OPEN ? FILTER_CLOSED : FILTER_OPEN, + updateTimelineIsLoading, + }), + [ + apolloClient, + canUserCRUD, + createTimelineCallback, + hasIndexWrite, + filterGroup, + setEventsLoadingCallback, + setEventsDeletedCallback, + updateTimelineIsLoading, + ] + ); + + const defaultIndices = useMemo(() => [signalsIndex], [signalsIndex]); + const defaultFiltersMemo = useMemo(() => { + if (isEmpty(defaultFilters)) { + return filterGroup === FILTER_OPEN ? signalsOpenFilters : signalsClosedFilters; + } else if (defaultFilters != null && !isEmpty(defaultFilters)) { + return [ + ...defaultFilters, + ...(filterGroup === FILTER_OPEN ? signalsOpenFilters : signalsClosedFilters), + ]; + } + }, [defaultFilters, filterGroup]); + + const timelineTypeContext = useMemo( + () => ({ + documentType: i18n.SIGNALS_DOCUMENT_TYPE, + footerText: i18n.TOTAL_COUNT_OF_SIGNALS, + loadingText: i18n.LOADING_SIGNALS, + queryFields: requiredFieldsForActions, + timelineActions: additionalActions, + title: i18n.SIGNALS_TABLE_TITLE, + selectAll: canUserCRUD ? selectAll : false, + }), + [additionalActions, canUserCRUD, selectAll] + ); + + const headerFilterGroup = useMemo( + () => , + [onFilterGroupChangedCallback] + ); + + if (loading || isEmpty(signalsIndex)) { + return ( + + + + + ); + } + + return ( + + ); +}; + +const makeMapStateToProps = () => { + const getTimeline = timelineSelectors.getTimelineByIdSelector(); + const getGlobalInputs = inputsSelectors.globalSelector(); + const mapStateToProps = (state: State) => { + const timeline: TimelineModel = + getTimeline(state, SIGNALS_PAGE_TIMELINE_ID) ?? timelineDefaults; + const { deletedEventIds, isSelectAllChecked, loadingEventIds, selectedEventIds } = timeline; + + const globalInputs: inputsModel.InputsRange = getGlobalInputs(state); + const { query, filters } = globalInputs; + return { + globalQuery: query, + globalFilters: filters, + deletedEventIds, + isSelectAllChecked, + loadingEventIds, + selectedEventIds, + }; + }; + return mapStateToProps; +}; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + clearSelected: ({ id }: { id: string }) => dispatch(timelineActions.clearSelected({ id })), + setEventsLoading: ({ + id, + eventIds, + isLoading, + }: { + id: string; + eventIds: string[]; + isLoading: boolean; + }) => dispatch(timelineActions.setEventsLoading({ id, eventIds, isLoading })), + clearEventsLoading: ({ id }: { id: string }) => + dispatch(timelineActions.clearEventsLoading({ id })), + setEventsDeleted: ({ + id, + eventIds, + isDeleted, + }: { + id: string; + eventIds: string[]; + isDeleted: boolean; + }) => dispatch(timelineActions.setEventsDeleted({ id, eventIds, isDeleted })), + clearEventsDeleted: ({ id }: { id: string }) => + dispatch(timelineActions.clearEventsDeleted({ id })), + updateTimelineIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => + dispatch(timelineActions.updateIsLoading({ id, isLoading })), + updateTimeline: dispatchUpdateTimeline(dispatch), +}); + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const SignalsTable = connector(React.memo(SignalsTableComponent)); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/signals_filter_group/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/signals/signals_filter_group/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/components/signals/signals_filter_group/index.test.tsx rename to x-pack/plugins/siem/public/alerts/components/signals/signals_filter_group/index.test.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/signals_filter_group/index.tsx b/x-pack/plugins/siem/public/alerts/components/signals/signals_filter_group/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/components/signals/signals_filter_group/index.tsx rename to x-pack/plugins/siem/public/alerts/components/signals/signals_filter_group/index.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/signals/signals_utility_bar/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/signals/signals_utility_bar/index.test.tsx new file mode 100644 index 00000000000000..3b43185c2c16b4 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/signals/signals_utility_bar/index.test.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { SignalsUtilityBar } from './index'; + +jest.mock('../../../../common/lib/kibana'); + +describe('SignalsUtilityBar', () => { + it('renders correctly', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find('[dataTestSubj="openCloseSignal"]')).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/components/signals/signals_utility_bar/index.tsx b/x-pack/plugins/siem/public/alerts/components/signals/signals_utility_bar/index.tsx new file mode 100644 index 00000000000000..e23f4ebdd3d30a --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/signals/signals_utility_bar/index.tsx @@ -0,0 +1,125 @@ +/* + * 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 { isEmpty } from 'lodash/fp'; +import React, { useCallback } from 'react'; +import numeral from '@elastic/numeral'; + +import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; +import { + UtilityBar, + UtilityBarAction, + UtilityBarGroup, + UtilityBarSection, + UtilityBarText, +} from '../../../../common/components/utility_bar'; +import * as i18n from './translations'; +import { useUiSetting$ } from '../../../../common/lib/kibana'; +import { TimelineNonEcsData } from '../../../../graphql/types'; +import { UpdateSignalsStatus } from '../types'; +import { FILTER_CLOSED, FILTER_OPEN } from '../signals_filter_group'; + +interface SignalsUtilityBarProps { + canUserCRUD: boolean; + hasIndexWrite: boolean; + areEventsLoading: boolean; + clearSelection: () => void; + isFilteredToOpen: boolean; + selectAll: () => void; + selectedEventIds: Readonly>; + showClearSelection: boolean; + totalCount: number; + updateSignalsStatus: UpdateSignalsStatus; +} + +const SignalsUtilityBarComponent: React.FC = ({ + canUserCRUD, + hasIndexWrite, + areEventsLoading, + clearSelection, + totalCount, + selectedEventIds, + isFilteredToOpen, + selectAll, + showClearSelection, + updateSignalsStatus, +}) => { + const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); + + const handleUpdateStatus = useCallback(async () => { + await updateSignalsStatus({ + signalIds: Object.keys(selectedEventIds), + status: isFilteredToOpen ? FILTER_CLOSED : FILTER_OPEN, + }); + }, [selectedEventIds, updateSignalsStatus, isFilteredToOpen]); + + const formattedTotalCount = numeral(totalCount).format(defaultNumberFormat); + const formattedSelectedEventsCount = numeral(Object.keys(selectedEventIds).length).format( + defaultNumberFormat + ); + + return ( + <> + + + + + {i18n.SHOWING_SIGNALS(formattedTotalCount, totalCount)} + + + + + {canUserCRUD && hasIndexWrite && ( + <> + + {i18n.SELECTED_SIGNALS( + showClearSelection ? formattedTotalCount : formattedSelectedEventsCount, + showClearSelection ? totalCount : Object.keys(selectedEventIds).length + )} + + + + {isFilteredToOpen + ? i18n.BATCH_ACTION_CLOSE_SELECTED + : i18n.BATCH_ACTION_OPEN_SELECTED} + + + { + if (!showClearSelection) { + selectAll(); + } else { + clearSelection(); + } + }} + > + {showClearSelection + ? i18n.CLEAR_SELECTION + : i18n.SELECT_ALL_SIGNALS(formattedTotalCount, totalCount)} + + + )} + + + + + ); +}; + +export const SignalsUtilityBar = React.memo( + SignalsUtilityBarComponent, + (prevProps, nextProps) => + prevProps.areEventsLoading === nextProps.areEventsLoading && + prevProps.selectedEventIds === nextProps.selectedEventIds && + prevProps.totalCount === nextProps.totalCount && + prevProps.showClearSelection === nextProps.showClearSelection +); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/translations.ts b/x-pack/plugins/siem/public/alerts/components/signals/signals_utility_bar/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/translations.ts rename to x-pack/plugins/siem/public/alerts/components/signals/signals_utility_bar/translations.ts diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/translations.ts b/x-pack/plugins/siem/public/alerts/components/signals/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/components/signals/translations.ts rename to x-pack/plugins/siem/public/alerts/components/signals/translations.ts diff --git a/x-pack/plugins/siem/public/alerts/components/signals/types.ts b/x-pack/plugins/siem/public/alerts/components/signals/types.ts new file mode 100644 index 00000000000000..b3c770415ed574 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/signals/types.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 ApolloClient from 'apollo-client'; + +import { Ecs } from '../../../graphql/types'; +import { TimelineModel } from '../../../timelines/store/timeline/model'; +import { inputsModel } from '../../../common/store'; + +export interface SetEventsLoadingProps { + eventIds: string[]; + isLoading: boolean; +} + +export interface SetEventsDeletedProps { + eventIds: string[]; + isDeleted: boolean; +} + +export interface UpdateSignalsStatusProps { + signalIds: string[]; + status: 'open' | 'closed'; +} + +export type UpdateSignalsStatusCallback = ( + refetchQuery: inputsModel.Refetch, + { signalIds, status }: UpdateSignalsStatusProps +) => void; +export type UpdateSignalsStatus = ({ signalIds, status }: UpdateSignalsStatusProps) => void; + +export interface UpdateSignalStatusActionProps { + query?: string; + signalIds: string[]; + status: 'open' | 'closed'; + setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void; + setEventsDeleted: ({ eventIds, isDeleted }: SetEventsDeletedProps) => void; +} + +export type SendSignalsToTimeline = () => void; + +export interface SendSignalToTimelineActionProps { + apolloClient?: ApolloClient<{}>; + createTimeline: CreateTimeline; + ecsData: Ecs; + updateTimelineIsLoading: UpdateTimelineLoading; +} + +export type UpdateTimelineLoading = ({ id, isLoading }: { id: string; isLoading: boolean }) => void; + +export interface CreateTimelineProps { + from: number; + timeline: TimelineModel; + to: number; + ruleNote?: string; +} + +export type CreateTimeline = ({ from, timeline, to }: CreateTimelineProps) => void; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/config.ts b/x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/config.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/config.ts rename to x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/config.ts diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/helpers.test.tsx b/x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/helpers.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/helpers.test.tsx rename to x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/helpers.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/helpers.tsx b/x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/helpers.tsx new file mode 100644 index 00000000000000..0c9fa39e53d00b --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/helpers.tsx @@ -0,0 +1,101 @@ +/* + * 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 { showAllOthersBucket } from '../../../../common/constants'; +import { HistogramData, SignalsAggregation, SignalsBucket, SignalsGroupBucket } from './types'; +import { SignalSearchResponse } from '../../containers/detection_engine/signals/types'; +import * as i18n from './translations'; + +export const formatSignalsData = ( + signalsData: SignalSearchResponse<{}, SignalsAggregation> | null +) => { + const groupBuckets: SignalsGroupBucket[] = + signalsData?.aggregations?.signalsByGrouping?.buckets ?? []; + return groupBuckets.reduce((acc, { key: group, signals }) => { + const signalsBucket: SignalsBucket[] = signals.buckets ?? []; + + return [ + ...acc, + ...signalsBucket.map(({ key, doc_count }: SignalsBucket) => ({ + x: key, + y: doc_count, + g: group, + })), + ]; + }, []); +}; + +export const getSignalsHistogramQuery = ( + stackByField: string, + from: number, + to: number, + additionalFilters: Array<{ + bool: { filter: unknown[]; should: unknown[]; must_not: unknown[]; must: unknown[] }; + }> +) => { + const missing = showAllOthersBucket.includes(stackByField) + ? { + missing: stackByField.endsWith('.ip') ? '0.0.0.0' : i18n.ALL_OTHERS, + } + : {}; + + return { + aggs: { + signalsByGrouping: { + terms: { + field: stackByField, + ...missing, + order: { + _count: 'desc', + }, + size: 10, + }, + aggs: { + signals: { + date_histogram: { + field: '@timestamp', + fixed_interval: `${Math.floor((to - from) / 32)}ms`, + min_doc_count: 0, + extended_bounds: { + min: from, + max: to, + }, + }, + }, + }, + }, + }, + query: { + bool: { + filter: [ + ...additionalFilters, + { + range: { + '@timestamp': { + gte: from, + lte: to, + }, + }, + }, + ], + }, + }, + }; +}; + +/** + * Returns `true` when the signals histogram initial loading spinner should be shown + * + * @param isInitialLoading The loading spinner will only be displayed if this value is `true`, because after initial load, a different, non-spinner loading indicator is displayed + * @param isLoadingSignals When `true`, IO is being performed to request signals (for rendering in the histogram) + */ +export const showInitialLoadingSpinner = ({ + isInitialLoading, + isLoadingSignals, +}: { + isInitialLoading: boolean; + isLoadingSignals: boolean; +}): boolean => isInitialLoading && isLoadingSignals; diff --git a/x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/index.test.tsx new file mode 100644 index 00000000000000..6578af19094df7 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/index.test.tsx @@ -0,0 +1,29 @@ +/* + * 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 { shallow } from 'enzyme'; + +import { SignalsHistogramPanel } from './index'; + +jest.mock('../../../common/lib/kibana'); +jest.mock('../../../common/components/navigation/use_get_url_search'); + +describe('SignalsHistogramPanel', () => { + it('renders correctly', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find('[id="detections-histogram"]')).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/index.tsx b/x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/index.tsx new file mode 100644 index 00000000000000..0a1ce5a39af895 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/index.tsx @@ -0,0 +1,284 @@ +/* + * 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 { Position } from '@elastic/charts'; +import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSelect, EuiPanel } from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import React, { memo, useCallback, useMemo, useState, useEffect } from 'react'; +import styled from 'styled-components'; +import { isEmpty } from 'lodash/fp'; +import uuid from 'uuid'; + +import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; +import { UpdateDateRange } from '../../../common/components/charts/common'; +import { LegendItem } from '../../../common/components/charts/draggable_legend_item'; +import { escapeDataProviderId } from '../../../common/components/drag_and_drop/helpers'; +import { HeaderSection } from '../../../common/components/header_section'; +import { Filter, esQuery, Query } from '../../../../../../../src/plugins/data/public'; +import { useQuerySignals } from '../../containers/detection_engine/signals/use_query'; +import { getDetectionEngineUrl } from '../../../common/components/link_to'; +import { defaultLegendColors } from '../../../common/components/matrix_histogram/utils'; +import { InspectButtonContainer } from '../../../common/components/inspect'; +import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search'; +import { MatrixLoader } from '../../../common/components/matrix_histogram/matrix_loader'; +import { MatrixHistogramOption } from '../../../common/components/matrix_histogram/types'; +import { useKibana, useUiSetting$ } from '../../../common/lib/kibana'; +import { navTabs } from '../../../app/home/home_navigations'; +import { signalsHistogramOptions } from './config'; +import { formatSignalsData, getSignalsHistogramQuery, showInitialLoadingSpinner } from './helpers'; +import { SignalsHistogram } from './signals_histogram'; +import * as i18n from './translations'; +import { RegisterQuery, SignalsHistogramOption, SignalsAggregation, SignalsTotal } from './types'; + +const DEFAULT_PANEL_HEIGHT = 300; + +const StyledEuiPanel = styled(EuiPanel)<{ height?: number }>` + display: flex; + flex-direction: column; + ${({ height }) => (height != null ? `height: ${height}px;` : '')} + position: relative; +`; + +const defaultTotalSignalsObj: SignalsTotal = { + value: 0, + relation: 'eq', +}; + +export const DETECTIONS_HISTOGRAM_ID = 'detections-histogram'; + +const ViewSignalsFlexItem = styled(EuiFlexItem)` + margin-left: 24px; +`; + +interface SignalsHistogramPanelProps { + chartHeight?: number; + defaultStackByOption?: SignalsHistogramOption; + deleteQuery?: ({ id }: { id: string }) => void; + filters?: Filter[]; + from: number; + headerChildren?: React.ReactNode; + /** Override all defaults, and only display this field */ + onlyField?: string; + query?: Query; + legendPosition?: Position; + panelHeight?: number; + signalIndexName: string | null; + setQuery: (params: RegisterQuery) => void; + showLinkToSignals?: boolean; + showTotalSignalsCount?: boolean; + stackByOptions?: SignalsHistogramOption[]; + title?: string; + to: number; + updateDateRange: UpdateDateRange; +} + +const getHistogramOption = (fieldName: string): MatrixHistogramOption => ({ + text: fieldName, + value: fieldName, +}); + +const NO_LEGEND_DATA: LegendItem[] = []; + +export const SignalsHistogramPanel = memo( + ({ + chartHeight, + defaultStackByOption = signalsHistogramOptions[0], + deleteQuery, + filters, + headerChildren, + onlyField, + query, + from, + legendPosition = 'right', + panelHeight = DEFAULT_PANEL_HEIGHT, + setQuery, + signalIndexName, + showLinkToSignals = false, + showTotalSignalsCount = false, + stackByOptions, + to, + title = i18n.HISTOGRAM_HEADER, + updateDateRange, + }) => { + // create a unique, but stable (across re-renders) query id + const uniqueQueryId = useMemo(() => `${DETECTIONS_HISTOGRAM_ID}-${uuid.v4()}`, []); + const [isInitialLoading, setIsInitialLoading] = useState(true); + const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); + const [totalSignalsObj, setTotalSignalsObj] = useState(defaultTotalSignalsObj); + const [selectedStackByOption, setSelectedStackByOption] = useState( + onlyField == null ? defaultStackByOption : getHistogramOption(onlyField) + ); + const { + loading: isLoadingSignals, + data: signalsData, + setQuery: setSignalsQuery, + response, + request, + refetch, + } = useQuerySignals<{}, SignalsAggregation>( + getSignalsHistogramQuery(selectedStackByOption.value, from, to, []), + signalIndexName + ); + const kibana = useKibana(); + const urlSearch = useGetUrlSearch(navTabs.detections); + + const totalSignals = useMemo( + () => + i18n.SHOWING_SIGNALS( + numeral(totalSignalsObj.value).format(defaultNumberFormat), + totalSignalsObj.value, + totalSignalsObj.relation === 'gte' ? '>' : totalSignalsObj.relation === 'lte' ? '<' : '' + ), + [totalSignalsObj] + ); + + const setSelectedOptionCallback = useCallback((event: React.ChangeEvent) => { + setSelectedStackByOption( + stackByOptions?.find(co => co.value === event.target.value) ?? defaultStackByOption + ); + }, []); + + const formattedSignalsData = useMemo(() => formatSignalsData(signalsData), [signalsData]); + + const legendItems: LegendItem[] = useMemo( + () => + signalsData?.aggregations?.signalsByGrouping?.buckets != null + ? signalsData.aggregations.signalsByGrouping.buckets.map((bucket, i) => ({ + color: i < defaultLegendColors.length ? defaultLegendColors[i] : undefined, + dataProviderId: escapeDataProviderId( + `draggable-legend-item-${uuid.v4()}-${selectedStackByOption.value}-${bucket.key}` + ), + field: selectedStackByOption.value, + value: bucket.key, + })) + : NO_LEGEND_DATA, + [signalsData, selectedStackByOption.value] + ); + + useEffect(() => { + let canceled = false; + + if (!canceled && !showInitialLoadingSpinner({ isInitialLoading, isLoadingSignals })) { + setIsInitialLoading(false); + } + + return () => { + canceled = true; // prevent long running data fetches from updating state after unmounting + }; + }, [isInitialLoading, isLoadingSignals, setIsInitialLoading]); + + useEffect(() => { + return () => { + if (deleteQuery) { + deleteQuery({ id: uniqueQueryId }); + } + }; + }, []); + + useEffect(() => { + if (refetch != null && setQuery != null) { + setQuery({ + id: uniqueQueryId, + inspect: { + dsl: [request], + response: [response], + }, + loading: isLoadingSignals, + refetch, + }); + } + }, [setQuery, isLoadingSignals, signalsData, response, request, refetch]); + + useEffect(() => { + setTotalSignalsObj( + signalsData?.hits.total ?? { + value: 0, + relation: 'eq', + } + ); + }, [signalsData]); + + useEffect(() => { + const converted = esQuery.buildEsQuery( + undefined, + query != null ? [query] : [], + filters?.filter(f => f.meta.disabled === false) ?? [], + { + ...esQuery.getEsQueryConfig(kibana.services.uiSettings), + dateFormatTZ: undefined, + } + ); + + setSignalsQuery( + getSignalsHistogramQuery( + selectedStackByOption.value, + from, + to, + !isEmpty(converted) ? [converted] : [] + ) + ); + }, [selectedStackByOption.value, from, to, query, filters]); + + const linkButton = useMemo(() => { + if (showLinkToSignals) { + return ( + + {i18n.VIEW_SIGNALS} + + ); + } + }, [showLinkToSignals, urlSearch]); + + const titleText = useMemo(() => (onlyField == null ? title : i18n.TOP(onlyField)), [ + onlyField, + title, + ]); + + return ( + + + + + + {stackByOptions && ( + + )} + {headerChildren != null && headerChildren} + + {linkButton} + + + + {isInitialLoading ? ( + + ) : ( + + )} + + + ); + } +); + +SignalsHistogramPanel.displayName = 'SignalsHistogramPanel'; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.test.tsx b/x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/signals_histogram.test.tsx similarity index 94% rename from x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.test.tsx rename to x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/signals_histogram.test.tsx index 6a116efb8f2f8b..f921c00cdafb7b 100644 --- a/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.test.tsx +++ b/x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/signals_histogram.test.tsx @@ -9,7 +9,7 @@ import { shallow } from 'enzyme'; import { SignalsHistogram } from './signals_histogram'; -jest.mock('../../../../lib/kibana'); +jest.mock('../../../common/lib/kibana'); describe('SignalsHistogram', () => { it('renders correctly', () => { diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.tsx b/x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/signals_histogram.tsx similarity index 89% rename from x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.tsx rename to x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/signals_histogram.tsx index a031f2542b8779..3c6e7b84fd2b41 100644 --- a/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/signals_histogram.tsx +++ b/x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/signals_histogram.tsx @@ -15,10 +15,10 @@ import { import { EuiFlexGroup, EuiFlexItem, EuiProgress } from '@elastic/eui'; import React, { useMemo } from 'react'; -import { useTheme, UpdateDateRange } from '../../../../components/charts/common'; -import { histogramDateTimeFormatter } from '../../../../components/utils'; -import { DraggableLegend } from '../../../../components/charts/draggable_legend'; -import { LegendItem } from '../../../../components/charts/draggable_legend_item'; +import { useTheme, UpdateDateRange } from '../../../common/components/charts/common'; +import { histogramDateTimeFormatter } from '../../../common/components/utils'; +import { DraggableLegend } from '../../../common/components/charts/draggable_legend'; +import { LegendItem } from '../../../common/components/charts/draggable_legend_item'; import { HistogramData } from './types'; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts b/x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/translations.ts rename to x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/translations.ts diff --git a/x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/types.ts b/x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/types.ts new file mode 100644 index 00000000000000..41d58a4a7391d9 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/signals_histogram_panel/types.ts @@ -0,0 +1,49 @@ +/* + * 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 { inputsModel } from '../../../common/store'; + +export interface SignalsHistogramOption { + text: string; + value: string; +} + +export interface HistogramData { + x: number; + y: number; + g: string; +} + +export interface SignalsAggregation { + signalsByGrouping: { + buckets: SignalsGroupBucket[]; + }; +} + +export interface SignalsBucket { + key_as_string: string; + key: number; + doc_count: number; +} + +export interface SignalsGroupBucket { + key: string; + signals: { + buckets: SignalsBucket[]; + }; +} + +export interface SignalsTotal { + value: number; + relation: string; +} + +export interface RegisterQuery { + id: string; + inspect: inputsModel.InspectQuery | null; + loading: boolean; + refetch: inputsModel.Refetch; +} diff --git a/x-pack/plugins/siem/public/alerts/components/signals_info/index.tsx b/x-pack/plugins/siem/public/alerts/components/signals_info/index.tsx new file mode 100644 index 00000000000000..b1d7f2cfe7eb52 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/signals_info/index.tsx @@ -0,0 +1,49 @@ +/* + * 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 { EuiLoadingSpinner } from '@elastic/eui'; +import { FormattedRelative } from '@kbn/i18n/react'; +import React, { useState, useEffect } from 'react'; + +import { useQuerySignals } from '../../containers/detection_engine/signals/use_query'; +import { buildLastSignalsQuery } from './query.dsl'; +import { Aggs } from './types'; + +interface SignalInfo { + ruleId?: string | null; +} + +type Return = [React.ReactNode, React.ReactNode]; + +export const useSignalInfo = ({ ruleId = null }: SignalInfo): Return => { + const [lastSignals, setLastSignals] = useState( + + ); + const [totalSignals, setTotalSignals] = useState( + + ); + + const { loading, data: signals } = useQuerySignals(buildLastSignalsQuery(ruleId)); + + useEffect(() => { + if (signals != null) { + const mySignals = signals; + setLastSignals( + mySignals.aggregations?.lastSeen.value != null ? ( + + ) : null + ); + setTotalSignals(<>{mySignals.hits.total.value}); + } else { + setLastSignals(null); + setTotalSignals(null); + } + }, [loading, signals]); + + return [lastSignals, totalSignals]; +}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals_info/query.dsl.ts b/x-pack/plugins/siem/public/alerts/components/signals_info/query.dsl.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/components/signals_info/query.dsl.ts rename to x-pack/plugins/siem/public/alerts/components/signals_info/query.dsl.ts diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals_info/types.ts b/x-pack/plugins/siem/public/alerts/components/signals_info/types.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/components/signals_info/types.ts rename to x-pack/plugins/siem/public/alerts/components/signals_info/types.ts diff --git a/x-pack/plugins/siem/public/alerts/components/user_info/index.test.tsx b/x-pack/plugins/siem/public/alerts/components/user_info/index.test.tsx new file mode 100644 index 00000000000000..81b2c4347e17c7 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/user_info/index.test.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { useUserInfo } from './index'; + +import { usePrivilegeUser } from '../../containers/detection_engine/signals/use_privilege_user'; +import { useSignalIndex } from '../../containers/detection_engine/signals/use_signal_index'; +import { useKibana } from '../../../common/lib/kibana'; +jest.mock('../../containers/detection_engine/signals/use_privilege_user'); +jest.mock('../../containers/detection_engine/signals/use_signal_index'); +jest.mock('../../../common/lib/kibana'); + +describe('useUserInfo', () => { + beforeAll(() => { + (usePrivilegeUser as jest.Mock).mockReturnValue({}); + (useSignalIndex as jest.Mock).mockReturnValue({}); + (useKibana as jest.Mock).mockReturnValue({ + services: { + application: { + capabilities: { + siem: { + crud: true, + }, + }, + }, + }, + }); + }); + it('returns default state', () => { + const { result } = renderHook(() => useUserInfo()); + + expect(result).toEqual({ + current: { + canUserCRUD: null, + hasEncryptionKey: null, + hasIndexManage: null, + hasIndexWrite: null, + isAuthenticated: null, + isSignalIndexExists: null, + loading: true, + signalIndexName: null, + }, + error: undefined, + }); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/components/user_info/index.tsx b/x-pack/plugins/siem/public/alerts/components/user_info/index.tsx new file mode 100644 index 00000000000000..faf90162925595 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/components/user_info/index.tsx @@ -0,0 +1,243 @@ +/* + * 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 { noop } from 'lodash/fp'; +import React, { useEffect, useReducer, Dispatch, createContext, useContext } from 'react'; + +import { usePrivilegeUser } from '../../containers/detection_engine/signals/use_privilege_user'; +import { useSignalIndex } from '../../containers/detection_engine/signals/use_signal_index'; +import { useKibana } from '../../../common/lib/kibana'; + +export interface State { + canUserCRUD: boolean | null; + hasIndexManage: boolean | null; + hasIndexWrite: boolean | null; + isSignalIndexExists: boolean | null; + isAuthenticated: boolean | null; + hasEncryptionKey: boolean | null; + loading: boolean; + signalIndexName: string | null; +} + +const initialState: State = { + canUserCRUD: null, + hasIndexManage: null, + hasIndexWrite: null, + isSignalIndexExists: null, + isAuthenticated: null, + hasEncryptionKey: null, + loading: true, + signalIndexName: null, +}; + +export type Action = + | { type: 'updateLoading'; loading: boolean } + | { + type: 'updateHasIndexManage'; + hasIndexManage: boolean | null; + } + | { + type: 'updateHasIndexWrite'; + hasIndexWrite: boolean | null; + } + | { + type: 'updateIsSignalIndexExists'; + isSignalIndexExists: boolean | null; + } + | { + type: 'updateIsAuthenticated'; + isAuthenticated: boolean | null; + } + | { + type: 'updateHasEncryptionKey'; + hasEncryptionKey: boolean | null; + } + | { + type: 'updateCanUserCRUD'; + canUserCRUD: boolean | null; + } + | { + type: 'updateSignalIndexName'; + signalIndexName: string | null; + }; + +export const userInfoReducer = (state: State, action: Action): State => { + switch (action.type) { + case 'updateLoading': { + return { + ...state, + loading: action.loading, + }; + } + case 'updateHasIndexManage': { + return { + ...state, + hasIndexManage: action.hasIndexManage, + }; + } + case 'updateHasIndexWrite': { + return { + ...state, + hasIndexWrite: action.hasIndexWrite, + }; + } + case 'updateIsSignalIndexExists': { + return { + ...state, + isSignalIndexExists: action.isSignalIndexExists, + }; + } + case 'updateIsAuthenticated': { + return { + ...state, + isAuthenticated: action.isAuthenticated, + }; + } + case 'updateHasEncryptionKey': { + return { + ...state, + hasEncryptionKey: action.hasEncryptionKey, + }; + } + case 'updateCanUserCRUD': { + return { + ...state, + canUserCRUD: action.canUserCRUD, + }; + } + case 'updateSignalIndexName': { + return { + ...state, + signalIndexName: action.signalIndexName, + }; + } + default: + return state; + } +}; + +const StateUserInfoContext = createContext<[State, Dispatch]>([initialState, () => noop]); + +const useUserData = () => useContext(StateUserInfoContext); + +interface ManageUserInfoProps { + children: React.ReactNode; +} + +export const ManageUserInfo = ({ children }: ManageUserInfoProps) => ( + + {children} + +); + +export const useUserInfo = (): State => { + const [ + { + canUserCRUD, + hasIndexManage, + hasIndexWrite, + isSignalIndexExists, + isAuthenticated, + hasEncryptionKey, + loading, + signalIndexName, + }, + dispatch, + ] = useUserData(); + const { + loading: privilegeLoading, + isAuthenticated: isApiAuthenticated, + hasEncryptionKey: isApiEncryptionKey, + hasIndexManage: hasApiIndexManage, + hasIndexWrite: hasApiIndexWrite, + } = usePrivilegeUser(); + const { + loading: indexNameLoading, + signalIndexExists: isApiSignalIndexExists, + signalIndexName: apiSignalIndexName, + createDeSignalIndex: createSignalIndex, + } = useSignalIndex(); + + const uiCapabilities = useKibana().services.application.capabilities; + const capabilitiesCanUserCRUD: boolean = + typeof uiCapabilities.siem.crud === 'boolean' ? uiCapabilities.siem.crud : false; + + useEffect(() => { + if (loading !== privilegeLoading || indexNameLoading) { + dispatch({ type: 'updateLoading', loading: privilegeLoading || indexNameLoading }); + } + }, [loading, privilegeLoading, indexNameLoading]); + + useEffect(() => { + if (!loading && hasIndexManage !== hasApiIndexManage && hasApiIndexManage != null) { + dispatch({ type: 'updateHasIndexManage', hasIndexManage: hasApiIndexManage }); + } + }, [loading, hasIndexManage, hasApiIndexManage]); + + useEffect(() => { + if (!loading && hasIndexWrite !== hasApiIndexWrite && hasApiIndexWrite != null) { + dispatch({ type: 'updateHasIndexWrite', hasIndexWrite: hasApiIndexWrite }); + } + }, [loading, hasIndexWrite, hasApiIndexWrite]); + + useEffect(() => { + if ( + !loading && + isSignalIndexExists !== isApiSignalIndexExists && + isApiSignalIndexExists != null + ) { + dispatch({ type: 'updateIsSignalIndexExists', isSignalIndexExists: isApiSignalIndexExists }); + } + }, [loading, isSignalIndexExists, isApiSignalIndexExists]); + + useEffect(() => { + if (!loading && isAuthenticated !== isApiAuthenticated && isApiAuthenticated != null) { + dispatch({ type: 'updateIsAuthenticated', isAuthenticated: isApiAuthenticated }); + } + }, [loading, isAuthenticated, isApiAuthenticated]); + + useEffect(() => { + if (!loading && hasEncryptionKey !== isApiEncryptionKey && isApiEncryptionKey != null) { + dispatch({ type: 'updateHasEncryptionKey', hasEncryptionKey: isApiEncryptionKey }); + } + }, [loading, hasEncryptionKey, isApiEncryptionKey]); + + useEffect(() => { + if (!loading && canUserCRUD !== capabilitiesCanUserCRUD && capabilitiesCanUserCRUD != null) { + dispatch({ type: 'updateCanUserCRUD', canUserCRUD: capabilitiesCanUserCRUD }); + } + }, [loading, canUserCRUD, capabilitiesCanUserCRUD]); + + useEffect(() => { + if (!loading && signalIndexName !== apiSignalIndexName && apiSignalIndexName != null) { + dispatch({ type: 'updateSignalIndexName', signalIndexName: apiSignalIndexName }); + } + }, [loading, signalIndexName, apiSignalIndexName]); + + useEffect(() => { + if ( + isAuthenticated && + hasEncryptionKey && + hasIndexManage && + isSignalIndexExists != null && + !isSignalIndexExists && + createSignalIndex != null + ) { + createSignalIndex(); + } + }, [createSignalIndex, isAuthenticated, hasEncryptionKey, isSignalIndexExists, hasIndexManage]); + + return { + loading, + isSignalIndexExists, + isAuthenticated, + hasEncryptionKey, + canUserCRUD, + hasIndexManage, + hasIndexWrite, + signalIndexName, + }; +}; diff --git a/x-pack/plugins/siem/public/containers/detection_engine/rules/__mocks__/api.ts b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/__mocks__/api.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/detection_engine/rules/__mocks__/api.ts rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/__mocks__/api.ts diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/api.test.ts b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/api.test.ts new file mode 100644 index 00000000000000..abba7c02cf8757 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/api.test.ts @@ -0,0 +1,559 @@ +/* + * 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 { KibanaServices } from '../../../../common/lib/kibana'; +import { + addRule, + fetchRules, + fetchRuleById, + enableRules, + deleteRules, + duplicateRules, + createPrepackagedRules, + importRules, + exportRules, + getRuleStatusById, + fetchTags, + getPrePackagedRulesStatus, +} from './api'; +import { ruleMock, rulesMock } from './mock'; + +const abortCtrl = new AbortController(); +const mockKibanaServices = KibanaServices.get as jest.Mock; +jest.mock('../../../../common/lib/kibana'); + +const fetchMock = jest.fn(); +mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } }); + +describe('Detections Rules API', () => { + describe('addRule', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(ruleMock); + }); + + test('check parameter url, body', async () => { + await addRule({ rule: ruleMock, signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules', { + body: + '{"description":"some desc","enabled":true,"false_positives":[],"filters":[],"from":"now-360s","index":["apm-*-transaction*","auditbeat-*","endgame-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"bbd3106e-b4b5-4d7c-a1a2-47531d6a2baf","language":"kuery","risk_score":75,"name":"Test rule","query":"user.email: \'root@elastic.co\'","references":[],"severity":"high","tags":["APM"],"to":"now","type":"query","threat":[],"throttle":null}', + method: 'POST', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const ruleResp = await addRule({ rule: ruleMock, signal: abortCtrl.signal }); + expect(ruleResp).toEqual(ruleMock); + }); + }); + + describe('fetchRules', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(rulesMock); + }); + + test('check parameter url, query without any options', async () => { + await fetchRules({ signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { + method: 'GET', + query: { + page: 1, + per_page: 20, + sort_field: 'enabled', + sort_order: 'desc', + }, + signal: abortCtrl.signal, + }); + }); + + test('check parameter url, query with a filter', async () => { + await fetchRules({ + filterOptions: { + filter: 'hello world', + sortField: 'enabled', + sortOrder: 'desc', + showCustomRules: false, + showElasticRules: false, + tags: [], + }, + signal: abortCtrl.signal, + }); + + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { + method: 'GET', + query: { + filter: 'alert.attributes.name: hello world', + page: 1, + per_page: 20, + sort_field: 'enabled', + sort_order: 'desc', + }, + signal: abortCtrl.signal, + }); + }); + + test('check parameter url, query with showCustomRules', async () => { + await fetchRules({ + filterOptions: { + filter: '', + sortField: 'enabled', + sortOrder: 'desc', + showCustomRules: true, + showElasticRules: false, + tags: [], + }, + signal: abortCtrl.signal, + }); + + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { + method: 'GET', + query: { + filter: 'alert.attributes.tags: "__internal_immutable:false"', + page: 1, + per_page: 20, + sort_field: 'enabled', + sort_order: 'desc', + }, + signal: abortCtrl.signal, + }); + }); + + test('check parameter url, query with showElasticRules', async () => { + await fetchRules({ + filterOptions: { + filter: '', + sortField: 'enabled', + sortOrder: 'desc', + showCustomRules: false, + showElasticRules: true, + tags: [], + }, + signal: abortCtrl.signal, + }); + + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { + method: 'GET', + query: { + filter: 'alert.attributes.tags: "__internal_immutable:true"', + page: 1, + per_page: 20, + sort_field: 'enabled', + sort_order: 'desc', + }, + signal: abortCtrl.signal, + }); + }); + + test('check parameter url, query with tags', async () => { + await fetchRules({ + filterOptions: { + filter: '', + sortField: 'enabled', + sortOrder: 'desc', + showCustomRules: false, + showElasticRules: false, + tags: ['hello', 'world'], + }, + signal: abortCtrl.signal, + }); + + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { + method: 'GET', + query: { + filter: 'alert.attributes.tags: hello AND alert.attributes.tags: world', + page: 1, + per_page: 20, + sort_field: 'enabled', + sort_order: 'desc', + }, + signal: abortCtrl.signal, + }); + }); + + test('check parameter url, query with all options', async () => { + await fetchRules({ + filterOptions: { + filter: 'ruleName', + sortField: 'enabled', + sortOrder: 'desc', + showCustomRules: true, + showElasticRules: true, + tags: ['hello', 'world'], + }, + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { + method: 'GET', + query: { + filter: + 'alert.attributes.name: ruleName AND alert.attributes.tags: "__internal_immutable:false" AND alert.attributes.tags: "__internal_immutable:true" AND alert.attributes.tags: hello AND alert.attributes.tags: world', + page: 1, + per_page: 20, + sort_field: 'enabled', + sort_order: 'desc', + }, + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const rulesResp = await fetchRules({ signal: abortCtrl.signal }); + expect(rulesResp).toEqual(rulesMock); + }); + }); + + describe('fetchRuleById', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(ruleMock); + }); + + test('check parameter url, query', async () => { + await fetchRuleById({ id: 'mySuperRuleId', signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules', { + query: { + id: 'mySuperRuleId', + }, + method: 'GET', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const ruleResp = await fetchRuleById({ id: 'mySuperRuleId', signal: abortCtrl.signal }); + expect(ruleResp).toEqual(ruleMock); + }); + }); + + describe('enableRules', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(rulesMock); + }); + + test('check parameter url, body when enabling rules', async () => { + await enableRules({ ids: ['mySuperRuleId', 'mySuperRuleId_II'], enabled: true }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_bulk_update', { + body: '[{"id":"mySuperRuleId","enabled":true},{"id":"mySuperRuleId_II","enabled":true}]', + method: 'PATCH', + }); + }); + test('check parameter url, body when disabling rules', async () => { + await enableRules({ ids: ['mySuperRuleId', 'mySuperRuleId_II'], enabled: false }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_bulk_update', { + body: '[{"id":"mySuperRuleId","enabled":false},{"id":"mySuperRuleId_II","enabled":false}]', + method: 'PATCH', + }); + }); + test('happy path', async () => { + const ruleResp = await enableRules({ + ids: ['mySuperRuleId', 'mySuperRuleId_II'], + enabled: true, + }); + expect(ruleResp).toEqual(rulesMock); + }); + }); + + describe('deleteRules', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(rulesMock); + }); + + test('check parameter url, body when deleting rules', async () => { + await deleteRules({ ids: ['mySuperRuleId', 'mySuperRuleId_II'] }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_bulk_delete', { + body: '[{"id":"mySuperRuleId"},{"id":"mySuperRuleId_II"}]', + method: 'DELETE', + }); + }); + + test('happy path', async () => { + const ruleResp = await deleteRules({ + ids: ['mySuperRuleId', 'mySuperRuleId_II'], + }); + expect(ruleResp).toEqual(rulesMock); + }); + }); + + describe('duplicateRules', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(rulesMock); + }); + + test('check parameter url, body when duplicating rules', async () => { + await duplicateRules({ rules: rulesMock.data }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_bulk_create', { + body: + '[{"actions":[],"description":"Elastic Endpoint detected Credential Dumping. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.","enabled":false,"false_positives":[],"from":"now-660s","index":["endgame-*"],"interval":"10m","language":"kuery","output_index":".siem-signals-default","max_signals":100,"risk_score":73,"name":"Credential Dumping - Detected - Elastic Endpoint [Duplicate]","query":"event.kind:alert and event.module:endgame and event.action:cred_theft_event and endgame.metadata.type:detection","filters":[],"references":[],"severity":"high","tags":["Elastic","Endpoint"],"to":"now","type":"query","threat":[],"throttle":null,"version":1},{"actions":[],"description":"Elastic Endpoint detected an Adversary Behavior. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.","enabled":false,"false_positives":[],"from":"now-660s","index":["endgame-*"],"interval":"10m","language":"kuery","output_index":".siem-signals-default","max_signals":100,"risk_score":47,"name":"Adversary Behavior - Detected - Elastic Endpoint [Duplicate]","query":"event.kind:alert and event.module:endgame and event.action:rules_engine_event","filters":[],"references":[],"severity":"medium","tags":["Elastic","Endpoint"],"to":"now","type":"query","threat":[],"throttle":null,"version":1}]', + method: 'POST', + }); + }); + + test('happy path', async () => { + const ruleResp = await duplicateRules({ rules: rulesMock.data }); + expect(ruleResp).toEqual(rulesMock); + }); + }); + + describe('createPrepackagedRules', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue('unknown'); + }); + + test('check parameter url when creating pre-packaged rules', async () => { + await createPrepackagedRules({ signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/prepackaged', { + signal: abortCtrl.signal, + method: 'PUT', + }); + }); + test('happy path', async () => { + const resp = await createPrepackagedRules({ signal: abortCtrl.signal }); + expect(resp).toEqual(true); + }); + }); + + describe('importRules', () => { + const fileToImport: File = { + lastModified: 33, + name: 'fileToImport', + size: 89, + type: 'json', + arrayBuffer: jest.fn(), + slice: jest.fn(), + stream: jest.fn(), + text: jest.fn(), + } as File; + const formData = new FormData(); + formData.append('file', fileToImport); + + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue('unknown'); + }); + + test('check parameter url, body and query when importing rules', async () => { + await importRules({ fileToImport, signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_import', { + signal: abortCtrl.signal, + method: 'POST', + body: formData, + headers: { + 'Content-Type': undefined, + }, + query: { + overwrite: false, + }, + }); + }); + + test('check parameter url, body and query when importing rules with overwrite', async () => { + await importRules({ fileToImport, overwrite: true, signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_import', { + signal: abortCtrl.signal, + method: 'POST', + body: formData, + headers: { + 'Content-Type': undefined, + }, + query: { + overwrite: true, + }, + }); + }); + + test('happy path', async () => { + fetchMock.mockResolvedValue({ + success: true, + success_count: 33, + errors: [], + }); + const resp = await importRules({ fileToImport, signal: abortCtrl.signal }); + expect(resp).toEqual({ + success: true, + success_count: 33, + errors: [], + }); + }); + }); + + describe('exportRules', () => { + const blob: Blob = { + size: 89, + type: 'json', + arrayBuffer: jest.fn(), + slice: jest.fn(), + stream: jest.fn(), + text: jest.fn(), + } as Blob; + + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(blob); + }); + + test('check parameter url, body and query when exporting rules', async () => { + await exportRules({ + ids: ['mySuperRuleId', 'mySuperRuleId_II'], + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_export', { + signal: abortCtrl.signal, + method: 'POST', + body: '{"objects":[{"rule_id":"mySuperRuleId"},{"rule_id":"mySuperRuleId_II"}]}', + query: { + exclude_export_details: false, + file_name: 'rules_export.ndjson', + }, + }); + }); + + test('check parameter url, body and query when exporting rules with excludeExportDetails', async () => { + await exportRules({ + excludeExportDetails: true, + ids: ['mySuperRuleId', 'mySuperRuleId_II'], + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_export', { + signal: abortCtrl.signal, + method: 'POST', + body: '{"objects":[{"rule_id":"mySuperRuleId"},{"rule_id":"mySuperRuleId_II"}]}', + query: { + exclude_export_details: true, + file_name: 'rules_export.ndjson', + }, + }); + }); + + test('check parameter url, body and query when exporting rules with fileName', async () => { + await exportRules({ + filename: 'myFileName.ndjson', + ids: ['mySuperRuleId', 'mySuperRuleId_II'], + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_export', { + signal: abortCtrl.signal, + method: 'POST', + body: '{"objects":[{"rule_id":"mySuperRuleId"},{"rule_id":"mySuperRuleId_II"}]}', + query: { + exclude_export_details: false, + file_name: 'myFileName.ndjson', + }, + }); + }); + + test('check parameter url, body and query when exporting rules with all options', async () => { + await exportRules({ + excludeExportDetails: true, + filename: 'myFileName.ndjson', + ids: ['mySuperRuleId', 'mySuperRuleId_II'], + signal: abortCtrl.signal, + }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_export', { + signal: abortCtrl.signal, + method: 'POST', + body: '{"objects":[{"rule_id":"mySuperRuleId"},{"rule_id":"mySuperRuleId_II"}]}', + query: { + exclude_export_details: true, + file_name: 'myFileName.ndjson', + }, + }); + }); + + test('happy path', async () => { + const resp = await exportRules({ + ids: ['mySuperRuleId', 'mySuperRuleId_II'], + signal: abortCtrl.signal, + }); + expect(resp).toEqual(blob); + }); + }); + + describe('getRuleStatusById', () => { + const statusMock = { + myRule: { + current_status: { + alert_id: 'alertId', + status_date: 'mm/dd/yyyyTHH:MM:sssz', + status: 'succeeded', + last_failure_at: null, + last_success_at: 'mm/dd/yyyyTHH:MM:sssz', + last_failure_message: null, + last_success_message: 'it is a success', + }, + failures: [], + }, + }; + + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(statusMock); + }); + + test('check parameter url, query', async () => { + await getRuleStatusById({ id: 'mySuperRuleId', signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find_statuses', { + body: '{"ids":["mySuperRuleId"]}', + method: 'POST', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const ruleResp = await getRuleStatusById({ id: 'mySuperRuleId', signal: abortCtrl.signal }); + expect(ruleResp).toEqual(statusMock); + }); + }); + + describe('fetchTags', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(['some', 'tags']); + }); + + test('check parameter url when fetching tags', async () => { + await fetchTags({ signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/tags', { + signal: abortCtrl.signal, + method: 'GET', + }); + }); + + test('happy path', async () => { + const resp = await fetchTags({ signal: abortCtrl.signal }); + expect(resp).toEqual(['some', 'tags']); + }); + }); + + describe('getPrePackagedRulesStatus', () => { + const prePackagedRulesStatus = { + rules_custom_installed: 33, + rules_installed: 12, + rules_not_installed: 0, + rules_not_updated: 2, + }; + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(prePackagedRulesStatus); + }); + test('check parameter url when fetching tags', async () => { + await getPrePackagedRulesStatus({ signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/prepackaged/_status', { + signal: abortCtrl.signal, + method: 'GET', + }); + }); + test('happy path', async () => { + const resp = await getPrePackagedRulesStatus({ signal: abortCtrl.signal }); + expect(resp).toEqual(prePackagedRulesStatus); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/api.ts b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/api.ts new file mode 100644 index 00000000000000..9ae29a740dd870 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/api.ts @@ -0,0 +1,331 @@ +/* + * 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 { + DETECTION_ENGINE_RULES_URL, + DETECTION_ENGINE_PREPACKAGED_URL, + DETECTION_ENGINE_RULES_STATUS_URL, + DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL, + DETECTION_ENGINE_TAGS_URL, +} from '../../../../../common/constants'; +import { + AddRulesProps, + DeleteRulesProps, + DuplicateRulesProps, + EnableRulesProps, + FetchRulesProps, + FetchRulesResponse, + NewRule, + Rule, + FetchRuleProps, + BasicFetchProps, + ImportDataProps, + ExportDocumentsProps, + RuleStatusResponse, + ImportDataResponse, + PrePackagedRulesStatusResponse, + BulkRuleResponse, +} from './types'; +import { KibanaServices } from '../../../../common/lib/kibana'; +import * as i18n from '../../../pages/detection_engine/rules/translations'; + +/** + * Add provided Rule + * + * @param rule to add + * @param signal to cancel request + * + * @throws An error if response is not OK + */ +export const addRule = async ({ rule, signal }: AddRulesProps): Promise => + KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_URL, { + method: rule.id != null ? 'PUT' : 'POST', + body: JSON.stringify(rule), + signal, + }); + +/** + * Fetches all rules from the Detection Engine API + * + * @param filterOptions desired filters (e.g. filter/sortField/sortOrder) + * @param pagination desired pagination options (e.g. page/perPage) + * @param signal to cancel request + * + * @throws An error if response is not OK + */ +export const fetchRules = async ({ + filterOptions = { + filter: '', + sortField: 'enabled', + sortOrder: 'desc', + showCustomRules: false, + showElasticRules: false, + tags: [], + }, + pagination = { + page: 1, + perPage: 20, + total: 0, + }, + signal, +}: FetchRulesProps): Promise => { + const filters = [ + ...(filterOptions.filter.length ? [`alert.attributes.name: ${filterOptions.filter}`] : []), + ...(filterOptions.showCustomRules + ? [`alert.attributes.tags: "__internal_immutable:false"`] + : []), + ...(filterOptions.showElasticRules + ? [`alert.attributes.tags: "__internal_immutable:true"`] + : []), + ...(filterOptions.tags?.map(t => `alert.attributes.tags: ${t}`) ?? []), + ]; + + const query = { + page: pagination.page, + per_page: pagination.perPage, + sort_field: filterOptions.sortField, + sort_order: filterOptions.sortOrder, + ...(filters.length ? { filter: filters.join(' AND ') } : {}), + }; + + return KibanaServices.get().http.fetch( + `${DETECTION_ENGINE_RULES_URL}/_find`, + { + method: 'GET', + query, + signal, + } + ); +}; + +/** + * Fetch a Rule by providing a Rule ID + * + * @param id Rule ID's (not rule_id) + * @param signal to cancel request + * + * @throws An error if response is not OK + */ +export const fetchRuleById = async ({ id, signal }: FetchRuleProps): Promise => + KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_URL, { + method: 'GET', + query: { id }, + signal, + }); + +/** + * Enables/Disables provided Rule ID's + * + * @param ids array of Rule ID's (not rule_id) to enable/disable + * @param enabled to enable or disable + * + * @throws An error if response is not OK + */ +export const enableRules = async ({ ids, enabled }: EnableRulesProps): Promise => + KibanaServices.get().http.fetch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`, { + method: 'PATCH', + body: JSON.stringify(ids.map(id => ({ id, enabled }))), + }); + +/** + * Deletes provided Rule ID's + * + * @param ids array of Rule ID's (not rule_id) to delete + * + * @throws An error if response is not OK + */ +export const deleteRules = async ({ ids }: DeleteRulesProps): Promise => + KibanaServices.get().http.fetch(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, { + method: 'DELETE', + body: JSON.stringify(ids.map(id => ({ id }))), + }); + +/** + * Duplicates provided Rules + * + * @param rules to duplicate + * + * @throws An error if response is not OK + */ +export const duplicateRules = async ({ rules }: DuplicateRulesProps): Promise => + KibanaServices.get().http.fetch(`${DETECTION_ENGINE_RULES_URL}/_bulk_create`, { + method: 'POST', + body: JSON.stringify( + rules.map(rule => ({ + ...rule, + name: `${rule.name} [${i18n.DUPLICATE}]`, + created_at: undefined, + created_by: undefined, + id: undefined, + rule_id: undefined, + updated_at: undefined, + updated_by: undefined, + enabled: rule.enabled, + immutable: undefined, + last_success_at: undefined, + last_success_message: undefined, + last_failure_at: undefined, + last_failure_message: undefined, + status: undefined, + status_date: undefined, + })) + ), + }); + +/** + * Create Prepackaged Rules + * + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const createPrepackagedRules = async ({ signal }: BasicFetchProps): Promise => { + await KibanaServices.get().http.fetch(DETECTION_ENGINE_PREPACKAGED_URL, { + method: 'PUT', + signal, + }); + + return true; +}; + +/** + * Imports rules in the same format as exported via the _export API + * + * @param fileToImport File to upload containing rules to import + * @param overwrite whether or not to overwrite rules with the same ruleId + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const importRules = async ({ + fileToImport, + overwrite = false, + signal, +}: ImportDataProps): Promise => { + const formData = new FormData(); + formData.append('file', fileToImport); + + return KibanaServices.get().http.fetch( + `${DETECTION_ENGINE_RULES_URL}/_import`, + { + method: 'POST', + headers: { 'Content-Type': undefined }, + query: { overwrite }, + body: formData, + signal, + } + ); +}; + +/** + * Export rules from the server as a file download + * + * @param excludeExportDetails whether or not to exclude additional details at bottom of exported file (defaults to false) + * @param filename of exported rules. Be sure to include `.ndjson` extension! (defaults to localized `rules_export.ndjson`) + * @param ruleIds array of rule_id's (not id!) to export (empty array exports _all_ rules) + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const exportRules = async ({ + excludeExportDetails = false, + filename = `${i18n.EXPORT_FILENAME}.ndjson`, + ids = [], + signal, +}: ExportDocumentsProps): Promise => { + const body = + ids.length > 0 ? JSON.stringify({ objects: ids.map(rule => ({ rule_id: rule })) }) : undefined; + + return KibanaServices.get().http.fetch(`${DETECTION_ENGINE_RULES_URL}/_export`, { + method: 'POST', + body, + query: { + exclude_export_details: excludeExportDetails, + file_name: filename, + }, + signal, + }); +}; + +/** + * Get Rule Status provided Rule ID + * + * @param id string of Rule ID's (not rule_id) + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const getRuleStatusById = async ({ + id, + signal, +}: { + id: string; + signal: AbortSignal; +}): Promise => + KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_STATUS_URL, { + method: 'POST', + body: JSON.stringify({ ids: [id] }), + signal, + }); + +/** + * Return rule statuses given list of alert ids + * + * @param ids array of string of Rule ID's (not rule_id) + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const getRulesStatusByIds = async ({ + ids, + signal, +}: { + ids: string[]; + signal: AbortSignal; +}): Promise => { + const res = await KibanaServices.get().http.fetch( + DETECTION_ENGINE_RULES_STATUS_URL, + { + method: 'POST', + body: JSON.stringify({ ids }), + signal, + } + ); + return res; +}; + +/** + * Fetch all unique Tags used by Rules + * + * @param signal to cancel request + * + * @throws An error if response is not OK + */ +export const fetchTags = async ({ signal }: { signal: AbortSignal }): Promise => + KibanaServices.get().http.fetch(DETECTION_ENGINE_TAGS_URL, { + method: 'GET', + signal, + }); + +/** + * Get pre packaged rules Status + * + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const getPrePackagedRulesStatus = async ({ + signal, +}: { + signal: AbortSignal; +}): Promise => + KibanaServices.get().http.fetch( + DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL, + { + method: 'GET', + signal, + } + ); diff --git a/x-pack/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.test.tsx b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/fetch_index_patterns.test.tsx similarity index 98% rename from x-pack/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.test.tsx rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/fetch_index_patterns.test.tsx index 8c688fe5615f00..79d5886f8845f1 100644 --- a/x-pack/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.test.tsx +++ b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/fetch_index_patterns.test.tsx @@ -6,14 +6,14 @@ import { renderHook, act } from '@testing-library/react-hooks'; -import { DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; -import { useApolloClient } from '../../../utils/apollo_context'; -import { mocksSource } from '../../source/mock'; +import { DEFAULT_INDEX_PATTERN } from '../../../../../common/constants'; +import { useApolloClient } from '../../../../common/utils/apollo_context'; +import { mocksSource } from '../../../../common/containers/source/mock'; import { useFetchIndexPatterns, Return } from './fetch_index_patterns'; const mockUseApolloClient = useApolloClient as jest.Mock; -jest.mock('../../../utils/apollo_context'); +jest.mock('../../../../common/utils/apollo_context'); describe('useFetchIndexPatterns', () => { beforeEach(() => { diff --git a/x-pack/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/fetch_index_patterns.tsx similarity index 89% rename from x-pack/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/fetch_index_patterns.tsx index 7e222045a1a3ba..dec9f344e16b83 100644 --- a/x-pack/plugins/siem/public/containers/detection_engine/rules/fetch_index_patterns.tsx +++ b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/fetch_index_patterns.tsx @@ -8,16 +8,16 @@ import { isEmpty, get } from 'lodash/fp'; import { useEffect, useState, Dispatch, SetStateAction } from 'react'; import deepEqual from 'fast-deep-equal'; -import { IIndexPattern } from '../../../../../../../src/plugins/data/public'; +import { IIndexPattern } from '../../../../../../../../src/plugins/data/public'; import { BrowserFields, getBrowserFields, getIndexFields, sourceQuery, -} from '../../../containers/source'; -import { errorToToaster, useStateToaster } from '../../../components/toasters'; -import { SourceQuery } from '../../../graphql/types'; -import { useApolloClient } from '../../../utils/apollo_context'; +} from '../../../../common/containers/source'; +import { errorToToaster, useStateToaster } from '../../../../common/components/toasters'; +import { SourceQuery } from '../../../../graphql/types'; +import { useApolloClient } from '../../../../common/utils/apollo_context'; import * as i18n from './translations'; diff --git a/x-pack/plugins/siem/public/containers/detection_engine/rules/index.ts b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/index.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/detection_engine/rules/index.ts rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/index.ts diff --git a/x-pack/plugins/siem/public/containers/detection_engine/rules/mock.ts b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/mock.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/detection_engine/rules/mock.ts rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/mock.ts diff --git a/x-pack/plugins/siem/public/containers/detection_engine/rules/persist_rule.test.tsx b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/persist_rule.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/containers/detection_engine/rules/persist_rule.test.tsx rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/persist_rule.test.tsx diff --git a/x-pack/plugins/siem/public/containers/detection_engine/rules/persist_rule.tsx b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/persist_rule.tsx similarity index 94% rename from x-pack/plugins/siem/public/containers/detection_engine/rules/persist_rule.tsx rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/persist_rule.tsx index 4d4f6c9d8f63a4..03080bf68cbf53 100644 --- a/x-pack/plugins/siem/public/containers/detection_engine/rules/persist_rule.tsx +++ b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/persist_rule.tsx @@ -6,7 +6,7 @@ import { useEffect, useState, Dispatch } from 'react'; -import { errorToToaster, useStateToaster } from '../../../components/toasters'; +import { errorToToaster, useStateToaster } from '../../../../common/components/toasters'; import { addRule as persistRule } from './api'; import * as i18n from './translations'; diff --git a/x-pack/plugins/siem/public/containers/detection_engine/rules/translations.ts b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/detection_engine/rules/translations.ts rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/translations.ts diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/types.ts b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/types.ts new file mode 100644 index 00000000000000..897568cdbf16ed --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/types.ts @@ -0,0 +1,246 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; + +import { RuleTypeSchema } from '../../../../../common/detection_engine/types'; + +/** + * Params is an "record", since it is a type of AlertActionParams which is action templates. + * @see x-pack/plugins/alerting/common/alert.ts + */ +export const action = t.exact( + t.type({ + group: t.string, + id: t.string, + action_type_id: t.string, + params: t.record(t.string, t.any), + }) +); + +export const NewRuleSchema = t.intersection([ + t.type({ + description: t.string, + enabled: t.boolean, + interval: t.string, + name: t.string, + risk_score: t.number, + severity: t.string, + type: RuleTypeSchema, + }), + t.partial({ + actions: t.array(action), + anomaly_threshold: t.number, + created_by: t.string, + false_positives: t.array(t.string), + filters: t.array(t.unknown), + from: t.string, + id: t.string, + index: t.array(t.string), + language: t.string, + machine_learning_job_id: t.string, + max_signals: t.number, + query: t.string, + references: t.array(t.string), + rule_id: t.string, + saved_id: t.string, + tags: t.array(t.string), + threat: t.array(t.unknown), + throttle: t.union([t.string, t.null]), + to: t.string, + updated_by: t.string, + note: t.string, + }), +]); + +export const NewRulesSchema = t.array(NewRuleSchema); +export type NewRule = t.TypeOf; + +export interface AddRulesProps { + rule: NewRule; + signal: AbortSignal; +} + +const MetaRule = t.intersection([ + t.type({ + from: t.string, + }), + t.partial({ + throttle: t.string, + kibana_siem_app_url: t.string, + }), +]); + +export const RuleSchema = t.intersection([ + t.type({ + created_at: t.string, + created_by: t.string, + description: t.string, + enabled: t.boolean, + false_positives: t.array(t.string), + from: t.string, + id: t.string, + interval: t.string, + immutable: t.boolean, + name: t.string, + max_signals: t.number, + references: t.array(t.string), + risk_score: t.number, + rule_id: t.string, + severity: t.string, + tags: t.array(t.string), + type: RuleTypeSchema, + to: t.string, + threat: t.array(t.unknown), + updated_at: t.string, + updated_by: t.string, + actions: t.array(action), + throttle: t.union([t.string, t.null]), + }), + t.partial({ + anomaly_threshold: t.number, + filters: t.array(t.unknown), + index: t.array(t.string), + language: t.string, + last_failure_at: t.string, + last_failure_message: t.string, + meta: MetaRule, + machine_learning_job_id: t.string, + output_index: t.string, + query: t.string, + saved_id: t.string, + status: t.string, + status_date: t.string, + timeline_id: t.string, + timeline_title: t.string, + note: t.string, + version: t.number, + }), +]); + +export const RulesSchema = t.array(RuleSchema); + +export type Rule = t.TypeOf; +export type Rules = t.TypeOf; + +export interface RuleError { + id?: string; + rule_id?: string; + error: { status_code: number; message: string }; +} + +export type BulkRuleResponse = Array; + +export interface RuleResponseBuckets { + rules: Rule[]; + errors: RuleError[]; +} + +export interface PaginationOptions { + page: number; + perPage: number; + total: number; +} + +export interface FetchRulesProps { + pagination?: PaginationOptions; + filterOptions?: FilterOptions; + signal: AbortSignal; +} + +export interface FilterOptions { + filter: string; + sortField: string; + sortOrder: 'asc' | 'desc'; + showCustomRules?: boolean; + showElasticRules?: boolean; + tags?: string[]; +} + +export interface FetchRulesResponse { + page: number; + perPage: number; + total: number; + data: Rule[]; +} + +export interface FetchRuleProps { + id: string; + signal: AbortSignal; +} + +export interface EnableRulesProps { + ids: string[]; + enabled: boolean; +} + +export interface DeleteRulesProps { + ids: string[]; +} + +export interface DuplicateRulesProps { + rules: Rule[]; +} + +export interface BasicFetchProps { + signal: AbortSignal; +} + +export interface ImportDataProps { + fileToImport: File; + overwrite?: boolean; + signal: AbortSignal; +} + +export interface ImportRulesResponseError { + rule_id: string; + error: { + status_code: number; + message: string; + }; +} + +export interface ImportDataResponse { + success: boolean; + success_count: number; + errors: ImportRulesResponseError[]; +} + +export interface ExportDocumentsProps { + ids: string[]; + filename?: string; + excludeExportDetails?: boolean; + signal: AbortSignal; +} + +export interface RuleStatus { + current_status: RuleInfoStatus; + failures: RuleInfoStatus[]; +} + +export type RuleStatusType = 'executing' | 'failed' | 'going to run' | 'succeeded'; +export interface RuleInfoStatus { + alert_id: string; + status_date: string; + status: RuleStatusType | null; + last_failure_at: string | null; + last_success_at: string | null; + last_failure_message: string | null; + last_success_message: string | null; + last_look_back_date: string | null | undefined; + gap: string | null | undefined; + bulk_create_time_durations: string[] | null | undefined; + search_after_time_durations: string[] | null | undefined; +} + +export type RuleStatusResponse = Record; + +export interface PrePackagedRulesStatusResponse { + rules_custom_installed: number; + rules_installed: number; + rules_not_installed: number; + rules_not_updated: number; +} diff --git a/x-pack/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_pre_packaged_rules.test.tsx diff --git a/x-pack/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_pre_packaged_rules.tsx similarity index 98% rename from x-pack/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_pre_packaged_rules.tsx index 44d5de10e361a0..f1897002e13cde 100644 --- a/x-pack/plugins/siem/public/containers/detection_engine/rules/use_pre_packaged_rules.tsx +++ b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_pre_packaged_rules.tsx @@ -6,7 +6,11 @@ import { useEffect, useState } from 'react'; -import { errorToToaster, useStateToaster, displaySuccessToast } from '../../../components/toasters'; +import { + errorToToaster, + useStateToaster, + displaySuccessToast, +} from '../../../../common/components/toasters'; import { getPrePackagedRulesStatus, createPrepackagedRules } from './api'; import * as i18n from './translations'; diff --git a/x-pack/plugins/siem/public/containers/detection_engine/rules/use_rule.test.tsx b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_rule.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/containers/detection_engine/rules/use_rule.test.tsx rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_rule.test.tsx diff --git a/x-pack/plugins/siem/public/containers/detection_engine/rules/use_rule.tsx b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_rule.tsx similarity index 94% rename from x-pack/plugins/siem/public/containers/detection_engine/rules/use_rule.tsx rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_rule.tsx index d6a49e006e1b83..6ae5da3e56ff64 100644 --- a/x-pack/plugins/siem/public/containers/detection_engine/rules/use_rule.tsx +++ b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_rule.tsx @@ -6,7 +6,7 @@ import { useEffect, useState } from 'react'; -import { errorToToaster, useStateToaster } from '../../../components/toasters'; +import { errorToToaster, useStateToaster } from '../../../../common/components/toasters'; import { fetchRuleById } from './api'; import * as i18n from './translations'; import { Rule } from './types'; diff --git a/x-pack/plugins/siem/public/containers/detection_engine/rules/use_rule_status.test.tsx b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_rule_status.test.tsx similarity index 99% rename from x-pack/plugins/siem/public/containers/detection_engine/rules/use_rule_status.test.tsx rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_rule_status.test.tsx index f74c2bad1019e8..f203eca42cde62 100644 --- a/x-pack/plugins/siem/public/containers/detection_engine/rules/use_rule_status.test.tsx +++ b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_rule_status.test.tsx @@ -12,7 +12,7 @@ import { ReturnRulesStatuses, } from './use_rule_status'; import * as api from './api'; -import { Rule } from '../rules/types'; +import { Rule } from './types'; jest.mock('./api'); diff --git a/x-pack/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_rule_status.tsx similarity index 97% rename from x-pack/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_rule_status.tsx index 412fc0706b1517..9164f38d2ac287 100644 --- a/x-pack/plugins/siem/public/containers/detection_engine/rules/use_rule_status.tsx +++ b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_rule_status.tsx @@ -6,7 +6,7 @@ import { useEffect, useRef, useState } from 'react'; -import { errorToToaster, useStateToaster } from '../../../components/toasters'; +import { errorToToaster, useStateToaster } from '../../../../common/components/toasters'; import { RuleStatusRowItemType } from '../../../pages/detection_engine/rules/all/columns'; import { getRuleStatusById, getRulesStatusByIds } from './api'; import * as i18n from './translations'; diff --git a/x-pack/plugins/siem/public/containers/detection_engine/rules/use_rules.test.tsx b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_rules.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/containers/detection_engine/rules/use_rules.test.tsx rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_rules.test.tsx diff --git a/x-pack/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_rules.tsx similarity index 97% rename from x-pack/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_rules.tsx index 6e41e229c24909..3a074f2bc3785a 100644 --- a/x-pack/plugins/siem/public/containers/detection_engine/rules/use_rules.tsx +++ b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_rules.tsx @@ -8,7 +8,7 @@ import { noop } from 'lodash/fp'; import { useEffect, useState, useRef } from 'react'; import { FetchRulesResponse, FilterOptions, PaginationOptions, Rule } from './types'; -import { errorToToaster, useStateToaster } from '../../../components/toasters'; +import { errorToToaster, useStateToaster } from '../../../../common/components/toasters'; import { fetchRules } from './api'; import * as i18n from './translations'; diff --git a/x-pack/plugins/siem/public/containers/detection_engine/rules/use_tags.test.tsx b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_tags.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/containers/detection_engine/rules/use_tags.test.tsx rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_tags.test.tsx diff --git a/x-pack/plugins/siem/public/containers/detection_engine/rules/use_tags.tsx b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_tags.tsx similarity index 94% rename from x-pack/plugins/siem/public/containers/detection_engine/rules/use_tags.tsx rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_tags.tsx index 669efedc619bbc..ebfe73f2f08639 100644 --- a/x-pack/plugins/siem/public/containers/detection_engine/rules/use_tags.tsx +++ b/x-pack/plugins/siem/public/alerts/containers/detection_engine/rules/use_tags.tsx @@ -6,7 +6,7 @@ import { noop } from 'lodash/fp'; import { useEffect, useState, useRef } from 'react'; -import { errorToToaster, useStateToaster } from '../../../components/toasters'; +import { errorToToaster, useStateToaster } from '../../../../common/components/toasters'; import { fetchTags } from './api'; import * as i18n from './translations'; diff --git a/x-pack/plugins/siem/public/containers/detection_engine/signals/__mocks__/api.ts b/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/__mocks__/api.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/detection_engine/signals/__mocks__/api.ts rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/__mocks__/api.ts diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/api.test.ts b/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/api.test.ts new file mode 100644 index 00000000000000..67d81d19faa7c5 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/api.test.ts @@ -0,0 +1,165 @@ +/* + * 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 { KibanaServices } from '../../../../common/lib/kibana'; +import { + signalsMock, + mockSignalsQuery, + mockStatusSignalQuery, + mockSignalIndex, + mockUserPrivilege, +} from './mock'; +import { + fetchQuerySignals, + updateSignalStatus, + getSignalIndex, + getUserPrivilege, + createSignalIndex, +} from './api'; + +const abortCtrl = new AbortController(); +const mockKibanaServices = KibanaServices.get as jest.Mock; +jest.mock('../../../../common/lib/kibana'); + +const fetchMock = jest.fn(); +mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } }); + +describe('Detections Signals API', () => { + describe('fetchQuerySignals', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(signalsMock); + }); + + test('check parameter url, body', async () => { + await fetchQuerySignals({ query: mockSignalsQuery, signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/signals/search', { + body: + '{"aggs":{"signalsByGrouping":{"terms":{"field":"signal.rule.risk_score","missing":"All others","order":{"_count":"desc"},"size":10},"aggs":{"signals":{"date_histogram":{"field":"@timestamp","fixed_interval":"81000000ms","min_doc_count":0,"extended_bounds":{"min":1579644343954,"max":1582236343955}}}}}},"query":{"bool":{"filter":[{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}},{"range":{"@timestamp":{"gte":1579644343954,"lte":1582236343955}}}]}}}', + method: 'POST', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const signalsResp = await fetchQuerySignals({ + query: mockSignalsQuery, + signal: abortCtrl.signal, + }); + expect(signalsResp).toEqual(signalsMock); + }); + }); + + describe('updateSignalStatus', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue({}); + }); + + test('check parameter url, body when closing a signal', async () => { + await updateSignalStatus({ + query: mockStatusSignalQuery, + signal: abortCtrl.signal, + status: 'closed', + }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/signals/status', { + body: + '{"status":"closed","bool":{"filter":{"terms":{"_id":["b4ee5c32e3a321057edcc953ca17228c6fdfe5ba43fdbbdaffa8cefa11605cc5"]}}}}', + method: 'POST', + signal: abortCtrl.signal, + }); + }); + + test('check parameter url, body when opening a signal', async () => { + await updateSignalStatus({ + query: mockStatusSignalQuery, + signal: abortCtrl.signal, + status: 'open', + }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/signals/status', { + body: + '{"status":"open","bool":{"filter":{"terms":{"_id":["b4ee5c32e3a321057edcc953ca17228c6fdfe5ba43fdbbdaffa8cefa11605cc5"]}}}}', + method: 'POST', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const signalsResp = await updateSignalStatus({ + query: mockStatusSignalQuery, + signal: abortCtrl.signal, + status: 'open', + }); + expect(signalsResp).toEqual({}); + }); + }); + + describe('getSignalIndex', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(mockSignalIndex); + }); + + test('check parameter url', async () => { + await getSignalIndex({ signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/index', { + method: 'GET', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const signalsResp = await getSignalIndex({ + signal: abortCtrl.signal, + }); + expect(signalsResp).toEqual(mockSignalIndex); + }); + }); + + describe('getUserPrivilege', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(mockUserPrivilege); + }); + + test('check parameter url', async () => { + await getUserPrivilege({ signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/privileges', { + method: 'GET', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const signalsResp = await getUserPrivilege({ + signal: abortCtrl.signal, + }); + expect(signalsResp).toEqual(mockUserPrivilege); + }); + }); + + describe('createSignalIndex', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(mockSignalIndex); + }); + + test('check parameter url', async () => { + await createSignalIndex({ signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/index', { + method: 'POST', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const signalsResp = await createSignalIndex({ + signal: abortCtrl.signal, + }); + expect(signalsResp).toEqual(mockSignalIndex); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/api.ts b/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/api.ts new file mode 100644 index 00000000000000..860305dd58e679 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/api.ts @@ -0,0 +1,101 @@ +/* + * 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 { + DETECTION_ENGINE_QUERY_SIGNALS_URL, + DETECTION_ENGINE_SIGNALS_STATUS_URL, + DETECTION_ENGINE_INDEX_URL, + DETECTION_ENGINE_PRIVILEGES_URL, +} from '../../../../../common/constants'; +import { KibanaServices } from '../../../../common/lib/kibana'; +import { + BasicSignals, + Privilege, + QuerySignals, + SignalSearchResponse, + SignalsIndex, + UpdateSignalStatusProps, +} from './types'; + +/** + * Fetch Signals by providing a query + * + * @param query String to match a dsl + * @param signal to cancel request + * + * @throws An error if response is not OK + */ +export const fetchQuerySignals = async ({ + query, + signal, +}: QuerySignals): Promise> => + KibanaServices.get().http.fetch>( + DETECTION_ENGINE_QUERY_SIGNALS_URL, + { + method: 'POST', + body: JSON.stringify(query), + signal, + } + ); + +/** + * Update signal status by query + * + * @param query of signals to update + * @param status to update to('open' / 'closed') + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const updateSignalStatus = async ({ + query, + status, + signal, +}: UpdateSignalStatusProps): Promise => + KibanaServices.get().http.fetch(DETECTION_ENGINE_SIGNALS_STATUS_URL, { + method: 'POST', + body: JSON.stringify({ status, ...query }), + signal, + }); + +/** + * Fetch Signal Index + * + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const getSignalIndex = async ({ signal }: BasicSignals): Promise => + KibanaServices.get().http.fetch(DETECTION_ENGINE_INDEX_URL, { + method: 'GET', + signal, + }); + +/** + * Get User Privileges + * + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const getUserPrivilege = async ({ signal }: BasicSignals): Promise => + KibanaServices.get().http.fetch(DETECTION_ENGINE_PRIVILEGES_URL, { + method: 'GET', + signal, + }); + +/** + * Create Signal Index if needed it + * + * @param signal AbortSignal for cancelling request + * + * @throws An error if response is not OK + */ +export const createSignalIndex = async ({ signal }: BasicSignals): Promise => + KibanaServices.get().http.fetch(DETECTION_ENGINE_INDEX_URL, { + method: 'POST', + signal, + }); diff --git a/x-pack/plugins/siem/public/containers/detection_engine/signals/mock.ts b/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/mock.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/detection_engine/signals/mock.ts rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/mock.ts diff --git a/x-pack/plugins/siem/public/containers/detection_engine/signals/translations.ts b/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/detection_engine/signals/translations.ts rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/translations.ts diff --git a/x-pack/plugins/siem/public/containers/detection_engine/signals/types.ts b/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/types.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/detection_engine/signals/types.ts rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/types.ts diff --git a/x-pack/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.test.tsx b/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/use_privilege_user.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.test.tsx rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/use_privilege_user.test.tsx diff --git a/x-pack/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx b/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/use_privilege_user.tsx similarity index 96% rename from x-pack/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/use_privilege_user.tsx index 140dd1544b12b0..e67afd686a7cab 100644 --- a/x-pack/plugins/siem/public/containers/detection_engine/signals/use_privilege_user.tsx +++ b/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/use_privilege_user.tsx @@ -6,7 +6,7 @@ import { useEffect, useState } from 'react'; -import { errorToToaster, useStateToaster } from '../../../components/toasters'; +import { errorToToaster, useStateToaster } from '../../../../common/components/toasters'; import { getUserPrivilege } from './api'; import * as i18n from './translations'; diff --git a/x-pack/plugins/siem/public/containers/detection_engine/signals/use_query.test.tsx b/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/use_query.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/containers/detection_engine/signals/use_query.test.tsx rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/use_query.test.tsx diff --git a/x-pack/plugins/siem/public/containers/detection_engine/signals/use_query.tsx b/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/use_query.tsx similarity index 100% rename from x-pack/plugins/siem/public/containers/detection_engine/signals/use_query.tsx rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/use_query.tsx diff --git a/x-pack/plugins/siem/public/containers/detection_engine/signals/use_signal_index.test.tsx b/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/use_signal_index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/containers/detection_engine/signals/use_signal_index.test.tsx rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/use_signal_index.test.tsx diff --git a/x-pack/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx b/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/use_signal_index.tsx similarity index 95% rename from x-pack/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx rename to x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/use_signal_index.tsx index a7f5c9731320e1..6c428bd9354ee3 100644 --- a/x-pack/plugins/siem/public/containers/detection_engine/signals/use_signal_index.tsx +++ b/x-pack/plugins/siem/public/alerts/containers/detection_engine/signals/use_signal_index.tsx @@ -6,10 +6,10 @@ import { useEffect, useState } from 'react'; -import { errorToToaster, useStateToaster } from '../../../components/toasters'; +import { errorToToaster, useStateToaster } from '../../../../common/components/toasters'; import { createSignalIndex, getSignalIndex } from './api'; import * as i18n from './translations'; -import { isApiError } from '../../../utils/api'; +import { isApiError } from '../../../../common/utils/api'; type Func = () => void; diff --git a/x-pack/plugins/siem/public/alerts/index.ts b/x-pack/plugins/siem/public/alerts/index.ts new file mode 100644 index 00000000000000..c1501419a1cf6f --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/index.ts @@ -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. + */ + +import { getAlertsRoutes } from './routes'; +import { SecuritySubPlugin } from '../app/types'; + +export class Alerts { + public setup() {} + + public start(): SecuritySubPlugin { + return { + routes: getAlertsRoutes(), + }; + } +} diff --git a/x-pack/plugins/siem/public/pages/detection_engine/mitre/mitre_tactics_techniques.ts b/x-pack/plugins/siem/public/alerts/mitre/mitre_tactics_techniques.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/mitre/mitre_tactics_techniques.ts rename to x-pack/plugins/siem/public/alerts/mitre/mitre_tactics_techniques.ts diff --git a/x-pack/plugins/siem/public/pages/detection_engine/mitre/types.ts b/x-pack/plugins/siem/public/alerts/mitre/types.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/mitre/types.ts rename to x-pack/plugins/siem/public/alerts/mitre/types.ts diff --git a/x-pack/plugins/siem/public/pages/detection_engine/detection_engine.test.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/detection_engine.test.tsx similarity index 80% rename from x-pack/plugins/siem/public/pages/detection_engine/detection_engine.test.tsx rename to x-pack/plugins/siem/public/alerts/pages/detection_engine/detection_engine.test.tsx index 779e9a4557f2af..de8a732839728d 100644 --- a/x-pack/plugins/siem/public/pages/detection_engine/detection_engine.test.tsx +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/detection_engine.test.tsx @@ -8,13 +8,13 @@ import React from 'react'; import { shallow } from 'enzyme'; import { useParams } from 'react-router-dom'; -import '../../mock/match_media'; -import { setAbsoluteRangeDatePicker } from '../../store/inputs/actions'; +import '../../../common/mock/match_media'; +import { setAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions'; import { DetectionEnginePageComponent } from './detection_engine'; -import { useUserInfo } from './components/user_info'; +import { useUserInfo } from '../../components/user_info'; -jest.mock('./components/user_info'); -jest.mock('../../lib/kibana'); +jest.mock('../../components/user_info'); +jest.mock('../../../common/lib/kibana'); jest.mock('react-router-dom', () => { const originalModule = jest.requireActual('react-router-dom'); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/detection_engine.tsx similarity index 80% rename from x-pack/plugins/siem/public/pages/detection_engine/detection_engine.tsx rename to x-pack/plugins/siem/public/alerts/pages/detection_engine/detection_engine.tsx index 3e23700b08e66d..a83a85678bd03e 100644 --- a/x-pack/plugins/siem/public/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/detection_engine.tsx @@ -10,35 +10,38 @@ import { useParams } from 'react-router-dom'; import { StickyContainer } from 'react-sticky'; import { connect, ConnectedProps } from 'react-redux'; -import { GlobalTime } from '../../containers/global_time'; -import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source'; -import { AlertsTable } from '../../components/alerts_viewer/alerts_table'; -import { UpdateDateRange } from '../../components/charts/common'; -import { FiltersGlobal } from '../../components/filters_global'; +import { GlobalTime } from '../../../common/containers/global_time'; +import { + indicesExistOrDataTemporarilyUnavailable, + WithSource, +} from '../../../common/containers/source'; +import { AlertsTable } from '../../../common/components/alerts_viewer/alerts_table'; +import { UpdateDateRange } from '../../../common/components/charts/common'; +import { FiltersGlobal } from '../../../common/components/filters_global'; import { getDetectionEngineTabUrl, getRulesUrl, -} from '../../components/link_to/redirect_to_detection_engine'; -import { SiemSearchBar } from '../../components/search_bar'; -import { WrapperPage } from '../../components/wrapper_page'; -import { SiemNavigation } from '../../components/navigation'; -import { NavTab } from '../../components/navigation/types'; -import { State } from '../../store'; -import { inputsSelectors } from '../../store/inputs'; -import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions'; -import { SpyRoute } from '../../utils/route/spy_routes'; -import { InputsRange } from '../../store/inputs/model'; -import { AlertsByCategory } from '../overview/alerts_by_category'; -import { useSignalInfo } from './components/signals_info'; -import { SignalsTable } from './components/signals'; -import { NoApiIntegrationKeyCallOut } from './components/no_api_integration_callout'; -import { NoWriteSignalsCallOut } from './components/no_write_signals_callout'; -import { SignalsHistogramPanel } from './components/signals_histogram_panel'; -import { signalsHistogramOptions } from './components/signals_histogram_panel/config'; -import { useUserInfo } from './components/user_info'; +} from '../../../common/components/link_to/redirect_to_detection_engine'; +import { SiemSearchBar } from '../../../common/components/search_bar'; +import { WrapperPage } from '../../../common/components/wrapper_page'; +import { SiemNavigation } from '../../../common/components/navigation'; +import { NavTab } from '../../../common/components/navigation/types'; +import { State } from '../../../common/store'; +import { inputsSelectors } from '../../../common/store/inputs'; +import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions'; +import { SpyRoute } from '../../../common/utils/route/spy_routes'; +import { InputsRange } from '../../../common/store/inputs/model'; +import { AlertsByCategory } from '../../../overview/components/alerts_by_category'; +import { useSignalInfo } from '../../components/signals_info'; +import { SignalsTable } from '../../components/signals'; +import { NoApiIntegrationKeyCallOut } from '../../components/no_api_integration_callout'; +import { NoWriteSignalsCallOut } from '../../components/no_write_signals_callout'; +import { SignalsHistogramPanel } from '../../components/signals_histogram_panel'; +import { signalsHistogramOptions } from '../../components/signals_histogram_panel/config'; +import { useUserInfo } from '../../components/user_info'; import { DetectionEngineEmptyPage } from './detection_engine_empty_page'; import { DetectionEngineNoIndex } from './detection_engine_no_signal_index'; -import { DetectionEngineHeaderPage } from './components/detection_engine_header_page'; +import { DetectionEngineHeaderPage } from '../../components/detection_engine_header_page'; import { DetectionEngineUserUnauthenticated } from './detection_engine_user_unauthenticated'; import * as i18n from './translations'; import { DetectionEngineTab } from './types'; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/detection_engine_empty_page.test.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/detection_engine_empty_page.test.tsx similarity index 93% rename from x-pack/plugins/siem/public/pages/detection_engine/detection_engine_empty_page.test.tsx rename to x-pack/plugins/siem/public/alerts/pages/detection_engine/detection_engine_empty_page.test.tsx index f64526fd2f7c46..039c878b121a09 100644 --- a/x-pack/plugins/siem/public/pages/detection_engine/detection_engine_empty_page.test.tsx +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/detection_engine_empty_page.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { DetectionEngineEmptyPage } from './detection_engine_empty_page'; -jest.mock('../../lib/kibana'); +jest.mock('../../../common/lib/kibana'); describe('DetectionEngineEmptyPage', () => { it('renders correctly', () => { diff --git a/x-pack/plugins/siem/public/pages/detection_engine/detection_engine_empty_page.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/detection_engine_empty_page.tsx similarity index 83% rename from x-pack/plugins/siem/public/pages/detection_engine/detection_engine_empty_page.tsx rename to x-pack/plugins/siem/public/alerts/pages/detection_engine/detection_engine_empty_page.tsx index 7516bb13a9e750..3d8f221a023754 100644 --- a/x-pack/plugins/siem/public/pages/detection_engine/detection_engine_empty_page.tsx +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/detection_engine_empty_page.tsx @@ -6,9 +6,9 @@ import React from 'react'; -import { useKibana } from '../../lib/kibana'; -import { EmptyPage } from '../../components/empty_page'; -import * as i18n from '../common/translations'; +import { useKibana } from '../../../common/lib/kibana'; +import { EmptyPage } from '../../../common/components/empty_page'; +import * as i18n from '../../../common/translations'; export const DetectionEngineEmptyPage = React.memo(() => ( { it('renders correctly', () => { diff --git a/x-pack/plugins/siem/public/pages/detection_engine/detection_engine_no_signal_index.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/detection_engine_no_signal_index.tsx similarity index 88% rename from x-pack/plugins/siem/public/pages/detection_engine/detection_engine_no_signal_index.tsx rename to x-pack/plugins/siem/public/alerts/pages/detection_engine/detection_engine_no_signal_index.tsx index f1478ab5858c9a..59267b5d62a26d 100644 --- a/x-pack/plugins/siem/public/pages/detection_engine/detection_engine_no_signal_index.tsx +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/detection_engine_no_signal_index.tsx @@ -6,9 +6,9 @@ import React from 'react'; -import { EmptyPage } from '../../components/empty_page'; +import { EmptyPage } from '../../../common/components/empty_page'; import * as i18n from './translations'; -import { useKibana } from '../../lib/kibana'; +import { useKibana } from '../../../common/lib/kibana'; export const DetectionEngineNoIndex = React.memo(() => { const docLinks = useKibana().services.docLinks; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/detection_engine_user_unauthenticated.test.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/detection_engine_user_unauthenticated.test.tsx similarity index 93% rename from x-pack/plugins/siem/public/pages/detection_engine/detection_engine_user_unauthenticated.test.tsx rename to x-pack/plugins/siem/public/alerts/pages/detection_engine/detection_engine_user_unauthenticated.test.tsx index e71f4de2b010bb..5a1efe1c71857b 100644 --- a/x-pack/plugins/siem/public/pages/detection_engine/detection_engine_user_unauthenticated.test.tsx +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/detection_engine_user_unauthenticated.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { DetectionEngineUserUnauthenticated } from './detection_engine_user_unauthenticated'; -jest.mock('../../lib/kibana'); +jest.mock('../../../common/lib/kibana'); describe('DetectionEngineUserUnauthenticated', () => { it('renders correctly', () => { diff --git a/x-pack/plugins/siem/public/pages/detection_engine/detection_engine_user_unauthenticated.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/detection_engine_user_unauthenticated.tsx similarity index 88% rename from x-pack/plugins/siem/public/pages/detection_engine/detection_engine_user_unauthenticated.tsx rename to x-pack/plugins/siem/public/alerts/pages/detection_engine/detection_engine_user_unauthenticated.tsx index b5c805f92135ad..fc1fee1077bd65 100644 --- a/x-pack/plugins/siem/public/pages/detection_engine/detection_engine_user_unauthenticated.tsx +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/detection_engine_user_unauthenticated.tsx @@ -6,9 +6,9 @@ import React from 'react'; -import { EmptyPage } from '../../components/empty_page'; +import { EmptyPage } from '../../../common/components/empty_page'; import * as i18n from './translations'; -import { useKibana } from '../../lib/kibana'; +import { useKibana } from '../../../common/lib/kibana'; export const DetectionEngineUserUnauthenticated = React.memo(() => { const docLinks = useKibana().services.docLinks; diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/index.test.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/index.test.tsx new file mode 100644 index 00000000000000..d4e654321ef988 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/index.test.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import '../../../common/mock/match_media'; +import { DetectionEngineContainer } from './index'; + +describe('DetectionEngineContainer', () => { + it('renders correctly', () => { + const wrapper = shallow(); + + expect(wrapper.find('ManageUserInfo')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/index.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/index.tsx new file mode 100644 index 00000000000000..756e222c029502 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/index.tsx @@ -0,0 +1,54 @@ +/* + * 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 { Redirect, Route, Switch, RouteComponentProps } from 'react-router-dom'; + +import { ManageUserInfo } from '../../components/user_info'; +import { CreateRulePage } from './rules/create'; +import { DetectionEnginePage } from './detection_engine'; +import { EditRulePage } from './rules/edit'; +import { RuleDetailsPage } from './rules/details'; +import { RulesPage } from './rules'; +import { DetectionEngineTab } from './types'; + +const detectionEnginePath = `/:pageName(detections)`; + +type Props = Partial> & { url: string }; + +const DetectionEngineContainerComponent: React.FC = () => ( + + + + + + + + + + + + + + + + + + ( + + )} + /> + + +); + +export const DetectionEngineContainer = React.memo(DetectionEngineContainerComponent); diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/__mocks__/mock.ts b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/__mocks__/mock.ts new file mode 100644 index 00000000000000..1b43a513d0d297 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/__mocks__/mock.ts @@ -0,0 +1,218 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { esFilters } from '../../../../../../../../../../src/plugins/data/public'; +import { Rule, RuleError } from '../../../../../../alerts/containers/detection_engine/rules'; +import { AboutStepRule, ActionsStepRule, DefineStepRule, ScheduleStepRule } from '../../types'; +import { FieldValueQueryBar } from '../../../../../../alerts/components/rules/query_bar'; + +export const mockQueryBar: FieldValueQueryBar = { + query: { + query: 'test query', + language: 'kuery', + }, + filters: [ + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ], + saved_id: 'test123', +}; + +export const mockRule = (id: string): Rule => ({ + actions: [], + created_at: '2020-01-10T21:11:45.839Z', + updated_at: '2020-01-10T21:11:45.839Z', + created_by: 'elastic', + description: '24/7', + enabled: true, + false_positives: [], + filters: [], + from: 'now-300s', + id, + immutable: false, + index: ['auditbeat-*'], + interval: '5m', + rule_id: 'b5ba41ab-aaf3-4f43-971b-bdf9434ce0ea', + language: 'kuery', + output_index: '.siem-signals-default', + max_signals: 100, + risk_score: 21, + name: 'Home Grown!', + query: '', + references: [], + saved_id: "Garrett's IP", + timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + timeline_title: 'Untitled timeline', + meta: { from: '0m' }, + severity: 'low', + updated_by: 'elastic', + tags: [], + to: 'now', + type: 'saved_query', + threat: [], + throttle: 'no_actions', + note: '# this is some markdown documentation', + version: 1, +}); + +export const mockRuleWithEverything = (id: string): Rule => ({ + actions: [], + created_at: '2020-01-10T21:11:45.839Z', + updated_at: '2020-01-10T21:11:45.839Z', + created_by: 'elastic', + description: '24/7', + enabled: true, + false_positives: ['test'], + filters: [ + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ], + from: 'now-300s', + id, + immutable: false, + index: ['auditbeat-*'], + interval: '5m', + rule_id: 'b5ba41ab-aaf3-4f43-971b-bdf9434ce0ea', + language: 'kuery', + output_index: '.siem-signals-default', + max_signals: 100, + risk_score: 21, + name: 'Query with rule-id', + query: 'user.name: root or user.name: admin', + references: ['www.test.co'], + saved_id: 'test123', + timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + timeline_title: 'Titled timeline', + meta: { from: '0m' }, + severity: 'low', + updated_by: 'elastic', + tags: ['tag1', 'tag2'], + to: 'now', + type: 'saved_query', + threat: [ + { + framework: 'mockFramework', + tactic: { + id: '1234', + name: 'tactic1', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + throttle: 'no_actions', + note: '# this is some markdown documentation', + version: 1, +}); + +export const mockAboutStepRule = (isNew = false): AboutStepRule => ({ + isNew, + name: 'Query with rule-id', + description: '24/7', + severity: 'low', + riskScore: 21, + references: ['www.test.co'], + falsePositives: ['test'], + tags: ['tag1', 'tag2'], + threat: [ + { + framework: 'mockFramework', + tactic: { + id: '1234', + name: 'tactic1', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + note: '# this is some markdown documentation', +}); + +export const mockActionsStepRule = (isNew = false, enabled = false): ActionsStepRule => ({ + isNew, + actions: [], + kibanaSiemAppUrl: 'http://localhost:5601/app/siem', + enabled, + throttle: 'no_actions', +}); + +export const mockDefineStepRule = (isNew = false): DefineStepRule => ({ + isNew, + ruleType: 'query', + anomalyThreshold: 50, + machineLearningJobId: '', + index: ['filebeat-'], + queryBar: mockQueryBar, + timeline: { + id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + title: 'Titled timeline', + }, +}); + +export const mockScheduleStepRule = (isNew = false): ScheduleStepRule => ({ + isNew, + interval: '5m', + from: '6m', + to: 'now', +}); + +export const mockRuleError = (id: string): RuleError => ({ + rule_id: id, + error: { status_code: 404, message: `id: "${id}" not found` }, +}); + +export const mockRules: Rule[] = [ + mockRule('abe6c564-050d-45a5-aaf0-386c37dd1f61'), + mockRule('63f06f34-c181-4b2d-af35-f2ace572a1ee'), +]; diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/actions.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/actions.tsx new file mode 100644 index 00000000000000..5ed7221b68bf35 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/actions.tsx @@ -0,0 +1,136 @@ +/* + * 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 H from 'history'; +import React, { Dispatch } from 'react'; + +import { DETECTION_ENGINE_PAGE_NAME } from '../../../../../common/components/link_to/redirect_to_detection_engine'; +import { + deleteRules, + duplicateRules, + enableRules, + Rule, +} from '../../../../../alerts/containers/detection_engine/rules'; +import { Action } from './reducer'; + +import { + ActionToaster, + displayErrorToast, + displaySuccessToast, + errorToToaster, +} from '../../../../../common/components/toasters'; +import { track, METRIC_TYPE, TELEMETRY_EVENT } from '../../../../../common/lib/telemetry'; + +import * as i18n from '../translations'; +import { bucketRulesResponse } from './helpers'; + +export const editRuleAction = (rule: Rule, history: H.History) => { + history.push(`/${DETECTION_ENGINE_PAGE_NAME}/rules/id/${rule.id}/edit`); +}; + +export const duplicateRulesAction = async ( + rules: Rule[], + ruleIds: string[], + dispatch: React.Dispatch, + dispatchToaster: Dispatch +) => { + try { + dispatch({ type: 'loadingRuleIds', ids: ruleIds, actionType: 'duplicate' }); + const response = await duplicateRules({ rules }); + const { errors } = bucketRulesResponse(response); + if (errors.length > 0) { + displayErrorToast( + i18n.DUPLICATE_RULE_ERROR, + errors.map(e => e.error.message), + dispatchToaster + ); + } else { + displaySuccessToast(i18n.SUCCESSFULLY_DUPLICATED_RULES(ruleIds.length), dispatchToaster); + } + dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); + } catch (error) { + dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); + errorToToaster({ title: i18n.DUPLICATE_RULE_ERROR, error, dispatchToaster }); + } +}; + +export const exportRulesAction = (exportRuleId: string[], dispatch: React.Dispatch) => { + dispatch({ type: 'exportRuleIds', ids: exportRuleId }); +}; + +export const deleteRulesAction = async ( + ruleIds: string[], + dispatch: React.Dispatch, + dispatchToaster: Dispatch, + onRuleDeleted?: () => void +) => { + try { + dispatch({ type: 'loadingRuleIds', ids: ruleIds, actionType: 'delete' }); + const response = await deleteRules({ ids: ruleIds }); + const { errors } = bucketRulesResponse(response); + dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); + if (errors.length > 0) { + displayErrorToast( + i18n.BATCH_ACTION_DELETE_SELECTED_ERROR(ruleIds.length), + errors.map(e => e.error.message), + dispatchToaster + ); + } else if (onRuleDeleted) { + onRuleDeleted(); + } + } catch (error) { + dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); + errorToToaster({ + title: i18n.BATCH_ACTION_DELETE_SELECTED_ERROR(ruleIds.length), + error, + dispatchToaster, + }); + } +}; + +export const enableRulesAction = async ( + ids: string[], + enabled: boolean, + dispatch: React.Dispatch, + dispatchToaster: Dispatch +) => { + const errorTitle = enabled + ? i18n.BATCH_ACTION_ACTIVATE_SELECTED_ERROR(ids.length) + : i18n.BATCH_ACTION_DEACTIVATE_SELECTED_ERROR(ids.length); + + try { + dispatch({ type: 'loadingRuleIds', ids, actionType: enabled ? 'enable' : 'disable' }); + + const response = await enableRules({ ids, enabled }); + const { rules, errors } = bucketRulesResponse(response); + + dispatch({ type: 'updateRules', rules }); + + if (errors.length > 0) { + displayErrorToast( + errorTitle, + errors.map(e => e.error.message), + dispatchToaster + ); + } + + if (rules.some(rule => rule.immutable)) { + track( + METRIC_TYPE.COUNT, + enabled ? TELEMETRY_EVENT.SIEM_RULE_ENABLED : TELEMETRY_EVENT.SIEM_RULE_DISABLED + ); + } + if (rules.some(rule => !rule.immutable)) { + track( + METRIC_TYPE.COUNT, + enabled ? TELEMETRY_EVENT.CUSTOM_RULE_ENABLED : TELEMETRY_EVENT.CUSTOM_RULE_DISABLED + ); + } + } catch (e) { + displayErrorToast(errorTitle, [e.message], dispatchToaster); + dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); + } +}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/batch_actions.tsx similarity index 97% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx rename to x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/batch_actions.tsx index 454ef18e0ae143..769839a62091b0 100644 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/all/batch_actions.tsx +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/batch_actions.tsx @@ -14,8 +14,8 @@ import { enableRulesAction, exportRulesAction, } from './actions'; -import { ActionToaster, displayWarningToast } from '../../../../components/toasters'; -import { Rule } from '../../../../containers/detection_engine/rules'; +import { ActionToaster, displayWarningToast } from '../../../../../common/components/toasters'; +import { Rule } from '../../../../../alerts/containers/detection_engine/rules'; import * as detectionI18n from '../../translations'; interface GetBatchItems { diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/all/columns.test.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/columns.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/all/columns.test.tsx rename to x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/columns.test.tsx diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/columns.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/columns.tsx new file mode 100644 index 00000000000000..224a32ef6ac9d8 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/columns.tsx @@ -0,0 +1,333 @@ +/* + * 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. + */ + +/* eslint-disable react/display-name */ + +import { + EuiBadge, + EuiLink, + EuiBasicTableColumn, + EuiTableActionsColumnType, + EuiText, + EuiHealth, + EuiToolTip, +} from '@elastic/eui'; +import { FormattedRelative } from '@kbn/i18n/react'; +import * as H from 'history'; +import React, { Dispatch } from 'react'; + +import { isMlRule } from '../../../../../../common/machine_learning/helpers'; +import { Rule, RuleStatus } from '../../../../../alerts/containers/detection_engine/rules'; +import { getEmptyTagValue } from '../../../../../common/components/empty_value'; +import { FormattedDate } from '../../../../../common/components/formatted_date'; +import { getRuleDetailsUrl } from '../../../../../common/components/link_to/redirect_to_detection_engine'; +import { ActionToaster } from '../../../../../common/components/toasters'; +import { TruncatableText } from '../../../../../common/components/truncatable_text'; +import { getStatusColor } from '../../../../components/rules/rule_status/helpers'; +import { RuleSwitch } from '../../../../components/rules/rule_switch'; +import { SeverityBadge } from '../../../../components/rules/severity_badge'; +import * as i18n from '../translations'; +import { + deleteRulesAction, + duplicateRulesAction, + editRuleAction, + exportRulesAction, +} from './actions'; +import { Action } from './reducer'; +import { LocalizedDateTooltip } from '../../../../../common/components/localized_date_tooltip'; +import * as detectionI18n from '../../translations'; + +export const getActions = ( + dispatch: React.Dispatch, + dispatchToaster: Dispatch, + history: H.History, + reFetchRules: (refreshPrePackagedRule?: boolean) => void +) => [ + { + description: i18n.EDIT_RULE_SETTINGS, + icon: 'controlsHorizontal', + name: i18n.EDIT_RULE_SETTINGS, + onClick: (rowItem: Rule) => editRuleAction(rowItem, history), + }, + { + description: i18n.DUPLICATE_RULE, + icon: 'copy', + name: i18n.DUPLICATE_RULE, + onClick: async (rowItem: Rule) => { + await duplicateRulesAction([rowItem], [rowItem.id], dispatch, dispatchToaster); + await reFetchRules(true); + }, + }, + { + 'data-test-subj': 'exportRuleAction', + description: i18n.EXPORT_RULE, + icon: 'exportAction', + name: i18n.EXPORT_RULE, + onClick: (rowItem: Rule) => exportRulesAction([rowItem.rule_id], dispatch), + enabled: (rowItem: Rule) => !rowItem.immutable, + }, + { + 'data-test-subj': 'deleteRuleAction', + description: i18n.DELETE_RULE, + icon: 'trash', + name: i18n.DELETE_RULE, + onClick: async (rowItem: Rule) => { + await deleteRulesAction([rowItem.id], dispatch, dispatchToaster); + await reFetchRules(true); + }, + }, +]; + +export type RuleStatusRowItemType = RuleStatus & { + name: string; + id: string; +}; +export type RulesColumns = EuiBasicTableColumn | EuiTableActionsColumnType; +export type RulesStatusesColumns = EuiBasicTableColumn; + +interface GetColumns { + dispatch: React.Dispatch; + dispatchToaster: Dispatch; + history: H.History; + hasMlPermissions: boolean; + hasNoPermissions: boolean; + loadingRuleIds: string[]; + reFetchRules: (refreshPrePackagedRule?: boolean) => void; +} + +// Michael: Are we able to do custom, in-table-header filters, as shown in my wireframes? +export const getColumns = ({ + dispatch, + dispatchToaster, + history, + hasMlPermissions, + hasNoPermissions, + loadingRuleIds, + reFetchRules, +}: GetColumns): RulesColumns[] => { + const cols: RulesColumns[] = [ + { + field: 'name', + name: i18n.COLUMN_RULE, + render: (value: Rule['name'], item: Rule) => ( + + {value} + + ), + truncateText: true, + width: '24%', + }, + { + field: 'risk_score', + name: i18n.COLUMN_RISK_SCORE, + render: (value: Rule['risk_score']) => ( + + {value} + + ), + truncateText: true, + width: '14%', + }, + { + field: 'severity', + name: i18n.COLUMN_SEVERITY, + render: (value: Rule['severity']) => , + truncateText: true, + width: '16%', + }, + { + field: 'status_date', + name: i18n.COLUMN_LAST_COMPLETE_RUN, + render: (value: Rule['status_date']) => { + return value == null ? ( + getEmptyTagValue() + ) : ( + + + + ); + }, + truncateText: true, + width: '20%', + }, + { + field: 'status', + name: i18n.COLUMN_LAST_RESPONSE, + render: (value: Rule['status']) => { + return ( + <> + + {value ?? getEmptyTagValue()} + + + ); + }, + width: '16%', + truncateText: true, + }, + { + field: 'tags', + name: i18n.COLUMN_TAGS, + render: (value: Rule['tags']) => ( + + {value.map((tag, i) => ( + + {tag} + + ))} + + ), + truncateText: true, + width: '20%', + }, + { + align: 'center', + field: 'enabled', + name: i18n.COLUMN_ACTIVATE, + render: (value: Rule['enabled'], item: Rule) => ( + + + + ), + sortable: true, + width: '95px', + }, + ]; + const actions: RulesColumns[] = [ + { + actions: getActions(dispatch, dispatchToaster, history, reFetchRules), + width: '40px', + } as EuiTableActionsColumnType, + ]; + + return hasNoPermissions ? cols : [...cols, ...actions]; +}; + +export const getMonitoringColumns = (): RulesStatusesColumns[] => { + const cols: RulesStatusesColumns[] = [ + { + field: 'name', + name: i18n.COLUMN_RULE, + render: (value: RuleStatus['current_status']['status'], item: RuleStatusRowItemType) => { + return ( + + {value} + + ); + }, + truncateText: true, + width: '24%', + }, + { + field: 'current_status.bulk_create_time_durations', + name: i18n.COLUMN_INDEXING_TIMES, + render: (value: RuleStatus['current_status']['bulk_create_time_durations']) => ( + + {value != null && value.length > 0 + ? Math.max(...value?.map(item => Number.parseFloat(item))) + : getEmptyTagValue()} + + ), + truncateText: true, + width: '14%', + }, + { + field: 'current_status.search_after_time_durations', + name: i18n.COLUMN_QUERY_TIMES, + render: (value: RuleStatus['current_status']['search_after_time_durations']) => ( + + {value != null && value.length > 0 + ? Math.max(...value?.map(item => Number.parseFloat(item))) + : getEmptyTagValue()} + + ), + truncateText: true, + width: '14%', + }, + { + field: 'current_status.gap', + name: i18n.COLUMN_GAP, + render: (value: RuleStatus['current_status']['gap']) => ( + + {value ?? getEmptyTagValue()} + + ), + truncateText: true, + width: '14%', + }, + { + field: 'current_status.last_look_back_date', + name: i18n.COLUMN_LAST_LOOKBACK_DATE, + render: (value: RuleStatus['current_status']['last_look_back_date']) => { + return value == null ? ( + getEmptyTagValue() + ) : ( + + ); + }, + truncateText: true, + width: '16%', + }, + { + field: 'current_status.status_date', + name: i18n.COLUMN_LAST_COMPLETE_RUN, + render: (value: RuleStatus['current_status']['status_date']) => { + return value == null ? ( + getEmptyTagValue() + ) : ( + + + + ); + }, + truncateText: true, + width: '20%', + }, + { + field: 'current_status.status', + name: i18n.COLUMN_LAST_RESPONSE, + render: (value: RuleStatus['current_status']['status']) => { + return ( + <> + + {value ?? getEmptyTagValue()} + + + ); + }, + width: '16%', + truncateText: true, + }, + { + field: 'activate', + name: i18n.COLUMN_ACTIVATE, + render: (value: Rule['enabled']) => ( + + {value ? i18n.ACTIVE : i18n.INACTIVE} + + ), + width: '95px', + }, + ]; + + return cols; +}; diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/helpers.test.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/helpers.test.tsx new file mode 100644 index 00000000000000..7350cec0115fb9 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/helpers.test.tsx @@ -0,0 +1,89 @@ +/* + * 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 { bucketRulesResponse, showRulesTable } from './helpers'; +import { mockRule, mockRuleError } from './__mocks__/mock'; +import uuid from 'uuid'; +import { Rule, RuleError } from '../../../../../alerts/containers/detection_engine/rules'; + +describe('AllRulesTable Helpers', () => { + const mockRule1: Readonly = mockRule(uuid.v4()); + const mockRule2: Readonly = mockRule(uuid.v4()); + const mockRuleError1: Readonly = mockRuleError(uuid.v4()); + const mockRuleError2: Readonly = mockRuleError(uuid.v4()); + + describe('bucketRulesResponse', () => { + test('buckets empty response', () => { + const bucketedResponse = bucketRulesResponse([]); + expect(bucketedResponse).toEqual({ rules: [], errors: [] }); + }); + + test('buckets all error response', () => { + const bucketedResponse = bucketRulesResponse([mockRuleError1, mockRuleError2]); + expect(bucketedResponse).toEqual({ rules: [], errors: [mockRuleError1, mockRuleError2] }); + }); + + test('buckets all success response', () => { + const bucketedResponse = bucketRulesResponse([mockRule1, mockRule2]); + expect(bucketedResponse).toEqual({ rules: [mockRule1, mockRule2], errors: [] }); + }); + + test('buckets mixed success/error response', () => { + const bucketedResponse = bucketRulesResponse([ + mockRule1, + mockRuleError1, + mockRule2, + mockRuleError2, + ]); + expect(bucketedResponse).toEqual({ + rules: [mockRule1, mockRule2], + errors: [mockRuleError1, mockRuleError2], + }); + }); + }); + + describe('showRulesTable', () => { + test('returns false when rulesCustomInstalled and rulesInstalled are null', () => { + const result = showRulesTable({ + rulesCustomInstalled: null, + rulesInstalled: null, + }); + expect(result).toBeFalsy(); + }); + + test('returns false when rulesCustomInstalled and rulesInstalled are 0', () => { + const result = showRulesTable({ + rulesCustomInstalled: 0, + rulesInstalled: 0, + }); + expect(result).toBeFalsy(); + }); + + test('returns false when both rulesCustomInstalled and rulesInstalled checks return false', () => { + const result = showRulesTable({ + rulesCustomInstalled: 0, + rulesInstalled: null, + }); + expect(result).toBeFalsy(); + }); + + test('returns true if rulesCustomInstalled is not null or 0', () => { + const result = showRulesTable({ + rulesCustomInstalled: 5, + rulesInstalled: null, + }); + expect(result).toBeTruthy(); + }); + + test('returns true if rulesInstalled is not null or 0', () => { + const result = showRulesTable({ + rulesCustomInstalled: null, + rulesInstalled: 5, + }); + expect(result).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/helpers.ts b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/helpers.ts new file mode 100644 index 00000000000000..632d03cebef713 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/helpers.ts @@ -0,0 +1,35 @@ +/* + * 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 { + BulkRuleResponse, + RuleResponseBuckets, +} from '../../../../../alerts/containers/detection_engine/rules'; + +/** + * Separates rules/errors from bulk rules API response (create/update/delete) + * + * @param response BulkRuleResponse from bulk rules API + */ +export const bucketRulesResponse = (response: BulkRuleResponse) => + response.reduce( + (acc, cv): RuleResponseBuckets => { + return 'error' in cv + ? { rules: [...acc.rules], errors: [...acc.errors, cv] } + : { rules: [...acc.rules, cv], errors: [...acc.errors] }; + }, + { rules: [], errors: [] } + ); + +export const showRulesTable = ({ + rulesCustomInstalled, + rulesInstalled, +}: { + rulesCustomInstalled: number | null; + rulesInstalled: number | null; +}) => + (rulesCustomInstalled != null && rulesCustomInstalled > 0) || + (rulesInstalled != null && rulesInstalled > 0); diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/index.test.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/index.test.tsx new file mode 100644 index 00000000000000..11909ae7d9c531 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/index.test.tsx @@ -0,0 +1,230 @@ +/* + * 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 { shallow, mount } from 'enzyme'; +import { act } from 'react-dom/test-utils'; + +import { createKibanaContextProviderMock } from '../../../../../common/mock/kibana_react'; +import { TestProviders } from '../../../../../common/mock'; +import { wait } from '../../../../../common/lib/helpers'; +import { AllRules } from './index'; + +jest.mock('./reducer', () => { + return { + allRulesReducer: jest.fn().mockReturnValue(() => ({ + exportRuleIds: [], + filterOptions: { + filter: 'some filter', + sortField: 'some sort field', + sortOrder: 'desc', + }, + loadingRuleIds: [], + loadingRulesAction: null, + pagination: { + page: 1, + perPage: 20, + total: 1, + }, + rules: [ + { + actions: [], + created_at: '2020-02-14T19:49:28.178Z', + created_by: 'elastic', + description: 'jibber jabber', + enabled: false, + false_positives: [], + filters: [], + from: 'now-660s', + id: 'rule-id-1', + immutable: true, + index: ['endgame-*'], + interval: '10m', + language: 'kuery', + max_signals: 100, + name: 'Credential Dumping - Detected - Elastic Endpoint', + output_index: '.siem-signals-default', + query: 'host.name:*', + references: [], + risk_score: 73, + rule_id: '571afc56-5ed9-465d-a2a9-045f099f6e7e', + severity: 'high', + tags: ['Elastic', 'Endpoint'], + threat: [], + throttle: null, + to: 'now', + type: 'query', + updated_at: '2020-02-14T19:49:28.320Z', + updated_by: 'elastic', + version: 1, + }, + ], + selectedRuleIds: [], + })), + }; +}); + +jest.mock('../../../../../alerts/containers/detection_engine/rules', () => { + return { + useRules: jest.fn().mockReturnValue([ + false, + { + page: 1, + perPage: 20, + total: 1, + data: [ + { + actions: [], + created_at: '2020-02-14T19:49:28.178Z', + created_by: 'elastic', + description: 'jibber jabber', + enabled: false, + false_positives: [], + filters: [], + from: 'now-660s', + id: 'rule-id-1', + immutable: true, + index: ['endgame-*'], + interval: '10m', + language: 'kuery', + max_signals: 100, + name: 'Credential Dumping - Detected - Elastic Endpoint', + output_index: '.siem-signals-default', + query: 'host.name:*', + references: [], + risk_score: 73, + rule_id: '571afc56-5ed9-465d-a2a9-045f099f6e7e', + severity: 'high', + tags: ['Elastic', 'Endpoint'], + threat: [], + throttle: null, + to: 'now', + type: 'query', + updated_at: '2020-02-14T19:49:28.320Z', + updated_by: 'elastic', + version: 1, + }, + ], + }, + ]), + useRulesStatuses: jest.fn().mockReturnValue({ + loading: false, + rulesStatuses: [ + { + current_status: { + alert_id: 'alertId', + bulk_create_time_durations: ['2235.01'], + gap: null, + last_failure_at: null, + last_failure_message: null, + last_look_back_date: new Date().toISOString(), + last_success_at: new Date().toISOString(), + last_success_message: 'it is a success', + search_after_time_durations: ['616.97'], + status: 'succeeded', + status_date: new Date().toISOString(), + }, + failures: [], + id: '12345678987654321', + activate: true, + name: 'Test rule', + }, + ], + }), + }; +}); + +jest.mock('react-router-dom', () => { + const originalModule = jest.requireActual('react-router-dom'); + + return { + ...originalModule, + useHistory: jest.fn(), + }; +}); + +describe('AllRules', () => { + it('renders correctly', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find('[title="All rules"]')).toHaveLength(1); + }); + + it('renders rules tab', async () => { + const KibanaContext = createKibanaContextProviderMock(); + const wrapper = mount( + + + + + + ); + + await act(async () => { + await wait(); + + expect(wrapper.exists('[data-test-subj="monitoring-table"]')).toBeFalsy(); + expect(wrapper.exists('[data-test-subj="rules-table"]')).toBeTruthy(); + }); + }); + + it('renders monitoring tab when monitoring tab clicked', async () => { + const KibanaContext = createKibanaContextProviderMock(); + + const wrapper = mount( + + + + + + ); + const monitoringTab = wrapper.find('[data-test-subj="allRulesTableTab-monitoring"] button'); + monitoringTab.simulate('click'); + + await act(async () => { + wrapper.update(); + await wait(); + + expect(wrapper.exists('[data-test-subj="monitoring-table"]')).toBeTruthy(); + expect(wrapper.exists('[data-test-subj="rules-table"]')).toBeFalsy(); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/index.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/index.tsx new file mode 100644 index 00000000000000..c1fd24e24a38bb --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/index.tsx @@ -0,0 +1,423 @@ +/* + * 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 { + EuiBasicTable, + EuiContextMenuPanel, + EuiLoadingContent, + EuiSpacer, + EuiTab, + EuiTabs, +} from '@elastic/eui'; +import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'; +import { useHistory } from 'react-router-dom'; +import uuid from 'uuid'; + +import { + useRules, + useRulesStatuses, + CreatePreBuiltRules, + FilterOptions, + Rule, + PaginationOptions, + exportRules, +} from '../../../../../alerts/containers/detection_engine/rules'; +import { HeaderSection } from '../../../../../common/components/header_section'; +import { + UtilityBar, + UtilityBarAction, + UtilityBarGroup, + UtilityBarSection, + UtilityBarText, +} from '../../../../../common/components/utility_bar'; +import { useStateToaster } from '../../../../../common/components/toasters'; +import { Loader } from '../../../../../common/components/loader'; +import { Panel } from '../../../../../common/components/panel'; +import { PrePackagedRulesPrompt } from '../../../../components/rules/pre_packaged_rules/load_empty_prompt'; +import { GenericDownloader } from '../../../../../common/components/generic_downloader'; +import { AllRulesTables, SortingType } from '../../../../components/rules/all_rules_tables'; +import { getPrePackagedRuleStatus } from '../helpers'; +import * as i18n from '../translations'; +import { EuiBasicTableOnChange } from '../types'; +import { getBatchItems } from './batch_actions'; +import { getColumns, getMonitoringColumns } from './columns'; +import { showRulesTable } from './helpers'; +import { allRulesReducer, State } from './reducer'; +import { RulesTableFilters } from './rules_table_filters/rules_table_filters'; +import { useMlCapabilities } from '../../../../../common/components/ml_popover/hooks/use_ml_capabilities'; +import { hasMlAdminPermissions } from '../../../../../../common/machine_learning/has_ml_admin_permissions'; + +const SORT_FIELD = 'enabled'; +const initialState: State = { + exportRuleIds: [], + filterOptions: { + filter: '', + sortField: SORT_FIELD, + sortOrder: 'desc', + }, + loadingRuleIds: [], + loadingRulesAction: null, + pagination: { + page: 1, + perPage: 20, + total: 0, + }, + rules: [], + selectedRuleIds: [], +}; + +interface AllRulesProps { + createPrePackagedRules: CreatePreBuiltRules | null; + hasNoPermissions: boolean; + loading: boolean; + loadingCreatePrePackagedRules: boolean; + refetchPrePackagedRulesStatus: () => void; + rulesCustomInstalled: number | null; + rulesInstalled: number | null; + rulesNotInstalled: number | null; + rulesNotUpdated: number | null; + setRefreshRulesData: (refreshRule: (refreshPrePackagedRule?: boolean) => void) => void; +} + +export enum AllRulesTabs { + rules = 'rules', + monitoring = 'monitoring', +} + +const allRulesTabs = [ + { + id: AllRulesTabs.rules, + name: i18n.RULES_TAB, + disabled: false, + }, + { + id: AllRulesTabs.monitoring, + name: i18n.MONITORING_TAB, + disabled: false, + }, +]; + +/** + * Table Component for displaying all Rules for a given cluster. Provides the ability to filter + * by name, sort by enabled, and perform the following actions: + * * Enable/Disable + * * Duplicate + * * Delete + * * Import/Export + */ +export const AllRules = React.memo( + ({ + createPrePackagedRules, + hasNoPermissions, + loading, + loadingCreatePrePackagedRules, + refetchPrePackagedRulesStatus, + rulesCustomInstalled, + rulesInstalled, + rulesNotInstalled, + rulesNotUpdated, + setRefreshRulesData, + }) => { + const [initLoading, setInitLoading] = useState(true); + const tableRef = useRef(); + const [ + { + exportRuleIds, + filterOptions, + loadingRuleIds, + loadingRulesAction, + pagination, + rules, + selectedRuleIds, + }, + dispatch, + ] = useReducer(allRulesReducer(tableRef), initialState); + const { loading: isLoadingRulesStatuses, rulesStatuses } = useRulesStatuses(rules); + const history = useHistory(); + const [, dispatchToaster] = useStateToaster(); + const mlCapabilities = useMlCapabilities(); + const [allRulesTab, setAllRulesTab] = useState(AllRulesTabs.rules); + + // TODO: Refactor license check + hasMlAdminPermissions to common check + const hasMlPermissions = + mlCapabilities.isPlatinumOrTrialLicense && hasMlAdminPermissions(mlCapabilities); + + const setRules = useCallback((newRules: Rule[], newPagination: Partial) => { + dispatch({ + type: 'setRules', + rules: newRules, + pagination: newPagination, + }); + }, []); + + const [isLoadingRules, , reFetchRulesData] = useRules({ + pagination, + filterOptions, + refetchPrePackagedRulesStatus, + dispatchRulesInReducer: setRules, + }); + + const sorting = useMemo( + (): SortingType => ({ sort: { field: 'enabled', direction: filterOptions.sortOrder } }), + [filterOptions.sortOrder] + ); + + const prePackagedRuleStatus = getPrePackagedRuleStatus( + rulesInstalled, + rulesNotInstalled, + rulesNotUpdated + ); + + const getBatchItemsPopoverContent = useCallback( + (closePopover: () => void) => ( + + ), + [ + dispatch, + dispatchToaster, + hasMlPermissions, + loadingRuleIds, + reFetchRulesData, + rules, + selectedRuleIds, + ] + ); + + const paginationMemo = useMemo( + () => ({ + pageIndex: pagination.page - 1, + pageSize: pagination.perPage, + totalItemCount: pagination.total, + pageSizeOptions: [5, 10, 20, 50, 100, 200, 300], + }), + [pagination] + ); + + const tableOnChangeCallback = useCallback( + ({ page, sort }: EuiBasicTableOnChange) => { + dispatch({ + type: 'updateFilterOptions', + filterOptions: { + sortField: SORT_FIELD, // Only enabled is supported for sorting currently + sortOrder: sort?.direction ?? 'desc', + }, + pagination: { page: page.index + 1, perPage: page.size }, + }); + }, + [dispatch] + ); + + const rulesColumns = useMemo(() => { + return getColumns({ + dispatch, + dispatchToaster, + history, + hasMlPermissions, + hasNoPermissions, + loadingRuleIds: + loadingRulesAction != null && + (loadingRulesAction === 'enable' || loadingRulesAction === 'disable') + ? loadingRuleIds + : [], + reFetchRules: reFetchRulesData, + }); + }, [ + dispatch, + dispatchToaster, + hasMlPermissions, + history, + loadingRuleIds, + loadingRulesAction, + reFetchRulesData, + ]); + + const monitoringColumns = useMemo(() => getMonitoringColumns(), []); + + useEffect(() => { + if (reFetchRulesData != null) { + setRefreshRulesData(reFetchRulesData); + } + }, [reFetchRulesData, setRefreshRulesData]); + + useEffect(() => { + if (initLoading && !loading && !isLoadingRules && !isLoadingRulesStatuses) { + setInitLoading(false); + } + }, [initLoading, loading, isLoadingRules, isLoadingRulesStatuses]); + + const handleCreatePrePackagedRules = useCallback(async () => { + if (createPrePackagedRules != null && reFetchRulesData != null) { + await createPrePackagedRules(); + reFetchRulesData(true); + } + }, [createPrePackagedRules, reFetchRulesData]); + + const euiBasicTableSelectionProps = useMemo( + () => ({ + selectable: (item: Rule) => !loadingRuleIds.includes(item.id), + onSelectionChange: (selected: Rule[]) => + dispatch({ type: 'selectedRuleIds', ids: selected.map(r => r.id) }), + }), + [loadingRuleIds] + ); + + const onFilterChangedCallback = useCallback((newFilterOptions: Partial) => { + dispatch({ + type: 'updateFilterOptions', + filterOptions: { + ...newFilterOptions, + }, + pagination: { page: 1 }, + }); + }, []); + + const isLoadingAnActionOnRule = useMemo(() => { + if ( + loadingRuleIds.length > 0 && + (loadingRulesAction === 'disable' || loadingRulesAction === 'enable') + ) { + return false; + } else if (loadingRuleIds.length > 0) { + return true; + } + return false; + }, [loadingRuleIds, loadingRulesAction]); + + const tabs = useMemo( + () => ( + + {allRulesTabs.map(tab => ( + setAllRulesTab(tab.id)} + isSelected={tab.id === allRulesTab} + disabled={tab.disabled} + key={tab.id} + > + {tab.name} + + ))} + + ), + [allRulesTabs, allRulesTab, setAllRulesTab] + ); + + return ( + <> + { + dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); + dispatchToaster({ + type: 'addToaster', + toast: { + id: uuid.v4(), + title: i18n.SUCCESSFULLY_EXPORTED_RULES(exportCount), + color: 'success', + iconType: 'check', + }, + }); + }} + exportSelectedData={exportRules} + /> + + {tabs} + + + + <> + + + + + {(loading || isLoadingRules || isLoadingAnActionOnRule || isLoadingRulesStatuses) && + !initLoading && ( + + )} + {rulesCustomInstalled != null && + rulesCustomInstalled === 0 && + prePackagedRuleStatus === 'ruleNotInstalled' && + !initLoading && ( + + )} + {initLoading && ( + + )} + {showRulesTable({ rulesCustomInstalled, rulesInstalled }) && !initLoading && ( + <> + + + + + {i18n.SHOWING_RULES(pagination.total ?? 0)} + + + + + {i18n.SELECTED_RULES(selectedRuleIds.length)} + {!hasNoPermissions && ( + + {i18n.BATCH_ACTIONS} + + )} + reFetchRulesData(true)} + > + {i18n.REFRESH} + + + + + + + )} + + + + ); + } +); + +AllRules.displayName = 'AllRules'; diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/reducer.ts b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/reducer.ts new file mode 100644 index 00000000000000..72559d84eeab42 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/reducer.ts @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiBasicTable } from '@elastic/eui'; +import { + FilterOptions, + PaginationOptions, + Rule, +} from '../../../../../alerts/containers/detection_engine/rules'; + +type LoadingRuleAction = 'duplicate' | 'enable' | 'disable' | 'export' | 'delete' | null; +export interface State { + exportRuleIds: string[]; + filterOptions: FilterOptions; + loadingRuleIds: string[]; + loadingRulesAction: LoadingRuleAction; + pagination: PaginationOptions; + rules: Rule[]; + selectedRuleIds: string[]; +} + +export type Action = + | { type: 'exportRuleIds'; ids: string[] } + | { type: 'loadingRuleIds'; ids: string[]; actionType: LoadingRuleAction } + | { type: 'selectedRuleIds'; ids: string[] } + | { type: 'setRules'; rules: Rule[]; pagination: Partial } + | { type: 'updateRules'; rules: Rule[] } + | { + type: 'updateFilterOptions'; + filterOptions: Partial; + pagination: Partial; + } + | { type: 'failure' }; + +export const allRulesReducer = ( + tableRef: React.MutableRefObject | undefined> +) => (state: State, action: Action): State => { + switch (action.type) { + case 'exportRuleIds': { + return { + ...state, + loadingRuleIds: action.ids, + loadingRulesAction: 'export', + exportRuleIds: action.ids, + }; + } + case 'loadingRuleIds': { + return { + ...state, + loadingRuleIds: action.actionType == null ? [] : [...state.loadingRuleIds, ...action.ids], + loadingRulesAction: action.actionType, + }; + } + case 'selectedRuleIds': { + return { + ...state, + selectedRuleIds: action.ids, + }; + } + case 'setRules': { + if ( + tableRef != null && + tableRef.current != null && + tableRef.current.changeSelection != null + ) { + // for future devs: eui basic table is not giving us a prop to set the value, so + // we are using the ref in setTimeout to reset on the next loop so that we + // do not get a warning telling us we are trying to update during a render + window.setTimeout(() => tableRef?.current?.changeSelection([]), 0); + } + + return { + ...state, + rules: action.rules, + selectedRuleIds: [], + loadingRuleIds: [], + loadingRulesAction: null, + pagination: { + ...state.pagination, + ...action.pagination, + }, + }; + } + case 'updateRules': { + if (state.rules != null) { + const ruleIds = state.rules.map(r => r.id); + const updatedRules = action.rules.reduce((rules, updatedRule) => { + let newRules = rules; + if (ruleIds.includes(updatedRule.id)) { + newRules = newRules.map(r => (updatedRule.id === r.id ? updatedRule : r)); + } else { + newRules = [...newRules, updatedRule]; + } + return newRules; + }, state.rules); + const updatedRuleIds = action.rules.map(r => r.id); + const newLoadingRuleIds = state.loadingRuleIds.filter(id => !updatedRuleIds.includes(id)); + return { + ...state, + rules: updatedRules, + loadingRuleIds: newLoadingRuleIds, + loadingRulesAction: newLoadingRuleIds.length === 0 ? null : state.loadingRulesAction, + }; + } + return state; + } + case 'updateFilterOptions': { + return { + ...state, + filterOptions: { + ...state.filterOptions, + ...action.filterOptions, + }, + pagination: { + ...state.pagination, + ...action.pagination, + }, + }; + } + case 'failure': { + return { + ...state, + rules: [], + }; + } + default: + return state; + } +}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx rename to x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.test.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx similarity index 95% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx rename to x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx index ddb8894c206b56..de4804f37f1bc9 100644 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/rules_table_filters/rules_table_filters.tsx @@ -16,8 +16,8 @@ import { import { isEqual } from 'lodash/fp'; import * as i18n from '../../translations'; -import { FilterOptions } from '../../../../../containers/detection_engine/rules'; -import { useTags } from '../../../../../containers/detection_engine/rules/use_tags'; +import { FilterOptions } from '../../../../../../alerts/containers/detection_engine/rules'; +import { useTags } from '../../../../../../alerts/containers/detection_engine/rules/use_tags'; import { TagsFilterPopover } from './tags_filter_popover'; interface RulesTableFiltersProps { diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.test.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.test.tsx rename to x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.test.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx similarity index 95% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx rename to x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx index 44149a072f5c1e..b453125223c307 100644 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx @@ -16,7 +16,7 @@ import { } from '@elastic/eui'; import styled from 'styled-components'; import * as i18n from '../../translations'; -import { toggleSelectedGroup } from '../../../../../components/ml_popover/jobs_table/filters/toggle_selected_group'; +import { toggleSelectedGroup } from '../../../../../../common/components/ml_popover/jobs_table/filters/toggle_selected_group'; interface TagsFilterPopoverProps { selectedTags: string[]; diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/create/helpers.test.ts b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/create/helpers.test.ts new file mode 100644 index 00000000000000..1894d0ab1a9e7b --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/create/helpers.test.ts @@ -0,0 +1,730 @@ +/* + * 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 { NewRule } from '../../../../../alerts/containers/detection_engine/rules'; +import { + DefineStepRuleJson, + ScheduleStepRuleJson, + AboutStepRuleJson, + ActionsStepRuleJson, + AboutStepRule, + ActionsStepRule, + ScheduleStepRule, + DefineStepRule, +} from '../types'; +import { + getTimeTypeValue, + formatDefineStepData, + formatScheduleStepData, + formatAboutStepData, + formatActionsStepData, + formatRule, + filterRuleFieldsForType, +} from './helpers'; +import { + mockDefineStepRule, + mockQueryBar, + mockScheduleStepRule, + mockAboutStepRule, + mockActionsStepRule, +} from '../all/__mocks__/mock'; + +describe('helpers', () => { + describe('getTimeTypeValue', () => { + test('returns timeObj with value 0 if no time value found', () => { + const result = getTimeTypeValue('m'); + + expect(result).toEqual({ unit: 'm', value: 0 }); + }); + + test('returns timeObj with unit set to empty string if no expected time type found', () => { + const result = getTimeTypeValue('5l'); + + expect(result).toEqual({ unit: '', value: 5 }); + }); + + test('returns timeObj with unit of s and value 5 when time is 5s ', () => { + const result = getTimeTypeValue('5s'); + + expect(result).toEqual({ unit: 's', value: 5 }); + }); + + test('returns timeObj with unit of m and value 5 when time is 5m ', () => { + const result = getTimeTypeValue('5m'); + + expect(result).toEqual({ unit: 'm', value: 5 }); + }); + + test('returns timeObj with unit of h and value 5 when time is 5h ', () => { + const result = getTimeTypeValue('5h'); + + expect(result).toEqual({ unit: 'h', value: 5 }); + }); + + test('returns timeObj with value of 5 when time is float like 5.6m ', () => { + const result = getTimeTypeValue('5m'); + + expect(result).toEqual({ unit: 'm', value: 5 }); + }); + + test('returns timeObj with value of 0 and unit of "" if random string passed in', () => { + const result = getTimeTypeValue('random'); + + expect(result).toEqual({ unit: '', value: 0 }); + }); + }); + + describe('formatDefineStepData', () => { + let mockData: DefineStepRule; + + beforeEach(() => { + mockData = mockDefineStepRule(); + }); + + test('returns formatted object as DefineStepRuleJson', () => { + const result: DefineStepRuleJson = formatDefineStepData(mockData); + const expected = { + language: 'kuery', + filters: mockQueryBar.filters, + query: 'test query', + saved_id: 'test123', + index: ['filebeat-'], + type: 'saved_query', + timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + timeline_title: 'Titled timeline', + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object with no saved_id if no savedId provided', () => { + const mockStepData = { + ...mockData, + queryBar: { + ...mockData.queryBar, + saved_id: '', + }, + }; + const result: DefineStepRuleJson = formatDefineStepData(mockStepData); + const expected = { + language: 'kuery', + filters: mockQueryBar.filters, + query: 'test query', + index: ['filebeat-'], + saved_id: '', + type: 'query', + timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + timeline_title: 'Titled timeline', + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object without timeline_id and timeline_title if timeline.id is null', () => { + const mockStepData = { + ...mockData, + }; + delete mockStepData.timeline.id; + + const result: DefineStepRuleJson = formatDefineStepData(mockStepData); + + const expected = { + language: 'kuery', + filters: mockQueryBar.filters, + query: 'test query', + index: ['filebeat-'], + saved_id: 'test123', + type: 'saved_query', + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object with timeline_id and timeline_title if timeline.id is "', () => { + const mockStepData = { + ...mockData, + timeline: { + ...mockData.timeline, + id: '', + }, + }; + const result: DefineStepRuleJson = formatDefineStepData(mockStepData); + + const expected = { + language: 'kuery', + filters: mockQueryBar.filters, + query: 'test query', + index: ['filebeat-'], + saved_id: 'test123', + type: 'saved_query', + timeline_id: '', + timeline_title: 'Titled timeline', + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object without timeline_id and timeline_title if timeline.title is null', () => { + const mockStepData = { + ...mockData, + timeline: { + ...mockData.timeline, + id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + }, + }; + delete mockStepData.timeline.title; + const result: DefineStepRuleJson = formatDefineStepData(mockStepData); + + const expected = { + language: 'kuery', + filters: mockQueryBar.filters, + query: 'test query', + index: ['filebeat-'], + saved_id: 'test123', + type: 'saved_query', + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object with timeline_id and timeline_title if timeline.title is "', () => { + const mockStepData = { + ...mockData, + timeline: { + ...mockData.timeline, + title: '', + }, + }; + const result: DefineStepRuleJson = formatDefineStepData(mockStepData); + + const expected = { + language: 'kuery', + filters: mockQueryBar.filters, + query: 'test query', + index: ['filebeat-'], + saved_id: 'test123', + type: 'saved_query', + timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + timeline_title: '', + }; + + expect(result).toEqual(expected); + }); + + test('returns ML fields if type is machine_learning', () => { + const mockStepData: DefineStepRule = { + ...mockData, + ruleType: 'machine_learning', + anomalyThreshold: 44, + machineLearningJobId: 'some_jobert_id', + }; + const result: DefineStepRuleJson = formatDefineStepData(mockStepData); + + const expected = { + type: 'machine_learning', + anomaly_threshold: 44, + machine_learning_job_id: 'some_jobert_id', + timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + timeline_title: 'Titled timeline', + }; + + expect(result).toEqual(expected); + }); + }); + + describe('formatScheduleStepData', () => { + let mockData: ScheduleStepRule; + + beforeEach(() => { + mockData = mockScheduleStepRule(); + }); + + test('returns formatted object as ScheduleStepRuleJson', () => { + const result: ScheduleStepRuleJson = formatScheduleStepData(mockData); + const expected = { + from: 'now-660s', + to: 'now', + interval: '5m', + meta: { + from: '6m', + }, + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object with "to" as "now" if "to" not supplied', () => { + const mockStepData = { + ...mockData, + }; + delete mockStepData.to; + const result: ScheduleStepRuleJson = formatScheduleStepData(mockStepData); + const expected = { + from: 'now-660s', + to: 'now', + interval: '5m', + meta: { + from: '6m', + }, + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object with "to" as "now" if "to" random string', () => { + const mockStepData = { + ...mockData, + to: 'random', + }; + const result: ScheduleStepRuleJson = formatScheduleStepData(mockStepData); + const expected = { + from: 'now-660s', + to: 'now', + interval: '5m', + meta: { + from: '6m', + }, + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object if "from" random string', () => { + const mockStepData = { + ...mockData, + from: 'random', + }; + const result: ScheduleStepRuleJson = formatScheduleStepData(mockStepData); + const expected = { + from: 'now-300s', + to: 'now', + interval: '5m', + meta: { + from: 'random', + }, + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object if "interval" random string', () => { + const mockStepData = { + ...mockData, + interval: 'random', + }; + const result: ScheduleStepRuleJson = formatScheduleStepData(mockStepData); + const expected = { + from: 'now-360s', + to: 'now', + interval: 'random', + meta: { + from: '6m', + }, + }; + + expect(result).toEqual(expected); + }); + }); + + describe('formatAboutStepData', () => { + let mockData: AboutStepRule; + + beforeEach(() => { + mockData = mockAboutStepRule(); + }); + + test('returns formatted object as AboutStepRuleJson', () => { + const result: AboutStepRuleJson = formatAboutStepData(mockData); + const expected = { + description: '24/7', + false_positives: ['test'], + name: 'Query with rule-id', + note: '# this is some markdown documentation', + references: ['www.test.co'], + risk_score: 21, + severity: 'low', + tags: ['tag1', 'tag2'], + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: '1234', + name: 'tactic1', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object with empty falsePositive and references filtered out', () => { + const mockStepData = { + ...mockData, + falsePositives: ['', 'test', ''], + references: ['www.test.co', ''], + }; + const result: AboutStepRuleJson = formatAboutStepData(mockStepData); + const expected = { + description: '24/7', + false_positives: ['test'], + name: 'Query with rule-id', + note: '# this is some markdown documentation', + references: ['www.test.co'], + risk_score: 21, + severity: 'low', + tags: ['tag1', 'tag2'], + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: '1234', + name: 'tactic1', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object without note if note is empty string', () => { + const mockStepData = { + ...mockData, + note: '', + }; + const result: AboutStepRuleJson = formatAboutStepData(mockStepData); + const expected = { + description: '24/7', + false_positives: ['test'], + name: 'Query with rule-id', + references: ['www.test.co'], + risk_score: 21, + severity: 'low', + tags: ['tag1', 'tag2'], + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { + id: '1234', + name: 'tactic1', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + }; + + expect(result).toEqual(expected); + }); + + test('returns formatted object with threats filtered out where tactic.name is "none"', () => { + const mockStepData = { + ...mockData, + threat: [ + { + framework: 'mockFramework', + tactic: { + id: '1234', + name: 'tactic1', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + { + framework: 'mockFramework', + tactic: { + id: '1234', + name: 'none', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + }; + const result: AboutStepRuleJson = formatAboutStepData(mockStepData); + const expected = { + description: '24/7', + false_positives: ['test'], + name: 'Query with rule-id', + note: '# this is some markdown documentation', + references: ['www.test.co'], + risk_score: 21, + severity: 'low', + tags: ['tag1', 'tag2'], + threat: [ + { + framework: 'MITRE ATT&CK', + tactic: { id: '1234', name: 'tactic1', reference: 'reference1' }, + technique: [{ id: '456', name: 'technique1', reference: 'technique reference' }], + }, + ], + }; + + expect(result).toEqual(expected); + }); + }); + + describe('formatActionsStepData', () => { + let mockData: ActionsStepRule; + + beforeEach(() => { + mockData = mockActionsStepRule(); + }); + + test('returns formatted object as ActionsStepRuleJson', () => { + const result: ActionsStepRuleJson = formatActionsStepData(mockData); + const expected = { + actions: [], + enabled: false, + meta: { + kibana_siem_app_url: 'http://localhost:5601/app/siem', + }, + throttle: 'no_actions', + }; + + expect(result).toEqual(expected); + }); + + test('returns proper throttle value for no_actions', () => { + const mockStepData = { + ...mockData, + throttle: 'no_actions', + }; + const result: ActionsStepRuleJson = formatActionsStepData(mockStepData); + const expected = { + actions: [], + enabled: false, + meta: { + kibana_siem_app_url: mockStepData.kibanaSiemAppUrl, + }, + throttle: 'no_actions', + }; + + expect(result).toEqual(expected); + }); + + test('returns proper throttle value for rule', () => { + const mockStepData = { + ...mockData, + throttle: 'rule', + actions: [ + { + group: 'default', + id: 'id', + actionTypeId: 'actionTypeId', + params: {}, + }, + ], + }; + const result: ActionsStepRuleJson = formatActionsStepData(mockStepData); + const expected = { + actions: [ + { + group: mockStepData.actions[0].group, + id: mockStepData.actions[0].id, + action_type_id: mockStepData.actions[0].actionTypeId, + params: mockStepData.actions[0].params, + }, + ], + enabled: false, + meta: { + kibana_siem_app_url: mockStepData.kibanaSiemAppUrl, + }, + throttle: 'rule', + }; + + expect(result).toEqual(expected); + }); + + test('returns proper throttle value for interval', () => { + const mockStepData = { + ...mockData, + throttle: '1d', + actions: [ + { + group: 'default', + id: 'id', + actionTypeId: 'actionTypeId', + params: {}, + }, + ], + }; + const result: ActionsStepRuleJson = formatActionsStepData(mockStepData); + const expected = { + actions: [ + { + group: mockStepData.actions[0].group, + id: mockStepData.actions[0].id, + action_type_id: mockStepData.actions[0].actionTypeId, + params: mockStepData.actions[0].params, + }, + ], + enabled: false, + meta: { + kibana_siem_app_url: mockStepData.kibanaSiemAppUrl, + }, + throttle: mockStepData.throttle, + }; + + expect(result).toEqual(expected); + }); + + test('returns actions with action_type_id', () => { + const mockAction = { + group: 'default', + id: '99403909-ca9b-49ba-9d7a-7e5320e68d05', + params: { message: 'ML Rule generated {{state.signals_count}} signals' }, + actionTypeId: '.slack', + }; + + const mockStepData = { + ...mockData, + actions: [mockAction], + }; + const result: ActionsStepRuleJson = formatActionsStepData(mockStepData); + const expected = { + actions: [ + { + group: mockAction.group, + id: mockAction.id, + params: mockAction.params, + action_type_id: mockAction.actionTypeId, + }, + ], + enabled: false, + meta: { + kibana_siem_app_url: mockStepData.kibanaSiemAppUrl, + }, + throttle: 'no_actions', + }; + + expect(result).toEqual(expected); + }); + }); + + describe('formatRule', () => { + let mockAbout: AboutStepRule; + let mockDefine: DefineStepRule; + let mockSchedule: ScheduleStepRule; + let mockActions: ActionsStepRule; + + beforeEach(() => { + mockAbout = mockAboutStepRule(); + mockDefine = mockDefineStepRule(); + mockSchedule = mockScheduleStepRule(); + mockActions = mockActionsStepRule(); + }); + + test('returns NewRule with type of saved_query when saved_id exists', () => { + const result: NewRule = formatRule(mockDefine, mockAbout, mockSchedule, mockActions); + + expect(result.type).toEqual('saved_query'); + }); + + test('returns NewRule with type of query when saved_id does not exist', () => { + const mockDefineStepRuleWithoutSavedId = { + ...mockDefine, + queryBar: { + ...mockDefine.queryBar, + saved_id: '', + }, + }; + const result: NewRule = formatRule( + mockDefineStepRuleWithoutSavedId, + mockAbout, + mockSchedule, + mockActions + ); + + expect(result.type).toEqual('query'); + }); + + test('returns NewRule without id if ruleId does not exist', () => { + const result: NewRule = formatRule(mockDefine, mockAbout, mockSchedule, mockActions); + + expect(result.id).toBeUndefined(); + }); + }); + + describe('filterRuleFieldsForType', () => { + let fields: DefineStepRule; + + beforeEach(() => { + fields = mockDefineStepRule(); + }); + + it('removes query fields if the type is machine learning', () => { + const result = filterRuleFieldsForType(fields, 'machine_learning'); + expect(result).not.toHaveProperty('index'); + expect(result).not.toHaveProperty('queryBar'); + }); + + it('leaves ML fields if the type is machine learning', () => { + const result = filterRuleFieldsForType(fields, 'machine_learning'); + expect(result).toHaveProperty('anomalyThreshold'); + expect(result).toHaveProperty('machineLearningJobId'); + }); + + it('leaves arbitrary fields if the type is machine learning', () => { + const result = filterRuleFieldsForType(fields, 'machine_learning'); + expect(result).toHaveProperty('timeline'); + expect(result).toHaveProperty('ruleType'); + }); + + it('removes ML fields if the type is not machine learning', () => { + const result = filterRuleFieldsForType(fields, 'query'); + expect(result).not.toHaveProperty('anomalyThreshold'); + expect(result).not.toHaveProperty('machineLearningJobId'); + }); + + it('leaves query fields if the type is query', () => { + const result = filterRuleFieldsForType(fields, 'query'); + expect(result).toHaveProperty('index'); + expect(result).toHaveProperty('queryBar'); + }); + + it('leaves arbitrary fields if the type is query', () => { + const result = filterRuleFieldsForType(fields, 'query'); + expect(result).toHaveProperty('timeline'); + expect(result).toHaveProperty('ruleType'); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/create/helpers.ts new file mode 100644 index 00000000000000..7f200ef421c489 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/create/helpers.ts @@ -0,0 +1,174 @@ +/* + * 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 { has, isEmpty } from 'lodash/fp'; +import moment from 'moment'; +import deepmerge from 'deepmerge'; + +import { NOTIFICATION_THROTTLE_NO_ACTIONS } from '../../../../../../common/constants'; +import { transformAlertToRuleAction } from '../../../../../../common/detection_engine/transform_actions'; +import { RuleType } from '../../../../../../common/detection_engine/types'; +import { isMlRule } from '../../../../../../common/machine_learning/helpers'; +import { NewRule } from '../../../../../alerts/containers/detection_engine/rules'; + +import { + AboutStepRule, + DefineStepRule, + ScheduleStepRule, + ActionsStepRule, + DefineStepRuleJson, + ScheduleStepRuleJson, + AboutStepRuleJson, + ActionsStepRuleJson, +} from '../types'; + +export const getTimeTypeValue = (time: string): { unit: string; value: number } => { + const timeObj = { + unit: '', + value: 0, + }; + const filterTimeVal = (time as string).match(/\d+/g); + const filterTimeType = (time as string).match(/[a-zA-Z]+/g); + if (!isEmpty(filterTimeVal) && filterTimeVal != null && !isNaN(Number(filterTimeVal[0]))) { + timeObj.value = Number(filterTimeVal[0]); + } + if ( + !isEmpty(filterTimeType) && + filterTimeType != null && + ['s', 'm', 'h'].includes(filterTimeType[0]) + ) { + timeObj.unit = filterTimeType[0]; + } + return timeObj; +}; + +export interface RuleFields { + anomalyThreshold: unknown; + machineLearningJobId: unknown; + queryBar: unknown; + index: unknown; + ruleType: unknown; +} +type QueryRuleFields = Omit; +type MlRuleFields = Omit; + +const isMlFields = (fields: QueryRuleFields | MlRuleFields): fields is MlRuleFields => + has('anomalyThreshold', fields); + +export const filterRuleFieldsForType = (fields: T, type: RuleType) => { + if (isMlRule(type)) { + const { index, queryBar, ...mlRuleFields } = fields; + return mlRuleFields; + } else { + const { anomalyThreshold, machineLearningJobId, ...queryRuleFields } = fields; + return queryRuleFields; + } +}; + +export const formatDefineStepData = (defineStepData: DefineStepRule): DefineStepRuleJson => { + const ruleFields = filterRuleFieldsForType(defineStepData, defineStepData.ruleType); + const { ruleType, timeline } = ruleFields; + const baseFields = { + type: ruleType, + ...(timeline.id != null && + timeline.title != null && { + timeline_id: timeline.id, + timeline_title: timeline.title, + }), + }; + + const typeFields = isMlFields(ruleFields) + ? { + anomaly_threshold: ruleFields.anomalyThreshold, + machine_learning_job_id: ruleFields.machineLearningJobId, + } + : { + index: ruleFields.index, + filters: ruleFields.queryBar?.filters, + language: ruleFields.queryBar?.query?.language, + query: ruleFields.queryBar?.query?.query as string, + saved_id: ruleFields.queryBar?.saved_id, + ...(ruleType === 'query' && + ruleFields.queryBar?.saved_id && { type: 'saved_query' as RuleType }), + }; + + return { + ...baseFields, + ...typeFields, + }; +}; + +export const formatScheduleStepData = (scheduleData: ScheduleStepRule): ScheduleStepRuleJson => { + const { isNew, ...formatScheduleData } = scheduleData; + if (!isEmpty(formatScheduleData.interval) && !isEmpty(formatScheduleData.from)) { + const { unit: intervalUnit, value: intervalValue } = getTimeTypeValue( + formatScheduleData.interval + ); + const { unit: fromUnit, value: fromValue } = getTimeTypeValue(formatScheduleData.from); + const duration = moment.duration(intervalValue, intervalUnit as 's' | 'm' | 'h'); + duration.add(fromValue, fromUnit as 's' | 'm' | 'h'); + formatScheduleData.from = `now-${duration.asSeconds()}s`; + formatScheduleData.to = 'now'; + } + return { + ...formatScheduleData, + meta: { + from: scheduleData.from, + }, + }; +}; + +export const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRuleJson => { + const { falsePositives, references, riskScore, threat, isNew, note, ...rest } = aboutStepData; + return { + false_positives: falsePositives.filter(item => !isEmpty(item)), + references: references.filter(item => !isEmpty(item)), + risk_score: riskScore, + threat: threat + .filter(singleThreat => singleThreat.tactic.name !== 'none') + .map(singleThreat => ({ + ...singleThreat, + framework: 'MITRE ATT&CK', + technique: singleThreat.technique.map(technique => { + const { id, name, reference } = technique; + return { id, name, reference }; + }), + })), + ...(!isEmpty(note) ? { note } : {}), + ...rest, + }; +}; + +export const formatActionsStepData = (actionsStepData: ActionsStepRule): ActionsStepRuleJson => { + const { + actions = [], + enabled, + kibanaSiemAppUrl, + throttle = NOTIFICATION_THROTTLE_NO_ACTIONS, + } = actionsStepData; + + return { + actions: actions.map(transformAlertToRuleAction), + enabled, + throttle: actions.length ? throttle : NOTIFICATION_THROTTLE_NO_ACTIONS, + meta: { + kibana_siem_app_url: kibanaSiemAppUrl, + }, + }; +}; + +export const formatRule = ( + defineStepData: DefineStepRule, + aboutStepData: AboutStepRule, + scheduleData: ScheduleStepRule, + actionsData: ActionsStepRule +): NewRule => + deepmerge.all([ + formatDefineStepData(defineStepData), + formatAboutStepData(aboutStepData), + formatScheduleStepData(scheduleData), + formatActionsStepData(actionsData), + ]) as NewRule; diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/create/index.test.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/create/index.test.tsx new file mode 100644 index 00000000000000..7749e38578e906 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/create/index.test.tsx @@ -0,0 +1,23 @@ +/* + * 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 { shallow } from 'enzyme'; + +import { TestProviders } from '../../../../../common/mock'; +import { CreateRulePage } from './index'; +import { useUserInfo } from '../../../../components/user_info'; + +jest.mock('../../../../components/user_info'); + +describe('CreateRulePage', () => { + it('renders correctly', () => { + (useUserInfo as jest.Mock).mockReturnValue({}); + const wrapper = shallow(, { wrappingComponent: TestProviders }); + + expect(wrapper.find('[title="Create new rule"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/create/index.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/create/index.tsx new file mode 100644 index 00000000000000..5cf7f9e5b15a30 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/create/index.tsx @@ -0,0 +1,428 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButtonEmpty, EuiAccordion, EuiHorizontalRule, EuiPanel, EuiSpacer } from '@elastic/eui'; +import React, { useCallback, useRef, useState, useMemo } from 'react'; +import { Redirect } from 'react-router-dom'; +import styled, { StyledComponent } from 'styled-components'; + +import { usePersistRule } from '../../../../../alerts/containers/detection_engine/rules'; + +import { DETECTION_ENGINE_PAGE_NAME } from '../../../../../common/components/link_to/redirect_to_detection_engine'; +import { WrapperPage } from '../../../../../common/components/wrapper_page'; +import { displaySuccessToast, useStateToaster } from '../../../../../common/components/toasters'; +import { SpyRoute } from '../../../../../common/utils/route/spy_routes'; +import { useUserInfo } from '../../../../components/user_info'; +import { AccordionTitle } from '../../../../components/rules/accordion_title'; +import { FormData, FormHook } from '../../../../../shared_imports'; +import { StepAboutRule } from '../../../../components/rules/step_about_rule'; +import { StepDefineRule } from '../../../../components/rules/step_define_rule'; +import { StepScheduleRule } from '../../../../components/rules/step_schedule_rule'; +import { StepRuleActions } from '../../../../components/rules/step_rule_actions'; +import { DetectionEngineHeaderPage } from '../../../../components/detection_engine_header_page'; +import * as RuleI18n from '../translations'; +import { redirectToDetections, getActionMessageParams, userHasNoPermissions } from '../helpers'; +import { + AboutStepRule, + DefineStepRule, + RuleStep, + RuleStepData, + ScheduleStepRule, + ActionsStepRule, +} from '../types'; +import { formatRule } from './helpers'; +import * as i18n from './translations'; + +const stepsRuleOrder = [ + RuleStep.defineRule, + RuleStep.aboutRule, + RuleStep.scheduleRule, + RuleStep.ruleActions, +]; + +const MyEuiPanel = styled(EuiPanel)<{ + zindex?: number; +}>` + position: relative; + z-index: ${props => props.zindex}; /* ugly fix to allow searchBar to overflow the EuiPanel */ + + > .euiAccordion > .euiAccordion__triggerWrapper { + .euiAccordion__button { + cursor: default !important; + &:hover { + text-decoration: none !important; + } + } + + .euiAccordion__iconWrapper { + display: none; + } + } +`; + +MyEuiPanel.displayName = 'MyEuiPanel'; + +const StepDefineRuleAccordion: StyledComponent< + typeof EuiAccordion, + any, // eslint-disable-line + { ref: React.MutableRefObject }, + never +> = styled(EuiAccordion)` + .euiAccordion__childWrapper { + overflow: visible; + } +`; + +StepDefineRuleAccordion.displayName = 'StepDefineRuleAccordion'; + +const CreateRulePageComponent: React.FC = () => { + const { + loading, + isSignalIndexExists, + isAuthenticated, + hasEncryptionKey, + canUserCRUD, + } = useUserInfo(); + const [, dispatchToaster] = useStateToaster(); + const [openAccordionId, setOpenAccordionId] = useState(RuleStep.defineRule); + const defineRuleRef = useRef(null); + const aboutRuleRef = useRef(null); + const scheduleRuleRef = useRef(null); + const ruleActionsRef = useRef(null); + const stepsForm = useRef | null>>({ + [RuleStep.defineRule]: null, + [RuleStep.aboutRule]: null, + [RuleStep.scheduleRule]: null, + [RuleStep.ruleActions]: null, + }); + const stepsData = useRef>({ + [RuleStep.defineRule]: { isValid: false, data: {} }, + [RuleStep.aboutRule]: { isValid: false, data: {} }, + [RuleStep.scheduleRule]: { isValid: false, data: {} }, + [RuleStep.ruleActions]: { isValid: false, data: {} }, + }); + const [isStepRuleInReadOnlyView, setIsStepRuleInEditView] = useState>({ + [RuleStep.defineRule]: false, + [RuleStep.aboutRule]: false, + [RuleStep.scheduleRule]: false, + [RuleStep.ruleActions]: false, + }); + const [{ isLoading, isSaved }, setRule] = usePersistRule(); + const actionMessageParams = useMemo( + () => + getActionMessageParams((stepsData.current['define-rule'].data as DefineStepRule).ruleType), + [stepsData.current['define-rule'].data] + ); + + const setStepData = useCallback( + (step: RuleStep, data: unknown, isValid: boolean) => { + stepsData.current[step] = { ...stepsData.current[step], data, isValid }; + if (isValid) { + const stepRuleIdx = stepsRuleOrder.findIndex(item => step === item); + if ([0, 1, 2].includes(stepRuleIdx)) { + if (isStepRuleInReadOnlyView[stepsRuleOrder[stepRuleIdx + 1]]) { + setOpenAccordionId(stepsRuleOrder[stepRuleIdx + 1]); + setIsStepRuleInEditView({ + ...isStepRuleInReadOnlyView, + [step]: true, + [stepsRuleOrder[stepRuleIdx + 1]]: false, + }); + } else if (openAccordionId !== stepsRuleOrder[stepRuleIdx + 1]) { + setIsStepRuleInEditView({ + ...isStepRuleInReadOnlyView, + [step]: true, + }); + openCloseAccordion(stepsRuleOrder[stepRuleIdx + 1]); + setOpenAccordionId(stepsRuleOrder[stepRuleIdx + 1]); + } + } else if ( + stepRuleIdx === 3 && + stepsData.current[RuleStep.defineRule].isValid && + stepsData.current[RuleStep.aboutRule].isValid && + stepsData.current[RuleStep.scheduleRule].isValid + ) { + setRule( + formatRule( + stepsData.current[RuleStep.defineRule].data as DefineStepRule, + stepsData.current[RuleStep.aboutRule].data as AboutStepRule, + stepsData.current[RuleStep.scheduleRule].data as ScheduleStepRule, + stepsData.current[RuleStep.ruleActions].data as ActionsStepRule + ) + ); + } + } + }, + [isStepRuleInReadOnlyView, openAccordionId, stepsData.current, setRule] + ); + + const setStepsForm = useCallback((step: RuleStep, form: FormHook) => { + stepsForm.current[step] = form; + }, []); + + const getAccordionType = useCallback( + (accordionId: RuleStep) => { + if (accordionId === openAccordionId) { + return 'active'; + } else if (stepsData.current[accordionId].isValid) { + return 'valid'; + } + return 'passive'; + }, + [openAccordionId, stepsData.current] + ); + + const defineRuleButton = ( + + ); + + const aboutRuleButton = ( + + ); + + const scheduleRuleButton = ( + + ); + + const ruleActionsButton = ( + + ); + + const openCloseAccordion = (accordionId: RuleStep | null) => { + if (accordionId != null) { + if (accordionId === RuleStep.defineRule && defineRuleRef.current != null) { + defineRuleRef.current.onToggle(); + } else if (accordionId === RuleStep.aboutRule && aboutRuleRef.current != null) { + aboutRuleRef.current.onToggle(); + } else if (accordionId === RuleStep.scheduleRule && scheduleRuleRef.current != null) { + scheduleRuleRef.current.onToggle(); + } else if (accordionId === RuleStep.ruleActions && ruleActionsRef.current != null) { + ruleActionsRef.current.onToggle(); + } + } + }; + + // eslint-disable-next-line react-hooks/rules-of-hooks + const manageAccordions = useCallback( + (id: RuleStep, isOpen: boolean) => { + const activeRuleIdx = stepsRuleOrder.findIndex(step => step === openAccordionId); + const stepRuleIdx = stepsRuleOrder.findIndex(step => step === id); + + if ((id === openAccordionId || stepRuleIdx < activeRuleIdx) && !isOpen) { + openCloseAccordion(id); + } else if (stepRuleIdx >= activeRuleIdx) { + if ( + openAccordionId !== id && + !stepsData.current[openAccordionId].isValid && + !isStepRuleInReadOnlyView[id] && + isOpen + ) { + openCloseAccordion(id); + } + } + }, + [isStepRuleInReadOnlyView, openAccordionId, stepsData] + ); + + // eslint-disable-next-line react-hooks/rules-of-hooks + const manageIsEditable = useCallback( + async (id: RuleStep) => { + const activeForm = await stepsForm.current[openAccordionId]?.submit(); + if (activeForm != null && activeForm?.isValid) { + stepsData.current[openAccordionId] = { + ...stepsData.current[openAccordionId], + data: activeForm.data, + isValid: activeForm.isValid, + }; + setOpenAccordionId(id); + setIsStepRuleInEditView({ + ...isStepRuleInReadOnlyView, + [openAccordionId]: true, + [id]: false, + }); + } + }, + [isStepRuleInReadOnlyView, openAccordionId] + ); + + if (isSaved) { + const ruleName = (stepsData.current[RuleStep.aboutRule].data as AboutStepRule).name; + displaySuccessToast(i18n.SUCCESSFULLY_CREATED_RULES(ruleName), dispatchToaster); + return ; + } + + if (redirectToDetections(isSignalIndexExists, isAuthenticated, hasEncryptionKey)) { + return ; + } else if (userHasNoPermissions(canUserCRUD)) { + return ; + } + + return ( + <> + + + + + {i18n.EDIT_RULE} + + ) + } + > + + + + + + + + {i18n.EDIT_RULE} + + ) + } + > + + + + + + + + {i18n.EDIT_RULE} + + ) + } + > + + + + + + + + {i18n.EDIT_RULE} + + ) + } + > + + + + + + + + + ); +}; + +export const CreateRulePage = React.memo(CreateRulePageComponent); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/create/translations.ts b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/create/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/create/translations.ts rename to x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/create/translations.ts diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/details/failure_history.test.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/details/failure_history.test.tsx similarity index 76% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/details/failure_history.test.tsx rename to x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/details/failure_history.test.tsx index a83ff4c54b0761..fc16bcd96f7665 100644 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/details/failure_history.test.tsx +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/details/failure_history.test.tsx @@ -7,10 +7,10 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { TestProviders } from '../../../../mock'; +import { TestProviders } from '../../../../../common/mock'; import { FailureHistory } from './failure_history'; -import { useRuleStatus } from '../../../../containers/detection_engine/rules'; -jest.mock('../../../../containers/detection_engine/rules'); +import { useRuleStatus } from '../../../../../alerts/containers/detection_engine/rules'; +jest.mock('../../../../../alerts/containers/detection_engine/rules'); describe('FailureHistory', () => { beforeAll(() => { diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/details/failure_history.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/details/failure_history.tsx similarity index 87% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/details/failure_history.tsx rename to x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/details/failure_history.tsx index f660c1763d5e01..f03f320c514184 100644 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/details/failure_history.tsx +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/details/failure_history.tsx @@ -15,10 +15,13 @@ import { } from '@elastic/eui'; import React, { memo } from 'react'; -import { useRuleStatus, RuleInfoStatus } from '../../../../containers/detection_engine/rules'; -import { HeaderSection } from '../../../../components/header_section'; +import { + useRuleStatus, + RuleInfoStatus, +} from '../../../../../alerts/containers/detection_engine/rules'; +import { HeaderSection } from '../../../../../common/components/header_section'; import * as i18n from './translations'; -import { FormattedDate } from '../../../../components/formatted_date'; +import { FormattedDate } from '../../../../../common/components/formatted_date'; interface FailureHistoryProps { id?: string | null; diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/details/index.test.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/details/index.test.tsx new file mode 100644 index 00000000000000..d755f972f29501 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/details/index.test.tsx @@ -0,0 +1,47 @@ +/* + * 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 { shallow } from 'enzyme'; + +import '../../../../../common/mock/match_media'; +import { TestProviders } from '../../../../../common/mock'; +import { RuleDetailsPageComponent } from './index'; +import { setAbsoluteRangeDatePicker } from '../../../../../common/store/inputs/actions'; +import { useUserInfo } from '../../../../components/user_info'; +import { useParams } from 'react-router-dom'; + +jest.mock('../../../../components/user_info'); +jest.mock('react-router-dom', () => { + const originalModule = jest.requireActual('react-router-dom'); + + return { + ...originalModule, + useParams: jest.fn(), + }; +}); + +describe('RuleDetailsPageComponent', () => { + beforeAll(() => { + (useUserInfo as jest.Mock).mockReturnValue({}); + (useParams as jest.Mock).mockReturnValue({}); + }); + + it('renders correctly', () => { + const wrapper = shallow( + , + { + wrappingComponent: TestProviders, + } + ); + + expect(wrapper.find('WithSource')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/details/index.tsx new file mode 100644 index 00000000000000..60491387c492d7 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/details/index.tsx @@ -0,0 +1,429 @@ +/* + * 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. + */ + +/* eslint-disable react-hooks/rules-of-hooks */ + +import { + EuiButton, + EuiLoadingSpinner, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiTab, + EuiTabs, + EuiToolTip, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { FC, memo, useCallback, useMemo, useState } from 'react'; +import { Redirect, useParams } from 'react-router-dom'; +import { StickyContainer } from 'react-sticky'; +import { connect, ConnectedProps } from 'react-redux'; + +import { UpdateDateRange } from '../../../../../common/components/charts/common'; +import { FiltersGlobal } from '../../../../../common/components/filters_global'; +import { FormattedDate } from '../../../../../common/components/formatted_date'; +import { + getEditRuleUrl, + getRulesUrl, + DETECTION_ENGINE_PAGE_NAME, +} from '../../../../../common/components/link_to/redirect_to_detection_engine'; +import { SiemSearchBar } from '../../../../../common/components/search_bar'; +import { WrapperPage } from '../../../../../common/components/wrapper_page'; +import { useRule } from '../../../../../alerts/containers/detection_engine/rules'; + +import { + indicesExistOrDataTemporarilyUnavailable, + WithSource, +} from '../../../../../common/containers/source'; +import { SpyRoute } from '../../../../../common/utils/route/spy_routes'; + +import { StepAboutRuleToggleDetails } from '../../../../components/rules/step_about_rule_details'; +import { DetectionEngineHeaderPage } from '../../../../components/detection_engine_header_page'; +import { SignalsHistogramPanel } from '../../../../components/signals_histogram_panel'; +import { SignalsTable } from '../../../../components/signals'; +import { useUserInfo } from '../../../../components/user_info'; +import { DetectionEngineEmptyPage } from '../../detection_engine_empty_page'; +import { useSignalInfo } from '../../../../components/signals_info'; +import { StepDefineRule } from '../../../../components/rules/step_define_rule'; +import { StepScheduleRule } from '../../../../components/rules/step_schedule_rule'; +import { buildSignalsRuleIdFilter } from '../../../../components/signals/default_config'; +import { NoWriteSignalsCallOut } from '../../../../components/no_write_signals_callout'; +import * as detectionI18n from '../../translations'; +import { ReadOnlyCallOut } from '../../../../components/rules/read_only_callout'; +import { RuleSwitch } from '../../../../components/rules/rule_switch'; +import { StepPanel } from '../../../../components/rules/step_panel'; +import { getStepsData, redirectToDetections, userHasNoPermissions } from '../helpers'; +import * as ruleI18n from '../translations'; +import * as i18n from './translations'; +import { GlobalTime } from '../../../../../common/containers/global_time'; +import { signalsHistogramOptions } from '../../../../components/signals_histogram_panel/config'; +import { inputsSelectors } from '../../../../../common/store/inputs'; +import { State } from '../../../../../common/store'; +import { InputsRange } from '../../../../../common/store/inputs/model'; +import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../../../../common/store/inputs/actions'; +import { RuleActionsOverflow } from '../../../../components/rules/rule_actions_overflow'; +import { RuleStatusFailedCallOut } from './status_failed_callout'; +import { FailureHistory } from './failure_history'; +import { RuleStatus } from '../../../../components/rules//rule_status'; +import { useMlCapabilities } from '../../../../../common/components/ml_popover/hooks/use_ml_capabilities'; +import { hasMlAdminPermissions } from '../../../../../../common/machine_learning/has_ml_admin_permissions'; + +enum RuleDetailTabs { + signals = 'signals', + failures = 'failures', +} + +const ruleDetailTabs = [ + { + id: RuleDetailTabs.signals, + name: detectionI18n.SIGNAL, + disabled: false, + }, + { + id: RuleDetailTabs.failures, + name: i18n.FAILURE_HISTORY_TAB, + disabled: false, + }, +]; + +export const RuleDetailsPageComponent: FC = ({ + filters, + query, + setAbsoluteRangeDatePicker, +}) => { + const { + loading, + isSignalIndexExists, + isAuthenticated, + hasEncryptionKey, + canUserCRUD, + hasIndexWrite, + signalIndexName, + } = useUserInfo(); + const { detailName: ruleId } = useParams(); + const [isLoading, rule] = useRule(ruleId); + // This is used to re-trigger api rule status when user de/activate rule + const [ruleEnabled, setRuleEnabled] = useState(null); + const [ruleDetailTab, setRuleDetailTab] = useState(RuleDetailTabs.signals); + const { aboutRuleData, modifiedAboutRuleDetailsData, defineRuleData, scheduleRuleData } = + rule != null + ? getStepsData({ rule, detailsView: true }) + : { + aboutRuleData: null, + modifiedAboutRuleDetailsData: null, + defineRuleData: null, + scheduleRuleData: null, + }; + const [lastSignals] = useSignalInfo({ ruleId }); + const mlCapabilities = useMlCapabilities(); + + // TODO: Refactor license check + hasMlAdminPermissions to common check + const hasMlPermissions = + mlCapabilities.isPlatinumOrTrialLicense && hasMlAdminPermissions(mlCapabilities); + + const title = isLoading === true || rule === null ? : rule.name; + const subTitle = useMemo( + () => + isLoading === true || rule === null ? ( + + ) : ( + [ + + ), + }} + />, + rule?.updated_by != null ? ( + + ), + }} + /> + ) : ( + '' + ), + ] + ), + [isLoading, rule] + ); + + const signalDefaultFilters = useMemo( + () => (ruleId != null ? buildSignalsRuleIdFilter(ruleId) : []), + [ruleId] + ); + + const signalMergedFilters = useMemo(() => [...signalDefaultFilters, ...filters], [ + signalDefaultFilters, + filters, + ]); + + const tabs = useMemo( + () => ( + + {ruleDetailTabs.map(tab => ( + setRuleDetailTab(tab.id)} + isSelected={tab.id === ruleDetailTab} + disabled={tab.disabled} + key={tab.id} + > + {tab.name} + + ))} + + ), + [ruleDetailTabs, ruleDetailTab, setRuleDetailTab] + ); + const ruleError = useMemo( + () => + rule?.status === 'failed' && + ruleDetailTab === RuleDetailTabs.signals && + rule?.last_failure_at != null ? ( + + ) : null, + [rule, ruleDetailTab] + ); + + const indexToAdd = useMemo(() => (signalIndexName == null ? [] : [signalIndexName]), [ + signalIndexName, + ]); + + const updateDateRangeCallback = useCallback( + ({ x }) => { + if (!x) { + return; + } + const [min, max] = x; + setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + }, + [setAbsoluteRangeDatePicker] + ); + + const handleOnChangeEnabledRule = useCallback( + (enabled: boolean) => { + if (ruleEnabled == null || enabled !== ruleEnabled) { + setRuleEnabled(enabled); + } + }, + [ruleEnabled, setRuleEnabled] + ); + + if (redirectToDetections(isSignalIndexExists, isAuthenticated, hasEncryptionKey)) { + return ; + } + + return ( + <> + {hasIndexWrite != null && !hasIndexWrite && } + {userHasNoPermissions(canUserCRUD) && } + + {({ indicesExist, indexPattern }) => { + return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( + + {({ to, from, deleteQuery, setQuery }) => ( + + + + + + + + {detectionI18n.LAST_SIGNAL} + {': '} + {lastSignals} + , + ] + : []), + , + ]} + title={title} + > + + + + + + + + + + + + {ruleI18n.EDIT_RULE_SETTINGS} + + + + + + + + + + {ruleError} + + + + + + + + + + + {defineRuleData != null && ( + + )} + + + + + + {scheduleRuleData != null && ( + + )} + + + + + + + {tabs} + + {ruleDetailTab === RuleDetailTabs.signals && ( + <> + + + {ruleId != null && ( + + )} + + )} + {ruleDetailTab === RuleDetailTabs.failures && } + + + )} + + ) : ( + + + + + + ); + }} + + + + + ); +}; + +const makeMapStateToProps = () => { + const getGlobalInputs = inputsSelectors.globalSelector(); + return (state: State) => { + const globalInputs: InputsRange = getGlobalInputs(state); + const { query, filters } = globalInputs; + + return { + query, + filters, + }; + }; +}; + +const mapDispatchToProps = { + setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker, +}; + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const RuleDetailsPage = connector(memo(RuleDetailsPageComponent)); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/details/status_failed_callout.test.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/details/status_failed_callout.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/details/status_failed_callout.test.tsx rename to x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/details/status_failed_callout.test.tsx diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/details/status_failed_callout.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/details/status_failed_callout.tsx similarity index 93% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/details/status_failed_callout.tsx rename to x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/details/status_failed_callout.tsx index d1699a83becafa..5b5b96ace8670c 100644 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/details/status_failed_callout.tsx +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/details/status_failed_callout.tsx @@ -7,7 +7,7 @@ import { EuiCallOut, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React, { memo } from 'react'; -import { FormattedDate } from '../../../../components/formatted_date'; +import { FormattedDate } from '../../../../../common/components/formatted_date'; import * as i18n from './translations'; interface RuleStatusFailedCallOutComponentProps { diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/details/translations.ts b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/details/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/details/translations.ts rename to x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/details/translations.ts diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/edit/index.test.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/edit/index.test.tsx new file mode 100644 index 00000000000000..91bc2ce7bce253 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/edit/index.test.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { TestProviders } from '../../../../../common/mock'; +import { EditRulePage } from './index'; +import { useUserInfo } from '../../../../components/user_info'; +import { useParams } from 'react-router-dom'; + +jest.mock('../../../../components/user_info'); +jest.mock('react-router-dom', () => { + const originalModule = jest.requireActual('react-router-dom'); + + return { + ...originalModule, + useParams: jest.fn(), + }; +}); + +describe('EditRulePage', () => { + it('renders correctly', () => { + (useUserInfo as jest.Mock).mockReturnValue({}); + (useParams as jest.Mock).mockReturnValue({}); + const wrapper = shallow(, { wrappingComponent: TestProviders }); + + expect(wrapper.find('[title="Edit rule settings"]')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/edit/index.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/edit/index.tsx new file mode 100644 index 00000000000000..041f932c412cf4 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/edit/index.tsx @@ -0,0 +1,431 @@ +/* + * 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. + */ + +/* eslint-disable react-hooks/rules-of-hooks */ + +import { + EuiButton, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiTabbedContent, + EuiTabbedContentTab, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { FC, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { Redirect, useParams } from 'react-router-dom'; + +import { useRule, usePersistRule } from '../../../../../alerts/containers/detection_engine/rules'; +import { WrapperPage } from '../../../../../common/components/wrapper_page'; +import { DETECTION_ENGINE_PAGE_NAME } from '../../../../../common/components/link_to/redirect_to_detection_engine'; +import { displaySuccessToast, useStateToaster } from '../../../../../common/components/toasters'; +import { SpyRoute } from '../../../../../common/utils/route/spy_routes'; +import { useUserInfo } from '../../../../components/user_info'; +import { DetectionEngineHeaderPage } from '../../../../components/detection_engine_header_page'; +import { FormHook, FormData } from '../../../../../shared_imports'; +import { StepPanel } from '../../../../components/rules/step_panel'; +import { StepAboutRule } from '../../../../components/rules/step_about_rule'; +import { StepDefineRule } from '../../../../components/rules/step_define_rule'; +import { StepScheduleRule } from '../../../../components/rules/step_schedule_rule'; +import { StepRuleActions } from '../../../../components/rules/step_rule_actions'; +import { formatRule } from '../create/helpers'; +import { + getStepsData, + redirectToDetections, + getActionMessageParams, + userHasNoPermissions, +} from '../helpers'; +import * as ruleI18n from '../translations'; +import { + RuleStep, + DefineStepRule, + AboutStepRule, + ScheduleStepRule, + ActionsStepRule, +} from '../types'; +import * as i18n from './translations'; + +interface StepRuleForm { + isValid: boolean; +} +interface AboutStepRuleForm extends StepRuleForm { + data: AboutStepRule | null; +} +interface DefineStepRuleForm extends StepRuleForm { + data: DefineStepRule | null; +} +interface ScheduleStepRuleForm extends StepRuleForm { + data: ScheduleStepRule | null; +} + +interface ActionsStepRuleForm extends StepRuleForm { + data: ActionsStepRule | null; +} + +const EditRulePageComponent: FC = () => { + const [, dispatchToaster] = useStateToaster(); + const { + loading: initLoading, + isSignalIndexExists, + isAuthenticated, + hasEncryptionKey, + canUserCRUD, + } = useUserInfo(); + const { detailName: ruleId } = useParams(); + const [loading, rule] = useRule(ruleId); + + const [initForm, setInitForm] = useState(false); + const [myAboutRuleForm, setMyAboutRuleForm] = useState({ + data: null, + isValid: false, + }); + const [myDefineRuleForm, setMyDefineRuleForm] = useState({ + data: null, + isValid: false, + }); + const [myScheduleRuleForm, setMyScheduleRuleForm] = useState({ + data: null, + isValid: false, + }); + const [myActionsRuleForm, setMyActionsRuleForm] = useState({ + data: null, + isValid: false, + }); + const [selectedTab, setSelectedTab] = useState(); + const stepsForm = useRef | null>>({ + [RuleStep.defineRule]: null, + [RuleStep.aboutRule]: null, + [RuleStep.scheduleRule]: null, + [RuleStep.ruleActions]: null, + }); + const [{ isLoading, isSaved }, setRule] = usePersistRule(); + const [tabHasError, setTabHasError] = useState([]); + const actionMessageParams = useMemo(() => getActionMessageParams(rule?.type), [rule]); + const setStepsForm = useCallback( + (step: RuleStep, form: FormHook) => { + stepsForm.current[step] = form; + if (initForm && step === (selectedTab?.id as RuleStep) && form.isSubmitted === false) { + setInitForm(false); + form.submit(); + } + }, + [initForm, selectedTab] + ); + const tabs = useMemo( + () => [ + { + id: RuleStep.defineRule, + name: ruleI18n.DEFINITION, + disabled: rule?.immutable, + content: ( + <> + + + {myDefineRuleForm.data != null && ( + + )} + + + + ), + }, + { + id: RuleStep.aboutRule, + name: ruleI18n.ABOUT, + disabled: rule?.immutable, + content: ( + <> + + + {myAboutRuleForm.data != null && ( + + )} + + + + ), + }, + { + id: RuleStep.scheduleRule, + name: ruleI18n.SCHEDULE, + disabled: rule?.immutable, + content: ( + <> + + + {myScheduleRuleForm.data != null && ( + + )} + + + + ), + }, + { + id: RuleStep.ruleActions, + name: ruleI18n.ACTIONS, + content: ( + <> + + + {myActionsRuleForm.data != null && ( + + )} + + + + ), + }, + ], + [ + rule, + loading, + initLoading, + isLoading, + myAboutRuleForm, + myDefineRuleForm, + myScheduleRuleForm, + myActionsRuleForm, + setStepsForm, + stepsForm, + actionMessageParams, + ] + ); + + const onSubmit = useCallback(async () => { + const activeFormId = selectedTab?.id as RuleStep; + const activeForm = await stepsForm.current[activeFormId]?.submit(); + + const invalidForms = [ + RuleStep.aboutRule, + RuleStep.defineRule, + RuleStep.scheduleRule, + RuleStep.ruleActions, + ].reduce((acc, step) => { + if ( + (step === activeFormId && activeForm != null && !activeForm?.isValid) || + (step === RuleStep.aboutRule && !myAboutRuleForm.isValid) || + (step === RuleStep.defineRule && !myDefineRuleForm.isValid) || + (step === RuleStep.scheduleRule && !myScheduleRuleForm.isValid) || + (step === RuleStep.ruleActions && !myActionsRuleForm.isValid) + ) { + return [...acc, step]; + } + return acc; + }, []); + + if (invalidForms.length === 0 && activeForm != null) { + setTabHasError([]); + setRule({ + ...formatRule( + (activeFormId === RuleStep.defineRule + ? activeForm.data + : myDefineRuleForm.data) as DefineStepRule, + (activeFormId === RuleStep.aboutRule + ? activeForm.data + : myAboutRuleForm.data) as AboutStepRule, + (activeFormId === RuleStep.scheduleRule + ? activeForm.data + : myScheduleRuleForm.data) as ScheduleStepRule, + (activeFormId === RuleStep.ruleActions + ? activeForm.data + : myActionsRuleForm.data) as ActionsStepRule + ), + ...(ruleId ? { id: ruleId } : {}), + }); + } else { + setTabHasError(invalidForms); + } + }, [ + stepsForm, + myAboutRuleForm, + myDefineRuleForm, + myScheduleRuleForm, + myActionsRuleForm, + selectedTab, + ruleId, + ]); + + useEffect(() => { + if (rule != null) { + const { aboutRuleData, defineRuleData, scheduleRuleData, ruleActionsData } = getStepsData({ + rule, + }); + setMyAboutRuleForm({ data: aboutRuleData, isValid: true }); + setMyDefineRuleForm({ data: defineRuleData, isValid: true }); + setMyScheduleRuleForm({ data: scheduleRuleData, isValid: true }); + setMyActionsRuleForm({ data: ruleActionsData, isValid: true }); + } + }, [rule]); + + const onTabClick = useCallback( + async (tab: EuiTabbedContentTab) => { + if (selectedTab != null) { + const ruleStep = selectedTab.id as RuleStep; + const respForm = await stepsForm.current[ruleStep]?.submit(); + + if (respForm != null) { + if (ruleStep === RuleStep.aboutRule) { + setMyAboutRuleForm({ + data: respForm.data as AboutStepRule, + isValid: respForm.isValid, + }); + } else if (ruleStep === RuleStep.defineRule) { + setMyDefineRuleForm({ + data: respForm.data as DefineStepRule, + isValid: respForm.isValid, + }); + } else if (ruleStep === RuleStep.scheduleRule) { + setMyScheduleRuleForm({ + data: respForm.data as ScheduleStepRule, + isValid: respForm.isValid, + }); + } else if (ruleStep === RuleStep.ruleActions) { + setMyActionsRuleForm({ + data: respForm.data as ActionsStepRule, + isValid: respForm.isValid, + }); + } + } + } + setInitForm(true); + setSelectedTab(tab); + }, + [selectedTab, stepsForm.current] + ); + + useEffect(() => { + if (rule != null) { + const { aboutRuleData, defineRuleData, scheduleRuleData, ruleActionsData } = getStepsData({ + rule, + }); + setMyAboutRuleForm({ data: aboutRuleData, isValid: true }); + setMyDefineRuleForm({ data: defineRuleData, isValid: true }); + setMyScheduleRuleForm({ data: scheduleRuleData, isValid: true }); + setMyActionsRuleForm({ data: ruleActionsData, isValid: true }); + } + }, [rule]); + + useEffect(() => { + const tabIndex = rule?.immutable ? 3 : 0; + setSelectedTab(tabs[tabIndex]); + }, [rule]); + + if (isSaved) { + displaySuccessToast(i18n.SUCCESSFULLY_SAVED_RULE(rule?.name ?? ''), dispatchToaster); + return ; + } + + if (redirectToDetections(isSignalIndexExists, isAuthenticated, hasEncryptionKey)) { + return ; + } else if (userHasNoPermissions(canUserCRUD)) { + return ; + } + + return ( + <> + + + {tabHasError.length > 0 && ( + + { + if (t === RuleStep.aboutRule) { + return ruleI18n.ABOUT; + } else if (t === RuleStep.defineRule) { + return ruleI18n.DEFINITION; + } else if (t === RuleStep.scheduleRule) { + return ruleI18n.SCHEDULE; + } else if (t === RuleStep.ruleActions) { + return ruleI18n.RULE_ACTIONS; + } + return t; + }) + .join(', '), + }} + /> + + )} + + t.id === selectedTab?.id)} + onTabClick={onTabClick} + tabs={tabs} + /> + + + + + + + {i18n.CANCEL} + + + + + + {i18n.SAVE_CHANGES} + + + + + + + + ); +}; + +export const EditRulePage = memo(EditRulePageComponent); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/edit/translations.ts b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/edit/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/edit/translations.ts rename to x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/edit/translations.ts diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/helpers.test.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/helpers.test.tsx new file mode 100644 index 00000000000000..6c64577b083df0 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/helpers.test.tsx @@ -0,0 +1,378 @@ +/* + * 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 { + GetStepsData, + getDefineStepsData, + getScheduleStepsData, + getStepsData, + getAboutStepsData, + getActionsStepsData, + getHumanizedDuration, + getModifiedAboutDetailsData, + determineDetailsValue, + userHasNoPermissions, +} from './helpers'; +import { mockRuleWithEverything, mockRule } from './all/__mocks__/mock'; +import { esFilters } from '../../../../../../../../src/plugins/data/public'; +import { Rule } from '../../../../alerts/containers/detection_engine/rules'; +import { + AboutStepRule, + AboutStepRuleDetails, + DefineStepRule, + ScheduleStepRule, + ActionsStepRule, +} from './types'; + +describe('rule helpers', () => { + describe('getStepsData', () => { + test('returns object with about, define, schedule and actions step properties formatted', () => { + const { + defineRuleData, + modifiedAboutRuleDetailsData, + aboutRuleData, + scheduleRuleData, + ruleActionsData, + }: GetStepsData = getStepsData({ + rule: mockRuleWithEverything('test-id'), + }); + const defineRuleStepData = { + isNew: false, + ruleType: 'saved_query', + anomalyThreshold: 50, + index: ['auditbeat-*'], + machineLearningJobId: '', + queryBar: { + query: { + query: 'user.name: root or user.name: admin', + language: 'kuery', + }, + filters: [ + { + $state: { + store: esFilters.FilterStateStore.GLOBAL_STATE, + }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + ], + saved_id: 'test123', + }, + timeline: { + id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + title: 'Titled timeline', + }, + }; + const aboutRuleStepData = { + description: '24/7', + falsePositives: ['test'], + isNew: false, + name: 'Query with rule-id', + note: '# this is some markdown documentation', + references: ['www.test.co'], + riskScore: 21, + severity: 'low', + tags: ['tag1', 'tag2'], + threat: [ + { + framework: 'mockFramework', + tactic: { + id: '1234', + name: 'tactic1', + reference: 'reference1', + }, + technique: [ + { + id: '456', + name: 'technique1', + reference: 'technique reference', + }, + ], + }, + ], + }; + const scheduleRuleStepData = { from: '0s', interval: '5m', isNew: false }; + const ruleActionsStepData = { + enabled: true, + throttle: 'no_actions', + isNew: false, + actions: [], + }; + const aboutRuleDataDetailsData = { + note: '# this is some markdown documentation', + description: '24/7', + }; + + expect(defineRuleData).toEqual(defineRuleStepData); + expect(aboutRuleData).toEqual(aboutRuleStepData); + expect(scheduleRuleData).toEqual(scheduleRuleStepData); + expect(ruleActionsData).toEqual(ruleActionsStepData); + expect(modifiedAboutRuleDetailsData).toEqual(aboutRuleDataDetailsData); + }); + }); + + describe('getAboutStepsData', () => { + test('returns name, description, and note as empty string if detailsView is true', () => { + const result: AboutStepRule = getAboutStepsData(mockRuleWithEverything('test-id'), true); + + expect(result.name).toEqual(''); + expect(result.description).toEqual(''); + expect(result.note).toEqual(''); + }); + + test('returns note as empty string if property does not exist on rule', () => { + const mockedRule = mockRuleWithEverything('test-id'); + delete mockedRule.note; + const result: AboutStepRule = getAboutStepsData(mockedRule, false); + + expect(result.note).toEqual(''); + }); + }); + + describe('determineDetailsValue', () => { + test('returns name, description, and note as empty string if detailsView is true', () => { + const result: Pick = determineDetailsValue( + mockRuleWithEverything('test-id'), + true + ); + const expected = { name: '', description: '', note: '' }; + + expect(result).toEqual(expected); + }); + + test('returns name, description, and note values if detailsView is false', () => { + const mockedRule = mockRuleWithEverything('test-id'); + const result: Pick = determineDetailsValue( + mockedRule, + false + ); + const expected = { + name: mockedRule.name, + description: mockedRule.description, + note: mockedRule.note, + }; + + expect(result).toEqual(expected); + }); + + test('returns note as empty string if property does not exist on rule', () => { + const mockedRule = mockRuleWithEverything('test-id'); + delete mockedRule.note; + const result: Pick = determineDetailsValue( + mockedRule, + false + ); + const expected = { name: mockedRule.name, description: mockedRule.description, note: '' }; + + expect(result).toEqual(expected); + }); + }); + + describe('getDefineStepsData', () => { + test('returns with saved_id if value exists on rule', () => { + const result: DefineStepRule = getDefineStepsData(mockRule('test-id')); + const expected = { + isNew: false, + ruleType: 'saved_query', + anomalyThreshold: 50, + machineLearningJobId: '', + index: ['auditbeat-*'], + queryBar: { + query: { + query: '', + language: 'kuery', + }, + filters: [], + saved_id: "Garrett's IP", + }, + timeline: { + id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + title: 'Untitled timeline', + }, + }; + + expect(result).toEqual(expected); + }); + + test('returns with saved_id of undefined if value does not exist on rule', () => { + const mockedRule = { + ...mockRule('test-id'), + }; + delete mockedRule.saved_id; + const result: DefineStepRule = getDefineStepsData(mockedRule); + const expected = { + isNew: false, + ruleType: 'saved_query', + anomalyThreshold: 50, + machineLearningJobId: '', + index: ['auditbeat-*'], + queryBar: { + query: { + query: '', + language: 'kuery', + }, + filters: [], + saved_id: undefined, + }, + timeline: { + id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', + title: 'Untitled timeline', + }, + }; + + expect(result).toEqual(expected); + }); + + test('returns timeline id and title of null if they do not exist on rule', () => { + const mockedRule = mockRuleWithEverything('test-id'); + delete mockedRule.timeline_id; + delete mockedRule.timeline_title; + const result: DefineStepRule = getDefineStepsData(mockedRule); + + expect(result.timeline.id).toBeNull(); + expect(result.timeline.title).toBeNull(); + }); + }); + + describe('getHumanizedDuration', () => { + test('returns from as seconds if from duration is less than a minute', () => { + const result = getHumanizedDuration('now-62s', '1m'); + + expect(result).toEqual('2s'); + }); + + test('returns from as minutes if from duration is less than an hour', () => { + const result = getHumanizedDuration('now-660s', '5m'); + + expect(result).toEqual('6m'); + }); + + test('returns from as hours if from duration is more than 60 minutes', () => { + const result = getHumanizedDuration('now-7400s', '5m'); + + expect(result).toEqual('1h'); + }); + + test('returns from as if from is not parsable as dateMath', () => { + const result = getHumanizedDuration('randomstring', '5m'); + + expect(result).toEqual('NaNh'); + }); + + test('returns from as 5m if interval is not parsable as dateMath', () => { + const result = getHumanizedDuration('now-300s', 'randomstring'); + + expect(result).toEqual('5m'); + }); + }); + + describe('getScheduleStepsData', () => { + test('returns expected ScheduleStep rule object', () => { + const mockedRule = { + ...mockRule('test-id'), + }; + const result: ScheduleStepRule = getScheduleStepsData(mockedRule); + const expected = { + isNew: false, + interval: mockedRule.interval, + from: '0s', + }; + + expect(result).toEqual(expected); + }); + }); + + describe('getActionsStepsData', () => { + test('returns expected ActionsStepRule rule object', () => { + const mockedRule = { + ...mockRule('test-id'), + actions: [ + { + id: 'id', + group: 'group', + params: {}, + action_type_id: 'action_type_id', + }, + ], + }; + const result: ActionsStepRule = getActionsStepsData(mockedRule); + const expected = { + actions: [ + { + id: 'id', + group: 'group', + params: {}, + actionTypeId: 'action_type_id', + }, + ], + enabled: mockedRule.enabled, + isNew: false, + throttle: 'no_actions', + }; + + expect(result).toEqual(expected); + }); + }); + + describe('getModifiedAboutDetailsData', () => { + test('returns object with "note" and "description" being those of passed in rule', () => { + const result: AboutStepRuleDetails = getModifiedAboutDetailsData( + mockRuleWithEverything('test-id') + ); + const aboutRuleDataDetailsData = { + note: '# this is some markdown documentation', + description: '24/7', + }; + + expect(result).toEqual(aboutRuleDataDetailsData); + }); + + test('returns "note" with empty string if "note" does not exist', () => { + const { note, ...mockRuleWithoutNote } = { ...mockRuleWithEverything('test-id') }; + const result: AboutStepRuleDetails = getModifiedAboutDetailsData(mockRuleWithoutNote); + + const aboutRuleDetailsData = { note: '', description: mockRuleWithoutNote.description }; + + expect(result).toEqual(aboutRuleDetailsData); + }); + }); + + describe('userHasNoPermissions', () => { + test("returns false when user's CRUD operations are null", () => { + const result: boolean = userHasNoPermissions(null); + const userHasNoPermissionsExpectedResult = false; + + expect(result).toEqual(userHasNoPermissionsExpectedResult); + }); + + test('returns true when user cannot CRUD', () => { + const result: boolean = userHasNoPermissions(false); + const userHasNoPermissionsExpectedResult = true; + + expect(result).toEqual(userHasNoPermissionsExpectedResult); + }); + + test('returns false when user can CRUD', () => { + const result: boolean = userHasNoPermissions(true); + const userHasNoPermissionsExpectedResult = false; + + expect(result).toEqual(userHasNoPermissionsExpectedResult); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/helpers.tsx new file mode 100644 index 00000000000000..8fbb8babe90c7b --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/helpers.tsx @@ -0,0 +1,273 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import dateMath from '@elastic/datemath'; +import { get } from 'lodash/fp'; +import moment from 'moment'; +import memoizeOne from 'memoize-one'; +import { useLocation } from 'react-router-dom'; + +import { RuleAlertAction, RuleType } from '../../../../../common/detection_engine/types'; +import { isMlRule } from '../../../../../common/machine_learning/helpers'; +import { transformRuleToAlertAction } from '../../../../../common/detection_engine/transform_actions'; +import { Filter } from '../../../../../../../../src/plugins/data/public'; +import { Rule } from '../../../../alerts/containers/detection_engine/rules'; +import { FormData, FormHook, FormSchema } from '../../../../shared_imports'; +import { + AboutStepRule, + AboutStepRuleDetails, + DefineStepRule, + IMitreEnterpriseAttack, + ScheduleStepRule, + ActionsStepRule, +} from './types'; + +export interface GetStepsData { + aboutRuleData: AboutStepRule; + modifiedAboutRuleDetailsData: AboutStepRuleDetails; + defineRuleData: DefineStepRule; + scheduleRuleData: ScheduleStepRule; + ruleActionsData: ActionsStepRule; +} + +export const getStepsData = ({ + rule, + detailsView = false, +}: { + rule: Rule; + detailsView?: boolean; +}): GetStepsData => { + const defineRuleData: DefineStepRule = getDefineStepsData(rule); + const aboutRuleData: AboutStepRule = getAboutStepsData(rule, detailsView); + const modifiedAboutRuleDetailsData: AboutStepRuleDetails = getModifiedAboutDetailsData(rule); + const scheduleRuleData: ScheduleStepRule = getScheduleStepsData(rule); + const ruleActionsData: ActionsStepRule = getActionsStepsData(rule); + + return { + aboutRuleData, + modifiedAboutRuleDetailsData, + defineRuleData, + scheduleRuleData, + ruleActionsData, + }; +}; + +export const getActionsStepsData = ( + rule: Omit & { actions: RuleAlertAction[] } +): ActionsStepRule => { + const { enabled, throttle, meta, actions = [] } = rule; + + return { + actions: actions?.map(transformRuleToAlertAction), + isNew: false, + throttle, + kibanaSiemAppUrl: meta?.kibana_siem_app_url, + enabled, + }; +}; + +export const getDefineStepsData = (rule: Rule): DefineStepRule => ({ + isNew: false, + ruleType: rule.type, + anomalyThreshold: rule.anomaly_threshold ?? 50, + machineLearningJobId: rule.machine_learning_job_id ?? '', + index: rule.index ?? [], + queryBar: { + query: { query: rule.query ?? '', language: rule.language ?? '' }, + filters: (rule.filters ?? []) as Filter[], + saved_id: rule.saved_id, + }, + timeline: { + id: rule.timeline_id ?? null, + title: rule.timeline_title ?? null, + }, +}); + +export const getScheduleStepsData = (rule: Rule): ScheduleStepRule => { + const { interval, from } = rule; + const fromHumanizedValue = getHumanizedDuration(from, interval); + + return { + isNew: false, + interval, + from: fromHumanizedValue, + }; +}; + +export const getHumanizedDuration = (from: string, interval: string): string => { + const fromValue = dateMath.parse(from) ?? moment(); + const intervalValue = dateMath.parse(`now-${interval}`) ?? moment(); + + const fromDuration = moment.duration(intervalValue.diff(fromValue)); + const fromHumanize = `${Math.floor(fromDuration.asHours())}h`; + + if (fromDuration.asSeconds() < 60) { + return `${Math.floor(fromDuration.asSeconds())}s`; + } else if (fromDuration.asMinutes() < 60) { + return `${Math.floor(fromDuration.asMinutes())}m`; + } + + return fromHumanize; +}; + +export const getAboutStepsData = (rule: Rule, detailsView: boolean): AboutStepRule => { + const { name, description, note } = determineDetailsValue(rule, detailsView); + const { + references, + severity, + false_positives: falsePositives, + risk_score: riskScore, + tags, + threat, + } = rule; + + return { + isNew: false, + name, + description, + note: note!, + references, + severity, + tags, + riskScore, + falsePositives, + threat: threat as IMitreEnterpriseAttack[], + }; +}; + +export const determineDetailsValue = ( + rule: Rule, + detailsView: boolean +): Pick => { + const { name, description, note } = rule; + if (detailsView) { + return { name: '', description: '', note: '' }; + } + + return { name, description, note: note ?? '' }; +}; + +export const getModifiedAboutDetailsData = (rule: Rule): AboutStepRuleDetails => ({ + note: rule.note ?? '', + description: rule.description, +}); + +export const useQuery = () => new URLSearchParams(useLocation().search); + +export type PrePackagedRuleStatus = + | 'ruleInstalled' + | 'ruleNotInstalled' + | 'ruleNeedUpdate' + | 'someRuleUninstall' + | 'unknown'; + +export const getPrePackagedRuleStatus = ( + rulesInstalled: number | null, + rulesNotInstalled: number | null, + rulesNotUpdated: number | null +): PrePackagedRuleStatus => { + if ( + rulesNotInstalled != null && + rulesInstalled === 0 && + rulesNotInstalled > 0 && + rulesNotUpdated === 0 + ) { + return 'ruleNotInstalled'; + } else if ( + rulesInstalled != null && + rulesInstalled > 0 && + rulesNotInstalled === 0 && + rulesNotUpdated === 0 + ) { + return 'ruleInstalled'; + } else if ( + rulesInstalled != null && + rulesNotInstalled != null && + rulesInstalled > 0 && + rulesNotInstalled > 0 && + rulesNotUpdated === 0 + ) { + return 'someRuleUninstall'; + } else if ( + rulesInstalled != null && + rulesNotInstalled != null && + rulesNotUpdated != null && + rulesInstalled > 0 && + rulesNotInstalled >= 0 && + rulesNotUpdated > 0 + ) { + return 'ruleNeedUpdate'; + } + return 'unknown'; +}; +export const setFieldValue = ( + form: FormHook, + schema: FormSchema, + defaultValues: unknown +) => + Object.keys(schema).forEach(key => { + const val = get(key, defaultValues); + if (val != null) { + form.setFieldValue(key, val); + } + }); + +export const redirectToDetections = ( + isSignalIndexExists: boolean | null, + isAuthenticated: boolean | null, + hasEncryptionKey: boolean | null +) => + isSignalIndexExists != null && + isAuthenticated != null && + hasEncryptionKey != null && + (!isSignalIndexExists || !isAuthenticated || !hasEncryptionKey); + +export const getActionMessageRuleParams = (ruleType: RuleType): string[] => { + const commonRuleParamsKeys = [ + 'id', + 'name', + 'description', + 'false_positives', + 'rule_id', + 'max_signals', + 'risk_score', + 'output_index', + 'references', + 'severity', + 'timeline_id', + 'timeline_title', + 'threat', + 'type', + 'version', + // 'lists', + ]; + + const ruleParamsKeys = [ + ...commonRuleParamsKeys, + ...(isMlRule(ruleType) + ? ['anomaly_threshold', 'machine_learning_job_id'] + : ['index', 'filters', 'language', 'query', 'saved_id']), + ].sort(); + + return ruleParamsKeys; +}; + +export const getActionMessageParams = memoizeOne((ruleType: RuleType | undefined): string[] => { + if (!ruleType) { + return []; + } + const actionMessageRuleParams = getActionMessageRuleParams(ruleType); + + return [ + 'state.signals_count', + '{context.results_link}', + ...actionMessageRuleParams.map(param => `context.rule.${param}`), + ]; +}); + +// typed as null not undefined as the initial state for this value is null. +export const userHasNoPermissions = (canUserCRUD: boolean | null): boolean => + canUserCRUD != null ? !canUserCRUD : false; diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/index.test.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/index.test.tsx new file mode 100644 index 00000000000000..29f875d113a424 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/index.test.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { RulesPage } from './index'; +import { useUserInfo } from '../../../components/user_info'; +import { usePrePackagedRules } from '../../../../alerts/containers/detection_engine/rules'; + +jest.mock('../../../components/user_info'); +jest.mock('../../../../alerts/containers/detection_engine/rules'); + +describe('RulesPage', () => { + beforeAll(() => { + (useUserInfo as jest.Mock).mockReturnValue({}); + (usePrePackagedRules as jest.Mock).mockReturnValue({}); + }); + it('renders correctly', () => { + const wrapper = shallow(); + + expect(wrapper.find('AllRules')).toHaveLength(1); + }); +}); diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/index.tsx b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/index.tsx new file mode 100644 index 00000000000000..7a9620df3a7b37 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/index.tsx @@ -0,0 +1,196 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { useCallback, useRef, useState } from 'react'; +import { Redirect } from 'react-router-dom'; + +import { + usePrePackagedRules, + importRules, +} from '../../../../alerts/containers/detection_engine/rules'; +import { + DETECTION_ENGINE_PAGE_NAME, + getDetectionEngineUrl, + getCreateRuleUrl, +} from '../../../../common/components/link_to/redirect_to_detection_engine'; +import { DetectionEngineHeaderPage } from '../../../components/detection_engine_header_page'; +import { WrapperPage } from '../../../../common/components/wrapper_page'; +import { SpyRoute } from '../../../../common/utils/route/spy_routes'; + +import { useUserInfo } from '../../../components/user_info'; +import { AllRules } from './all'; +import { ImportDataModal } from '../../../../common/components/import_data_modal'; +import { ReadOnlyCallOut } from '../../../components/rules/read_only_callout'; +import { UpdatePrePackagedRulesCallOut } from '../../../components/rules/pre_packaged_rules/update_callout'; +import { getPrePackagedRuleStatus, redirectToDetections, userHasNoPermissions } from './helpers'; +import * as i18n from './translations'; + +type Func = (refreshPrePackagedRule?: boolean) => void; + +const RulesPageComponent: React.FC = () => { + const [showImportModal, setShowImportModal] = useState(false); + const refreshRulesData = useRef(null); + const { + loading, + isSignalIndexExists, + isAuthenticated, + hasEncryptionKey, + canUserCRUD, + hasIndexWrite, + } = useUserInfo(); + const { + createPrePackagedRules, + loading: prePackagedRuleLoading, + loadingCreatePrePackagedRules, + refetchPrePackagedRulesStatus, + rulesCustomInstalled, + rulesInstalled, + rulesNotInstalled, + rulesNotUpdated, + } = usePrePackagedRules({ + canUserCRUD, + hasIndexWrite, + isSignalIndexExists, + isAuthenticated, + hasEncryptionKey, + }); + const prePackagedRuleStatus = getPrePackagedRuleStatus( + rulesInstalled, + rulesNotInstalled, + rulesNotUpdated + ); + + const handleRefreshRules = useCallback(async () => { + if (refreshRulesData.current != null) { + refreshRulesData.current(true); + } + }, [refreshRulesData]); + + const handleCreatePrePackagedRules = useCallback(async () => { + if (createPrePackagedRules != null) { + await createPrePackagedRules(); + handleRefreshRules(); + } + }, [createPrePackagedRules, handleRefreshRules]); + + const handleRefetchPrePackagedRulesStatus = useCallback(() => { + if (refetchPrePackagedRulesStatus != null) { + refetchPrePackagedRulesStatus(); + } + }, [refetchPrePackagedRulesStatus]); + + const handleSetRefreshRulesData = useCallback((refreshRule: Func) => { + refreshRulesData.current = refreshRule; + }, []); + + if (redirectToDetections(isSignalIndexExists, isAuthenticated, hasEncryptionKey)) { + return ; + } + + return ( + <> + {userHasNoPermissions(canUserCRUD) && } + setShowImportModal(false)} + description={i18n.SELECT_RULE} + errorMessage={i18n.IMPORT_FAILED} + failedDetailed={i18n.IMPORT_FAILED_DETAILED} + importComplete={handleRefreshRules} + importData={importRules} + successMessage={i18n.SUCCESSFULLY_IMPORTED_RULES} + showCheckBox={true} + showModal={showImportModal} + submitBtnText={i18n.IMPORT_RULE_BTN_TITLE} + subtitle={i18n.INITIAL_PROMPT_TEXT} + title={i18n.IMPORT_RULE} + /> + + + + {prePackagedRuleStatus === 'ruleNotInstalled' && ( + + + {i18n.LOAD_PREPACKAGED_RULES} + + + )} + {prePackagedRuleStatus === 'someRuleUninstall' && ( + + + {i18n.RELOAD_MISSING_PREPACKAGED_RULES(rulesNotInstalled ?? 0)} + + + )} + + { + setShowImportModal(true); + }} + > + {i18n.IMPORT_RULE} + + + + + {i18n.ADD_NEW_RULE} + + + + + {prePackagedRuleStatus === 'ruleNeedUpdate' && ( + + )} + + + + + + ); +}; + +export const RulesPage = React.memo(RulesPageComponent); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/translations.ts b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/translations.ts rename to x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/translations.ts diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/types.ts b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/types.ts new file mode 100644 index 00000000000000..92c9780a117221 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/types.ts @@ -0,0 +1,141 @@ +/* + * 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 { RuleAlertAction, RuleType } from '../../../../../common/detection_engine/types'; +import { AlertAction } from '../../../../../../alerting/common'; +import { Filter } from '../../../../../../../../src/plugins/data/common'; +import { FormData, FormHook } from '../../../../shared_imports'; +import { FieldValueQueryBar } from '../../../components/rules/query_bar'; +import { FieldValueTimeline } from '../../../components/rules/pick_timeline'; + +export interface EuiBasicTableSortTypes { + field: string; + direction: 'asc' | 'desc'; +} + +export interface EuiBasicTableOnChange { + page: { + index: number; + size: number; + }; + sort?: EuiBasicTableSortTypes; +} + +export enum RuleStep { + defineRule = 'define-rule', + aboutRule = 'about-rule', + scheduleRule = 'schedule-rule', + ruleActions = 'rule-actions', +} +export type RuleStatusType = 'passive' | 'active' | 'valid'; + +export interface RuleStepData { + data: unknown; + isValid: boolean; +} + +export interface RuleStepProps { + addPadding?: boolean; + descriptionColumns?: 'multi' | 'single' | 'singleSplit'; + setStepData?: (step: RuleStep, data: unknown, isValid: boolean) => void; + isReadOnlyView: boolean; + isUpdateView?: boolean; + isLoading: boolean; + resizeParentContainer?: (height: number) => void; + setForm?: (step: RuleStep, form: FormHook) => void; +} + +interface StepRuleData { + isNew: boolean; +} +export interface AboutStepRule extends StepRuleData { + name: string; + description: string; + severity: string; + riskScore: number; + references: string[]; + falsePositives: string[]; + tags: string[]; + threat: IMitreEnterpriseAttack[]; + note: string; +} + +export interface AboutStepRuleDetails { + note: string; + description: string; +} + +export interface DefineStepRule extends StepRuleData { + anomalyThreshold: number; + index: string[]; + machineLearningJobId: string; + queryBar: FieldValueQueryBar; + ruleType: RuleType; + timeline: FieldValueTimeline; +} + +export interface ScheduleStepRule extends StepRuleData { + interval: string; + from: string; + to?: string; +} + +export interface ActionsStepRule extends StepRuleData { + actions: AlertAction[]; + enabled: boolean; + kibanaSiemAppUrl?: string; + throttle?: string | null; +} + +export interface DefineStepRuleJson { + anomaly_threshold?: number; + index?: string[]; + filters?: Filter[]; + machine_learning_job_id?: string; + saved_id?: string; + query?: string; + language?: string; + timeline_id?: string; + timeline_title?: string; + type: RuleType; +} + +export interface AboutStepRuleJson { + name: string; + description: string; + severity: string; + risk_score: number; + references: string[]; + false_positives: string[]; + tags: string[]; + threat: IMitreEnterpriseAttack[]; + note?: string; +} + +export interface ScheduleStepRuleJson { + interval: string; + from: string; + to?: string; + meta?: unknown; +} + +export interface ActionsStepRuleJson { + actions: RuleAlertAction[]; + enabled: boolean; + throttle?: string | null; + meta?: unknown; +} + +export interface IMitreAttack { + id: string; + name: string; + reference: string; +} +export interface IMitreEnterpriseAttack { + framework: string; + tactic: IMitreAttack; + technique: IMitreAttack[]; +} diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/utils.test.ts b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/utils.test.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/rules/utils.test.ts rename to x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/utils.test.ts diff --git a/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/utils.ts b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/utils.ts new file mode 100644 index 00000000000000..159301a07de787 --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/pages/detection_engine/rules/utils.ts @@ -0,0 +1,99 @@ +/* + * 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 { isEmpty } from 'lodash/fp'; + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ChromeBreadcrumb } from '../../../../../../../../src/core/public'; +import { + getDetectionEngineUrl, + getDetectionEngineTabUrl, + getRulesUrl, + getRuleDetailsUrl, + getCreateRuleUrl, + getEditRuleUrl, +} from '../../../../common/components/link_to/redirect_to_detection_engine'; +import * as i18nDetections from '../translations'; +import * as i18nRules from './translations'; +import { RouteSpyState } from '../../../../common/utils/route/types'; + +const getTabBreadcrumb = (pathname: string, search: string[]) => { + const tabPath = pathname.split('/')[2]; + + if (tabPath === 'alerts') { + return { + text: i18nDetections.ALERT, + href: `${getDetectionEngineTabUrl(tabPath)}${!isEmpty(search[0]) ? search[0] : ''}`, + }; + } + + if (tabPath === 'signals') { + return { + text: i18nDetections.SIGNAL, + href: `${getDetectionEngineTabUrl(tabPath)}${!isEmpty(search[0]) ? search[0] : ''}`, + }; + } + + if (tabPath === 'rules') { + return { + text: i18nRules.PAGE_TITLE, + href: `${getRulesUrl()}${!isEmpty(search[0]) ? search[0] : ''}`, + }; + } +}; + +const isRuleCreatePage = (pathname: string) => + pathname.includes('/rules') && pathname.includes('/create'); + +const isRuleEditPage = (pathname: string) => + pathname.includes('/rules') && pathname.includes('/edit'); + +export const getBreadcrumbs = (params: RouteSpyState, search: string[]): ChromeBreadcrumb[] => { + let breadcrumb = [ + { + text: i18nDetections.PAGE_TITLE, + href: `${getDetectionEngineUrl()}${!isEmpty(search[0]) ? search[0] : ''}`, + }, + ]; + + const tabBreadcrumb = getTabBreadcrumb(params.pathName, search); + + if (tabBreadcrumb) { + breadcrumb = [...breadcrumb, tabBreadcrumb]; + } + + if (params.detailName && params.state?.ruleName) { + breadcrumb = [ + ...breadcrumb, + { + text: params.state.ruleName, + href: `${getRuleDetailsUrl(params.detailName)}${!isEmpty(search[1]) ? search[1] : ''}`, + }, + ]; + } + + if (isRuleCreatePage(params.pathName)) { + breadcrumb = [ + ...breadcrumb, + { + text: i18nRules.ADD_PAGE_TITLE, + href: `${getCreateRuleUrl()}${!isEmpty(search[1]) ? search[1] : ''}`, + }, + ]; + } + + if (isRuleEditPage(params.pathName) && params.detailName && params.state?.ruleName) { + breadcrumb = [ + ...breadcrumb, + { + text: i18nRules.EDIT_PAGE_TITLE, + href: `${getEditRuleUrl(params.detailName)}${!isEmpty(search[1]) ? search[1] : ''}`, + }, + ]; + } + + return breadcrumb; +}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/translations.ts b/x-pack/plugins/siem/public/alerts/pages/detection_engine/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/translations.ts rename to x-pack/plugins/siem/public/alerts/pages/detection_engine/translations.ts diff --git a/x-pack/plugins/siem/public/pages/detection_engine/types.ts b/x-pack/plugins/siem/public/alerts/pages/detection_engine/types.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/detection_engine/types.ts rename to x-pack/plugins/siem/public/alerts/pages/detection_engine/types.ts diff --git a/x-pack/plugins/siem/public/alerts/routes.tsx b/x-pack/plugins/siem/public/alerts/routes.tsx new file mode 100644 index 00000000000000..897ba3269546fc --- /dev/null +++ b/x-pack/plugins/siem/public/alerts/routes.tsx @@ -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 React from 'react'; +import { Route } from 'react-router-dom'; + +import { DetectionEngineContainer } from './pages/detection_engine'; +import { SiemPageName } from '../app/types'; + +export const getAlertsRoutes = () => [ + ( + + )} + />, +]; diff --git a/x-pack/plugins/siem/public/pages/404.tsx b/x-pack/plugins/siem/public/app/404.tsx similarity index 90% rename from x-pack/plugins/siem/public/pages/404.tsx rename to x-pack/plugins/siem/public/app/404.tsx index ba1cb4f40cbed6..6a1b5c56dc853a 100644 --- a/x-pack/plugins/siem/public/pages/404.tsx +++ b/x-pack/plugins/siem/public/app/404.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { WrapperPage } from '../components/wrapper_page'; +import { WrapperPage } from '../common/components/wrapper_page'; export const NotFoundPage = React.memo(() => ( diff --git a/x-pack/plugins/siem/public/app/app.tsx b/x-pack/plugins/siem/public/app/app.tsx index 6e2a4642f99a46..7aef91380b5225 100644 --- a/x-pack/plugins/siem/public/app/app.tsx +++ b/x-pack/plugins/siem/public/app/app.tsx @@ -17,32 +17,35 @@ import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { BehaviorSubject } from 'rxjs'; import { pluck } from 'rxjs/operators'; -import { KibanaContextProvider, useKibana, useUiSetting$ } from '../lib/kibana'; +import { KibanaContextProvider, useKibana, useUiSetting$ } from '../common/lib/kibana'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { DEFAULT_DARK_MODE } from '../../common/constants'; -import { ErrorToastDispatcher } from '../components/error_toast_dispatcher'; -import { compose } from '../lib/compose/kibana_compose'; -import { AppFrontendLibs, AppApolloClient } from '../lib/lib'; +import { ErrorToastDispatcher } from '../common/components/error_toast_dispatcher'; +import { compose } from '../common/lib/compose/kibana_compose'; +import { AppFrontendLibs, AppApolloClient } from '../common/lib/lib'; import { StartServices } from '../plugin'; -import { PageRouter } from '../routes'; -import { createStore, createInitialState } from '../store'; -import { GlobalToaster, ManageGlobalToaster } from '../components/toasters'; -import { MlCapabilitiesProvider } from '../components/ml/permissions/ml_capabilities_provider'; +import { PageRouter } from './routes'; +import { createStore, createInitialState } from '../common/store'; +import { GlobalToaster, ManageGlobalToaster } from '../common/components/toasters'; +import { MlCapabilitiesProvider } from '../common/components/ml/permissions/ml_capabilities_provider'; -import { ApolloClientContext } from '../utils/apollo_context'; +import { ApolloClientContext } from '../common/utils/apollo_context'; +import { SecuritySubPlugins } from './types'; interface AppPluginRootComponentProps { apolloClient: AppApolloClient; history: History; store: Store; + subPluginRoutes: React.ReactElement[]; theme: any; // eslint-disable-line @typescript-eslint/no-explicit-any } const AppPluginRootComponent: React.FC = ({ + apolloClient, theme, store, - apolloClient, + subPluginRoutes, history, }) => ( @@ -51,7 +54,7 @@ const AppPluginRootComponent: React.FC = ({ - + @@ -64,11 +67,22 @@ const AppPluginRootComponent: React.FC = ({ const AppPluginRoot = memo(AppPluginRootComponent); -const StartAppComponent: FC = libs => { +interface StartAppComponent extends AppFrontendLibs { + subPlugins: SecuritySubPlugins; +} + +const StartAppComponent: FC = ({ subPlugins, ...libs }) => { + const { routes: subPluginRoutes, store: subPluginsStore } = subPlugins; const { i18n } = useKibana().services; const history = createHashHistory(); const libs$ = new BehaviorSubject(libs); - const store = createStore(createInitialState(), libs$.pipe(pluck('apolloClient'))); + + const store = createStore( + createInitialState(subPluginsStore.initialState), + subPluginsStore.reducer, + libs$.pipe(pluck('apolloClient')) + ); + const [darkMode] = useUiSetting$(DEFAULT_DARK_MODE); const theme = useMemo( () => ({ @@ -82,9 +96,10 @@ const StartAppComponent: FC = libs => { @@ -96,9 +111,10 @@ const StartApp = memo(StartAppComponent); interface SiemAppComponentProps { services: StartServices; + subPlugins: SecuritySubPlugins; } -const SiemAppComponent: React.FC = ({ services }) => ( +const SiemAppComponent: React.FC = ({ services, subPlugins }) => ( = ({ services }) => ( ...services, }} > - + ); diff --git a/x-pack/plugins/siem/public/pages/home/home_navigations.tsx b/x-pack/plugins/siem/public/app/home/home_navigations.tsx similarity index 93% rename from x-pack/plugins/siem/public/pages/home/home_navigations.tsx rename to x-pack/plugins/siem/public/app/home/home_navigations.tsx index 543469e2fddb7f..2eed64a2b26e55 100644 --- a/x-pack/plugins/siem/public/pages/home/home_navigations.tsx +++ b/x-pack/plugins/siem/public/app/home/home_navigations.tsx @@ -11,9 +11,9 @@ import { getTimelinesUrl, getHostsUrl, getCaseUrl, -} from '../../components/link_to'; +} from '../../common/components/link_to'; import * as i18n from './translations'; -import { SiemPageName, SiemNavTab } from './types'; +import { SiemPageName, SiemNavTab } from '../types'; export const navTabs: SiemNavTab = { [SiemPageName.overview]: { diff --git a/x-pack/plugins/siem/public/app/home/index.tsx b/x-pack/plugins/siem/public/app/home/index.tsx new file mode 100644 index 00000000000000..b6116ad4f06664 --- /dev/null +++ b/x-pack/plugins/siem/public/app/home/index.tsx @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useMemo } from 'react'; +import { Redirect, Route, Switch } from 'react-router-dom'; +import styled from 'styled-components'; + +import { useThrottledResizeObserver } from '../../common/components/utils'; +import { DragDropContextWrapper } from '../../common/components/drag_and_drop/drag_drop_context_wrapper'; +import { Flyout } from '../../timelines/components/flyout'; +import { HeaderGlobal } from '../../common/components/header_global'; +import { HelpMenu } from '../../common/components/help_menu'; +import { LinkToPage } from '../../common/components/link_to'; +import { MlHostConditionalContainer } from '../../common/components/ml/conditional_links/ml_host_conditional_container'; +import { MlNetworkConditionalContainer } from '../../common/components/ml/conditional_links/ml_network_conditional_container'; +import { AutoSaveWarningMsg } from '../../timelines/components/timeline/auto_save_warning'; +import { UseUrlState } from '../../common/components/url_state'; +import { + WithSource, + indicesExistOrDataTemporarilyUnavailable, +} from '../../common/containers/source'; +import { SpyRoute } from '../../common/utils/route/spy_routes'; +import { useShowTimeline } from '../../common/utils/timeline/use_show_timeline'; +import { NotFoundPage } from '../404'; +import { navTabs } from './home_navigations'; +import { SiemPageName } from '../types'; + +const WrappedByAutoSizer = styled.div` + height: 100%; +`; +WrappedByAutoSizer.displayName = 'WrappedByAutoSizer'; + +const Main = styled.main` + height: 100%; +`; +Main.displayName = 'Main'; + +const usersViewing = ['elastic']; // TODO: get the users viewing this timeline from Elasticsearch (persistance) + +/** the global Kibana navigation at the top of every page */ +const globalHeaderHeightPx = 48; + +const calculateFlyoutHeight = ({ + globalHeaderSize, + windowHeight, +}: { + globalHeaderSize: number; + windowHeight: number; +}): number => Math.max(0, windowHeight - globalHeaderSize); + +interface HomePageProps { + subPlugins: JSX.Element[]; +} + +export const HomePage: React.FC = ({ subPlugins }) => { + const { ref: measureRef, height: windowHeight = 0 } = useThrottledResizeObserver(); + const flyoutHeight = useMemo( + () => + calculateFlyoutHeight({ + globalHeaderSize: globalHeaderHeightPx, + windowHeight, + }), + [windowHeight] + ); + + const [showTimeline] = useShowTimeline(); + + return ( + + + +
+ + {({ browserFields, indexPattern, indicesExist }) => ( + + + {indicesExistOrDataTemporarilyUnavailable(indicesExist) && showTimeline && ( + <> + + + + )} + + + + {subPlugins} + } /> + ( + + )} + /> + ( + + )} + /> + } /> + + + )} + +
+ + + + +
+ ); +}; + +HomePage.displayName = 'HomePage'; diff --git a/x-pack/plugins/siem/public/pages/home/translations.ts b/x-pack/plugins/siem/public/app/home/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/home/translations.ts rename to x-pack/plugins/siem/public/app/home/translations.ts diff --git a/x-pack/plugins/siem/public/app/index.tsx b/x-pack/plugins/siem/public/app/index.tsx index 7275a718564eff..d69be6e09e6149 100644 --- a/x-pack/plugins/siem/public/app/index.tsx +++ b/x-pack/plugins/siem/public/app/index.tsx @@ -7,11 +7,17 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths import { AppMountParameters } from '../../../../../src/core/public'; import { StartServices } from '../plugin'; import { SiemApp } from './app'; +import { SecuritySubPlugins } from './types'; -export const renderApp = (services: StartServices, { element }: AppMountParameters) => { - render(, element); +export const renderApp = ( + services: StartServices, + { element }: AppMountParameters, + subPlugins: SecuritySubPlugins +) => { + render(, element); return () => unmountComponentAtNode(element); }; diff --git a/x-pack/plugins/siem/public/app/routes.tsx b/x-pack/plugins/siem/public/app/routes.tsx new file mode 100644 index 00000000000000..ed3565df5f507b --- /dev/null +++ b/x-pack/plugins/siem/public/app/routes.tsx @@ -0,0 +1,35 @@ +/* + * 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 { History } from 'history'; +import React, { FC, memo } from 'react'; +import { Route, Router, Switch } from 'react-router-dom'; + +import { NotFoundPage } from './404'; +import { HomePage } from './home'; +import { ManageRoutesSpy } from '../common/utils/route/manage_spy_routes'; + +interface RouterProps { + history: History; + subPluginRoutes: JSX.Element[]; +} + +const PageRouterComponent: FC = ({ history, subPluginRoutes }) => ( + + + + + + + + + + + + +); + +export const PageRouter = memo(PageRouterComponent); diff --git a/x-pack/plugins/siem/public/app/types.ts b/x-pack/plugins/siem/public/app/types.ts new file mode 100644 index 00000000000000..5fe4b5a8d82271 --- /dev/null +++ b/x-pack/plugins/siem/public/app/types.ts @@ -0,0 +1,61 @@ +/* + * 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 { Reducer, AnyAction } from 'redux'; + +import { NavTab } from '../common/components/navigation/types'; +import { HostsState } from '../hosts/store'; +import { NetworkState } from '../network/store'; +import { TimelineState } from '../timelines/store/timeline/types'; + +export enum SiemPageName { + overview = 'overview', + hosts = 'hosts', + network = 'network', + detections = 'detections', + timelines = 'timelines', + case = 'case', +} + +export type SiemNavTabKey = + | SiemPageName.overview + | SiemPageName.hosts + | SiemPageName.network + | SiemPageName.detections + | SiemPageName.timelines + | SiemPageName.case; + +export type SiemNavTab = Record; + +export interface SecuritySubPluginStore { + initialState: Record; + reducer: Record>; +} + +export interface SecuritySubPlugin { + routes: React.ReactElement[]; +} + +type SecuritySubPluginKeyStore = 'hosts' | 'network' | 'timeline'; +export interface SecuritySubPluginWithStore + extends SecuritySubPlugin { + store: SecuritySubPluginStore; +} + +export interface SecuritySubPlugins extends SecuritySubPlugin { + store: { + initialState: { + hosts: HostsState; + network: NetworkState; + timeline: TimelineState; + }; + reducer: { + hosts: Reducer; + network: Reducer; + timeline: Reducer; + }; + }; +} diff --git a/x-pack/plugins/siem/public/pages/case/components/__mock__/form.ts b/x-pack/plugins/siem/public/cases/components/__mock__/form.ts similarity index 84% rename from x-pack/plugins/siem/public/pages/case/components/__mock__/form.ts rename to x-pack/plugins/siem/public/cases/components/__mock__/form.ts index 12946c3af06bdb..96c1217577ff25 100644 --- a/x-pack/plugins/siem/public/pages/case/components/__mock__/form.ts +++ b/x-pack/plugins/siem/public/cases/components/__mock__/form.ts @@ -3,9 +3,9 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { useForm } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks'; +import { useForm } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'; jest.mock( - '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' + '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' ); export const mockFormHook = { isSubmitted: false, diff --git a/x-pack/plugins/siem/public/pages/case/components/__mock__/router.ts b/x-pack/plugins/siem/public/cases/components/__mock__/router.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/case/components/__mock__/router.ts rename to x-pack/plugins/siem/public/cases/components/__mock__/router.ts diff --git a/x-pack/plugins/siem/public/cases/components/add_comment/index.test.tsx b/x-pack/plugins/siem/public/cases/components/add_comment/index.test.tsx new file mode 100644 index 00000000000000..ab61930cd841b7 --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/add_comment/index.test.tsx @@ -0,0 +1,146 @@ +/* + * 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 { AddComment } from '.'; +import { TestProviders } from '../../../common/mock'; +import { getFormMock } from '../__mock__/form'; +import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; + +import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline'; +import { usePostComment } from '../../containers/use_post_comment'; +import { useForm } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'; +import { wait } from '../../../common/lib/helpers'; + +jest.mock( + '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' +); + +jest.mock('../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline'); +jest.mock('../../containers/use_post_comment'); + +export const useFormMock = useForm as jest.Mock; + +const useInsertTimelineMock = useInsertTimeline as jest.Mock; +const usePostCommentMock = usePostComment as jest.Mock; + +const onCommentSaving = jest.fn(); +const onCommentPosted = jest.fn(); +const postComment = jest.fn(); +const handleCursorChange = jest.fn(); +const handleOnTimelineChange = jest.fn(); + +const addCommentProps = { + caseId: '1234', + disabled: false, + insertQuote: null, + onCommentSaving, + onCommentPosted, + showLoading: false, +}; + +const defaultInsertTimeline = { + cursorPosition: { + start: 0, + end: 0, + }, + handleCursorChange, + handleOnTimelineChange, +}; + +const defaultPostCommment = { + isLoading: false, + isError: false, + postComment, +}; +const sampleData = { + comment: 'what a cool comment', +}; +describe('AddComment ', () => { + const formHookMock = getFormMock(sampleData); + + beforeEach(() => { + jest.resetAllMocks(); + useInsertTimelineMock.mockImplementation(() => defaultInsertTimeline); + usePostCommentMock.mockImplementation(() => defaultPostCommment); + useFormMock.mockImplementation(() => ({ form: formHookMock })); + jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); + }); + + it('should post comment on submit click', async () => { + const wrapper = mount( + + + + + + ); + expect(wrapper.find(`[data-test-subj="add-comment"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="loading-spinner"]`).exists()).toBeFalsy(); + + wrapper + .find(`[data-test-subj="submit-comment"]`) + .first() + .simulate('click'); + await wait(); + expect(onCommentSaving).toBeCalled(); + expect(postComment).toBeCalledWith(sampleData, onCommentPosted); + expect(formHookMock.reset).toBeCalled(); + }); + + it('should render spinner and disable submit when loading', () => { + usePostCommentMock.mockImplementation(() => ({ ...defaultPostCommment, isLoading: true })); + const wrapper = mount( + + + + + + ); + expect(wrapper.find(`[data-test-subj="loading-spinner"]`).exists()).toBeTruthy(); + expect( + wrapper + .find(`[data-test-subj="submit-comment"]`) + .first() + .prop('isDisabled') + ).toBeTruthy(); + }); + + it('should disable submit button when disabled prop passed', () => { + usePostCommentMock.mockImplementation(() => ({ ...defaultPostCommment, isLoading: true })); + const wrapper = mount( + + + + + + ); + expect( + wrapper + .find(`[data-test-subj="submit-comment"]`) + .first() + .prop('isDisabled') + ).toBeTruthy(); + }); + + it('should insert a quote if one is available', () => { + const sampleQuote = 'what a cool quote'; + mount( + + + + + + ); + + expect(formHookMock.setFieldValue).toBeCalledWith( + 'comment', + `${sampleData.comment}\n\n${sampleQuote}` + ); + }); +}); diff --git a/x-pack/plugins/siem/public/cases/components/add_comment/index.tsx b/x-pack/plugins/siem/public/cases/components/add_comment/index.tsx new file mode 100644 index 00000000000000..277352c39df65e --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/add_comment/index.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButton, EuiLoadingSpinner } from '@elastic/eui'; +import React, { useCallback, useEffect } from 'react'; +import styled from 'styled-components'; + +import { CommentRequest } from '../../../../../case/common/api'; +import { usePostComment } from '../../containers/use_post_comment'; +import { Case } from '../../containers/types'; +import { MarkdownEditorForm } from '../../../common/components/markdown_editor/form'; +import { InsertTimelinePopover } from '../../../timelines/components/timeline/insert_timeline_popover'; +import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline'; +import { Form, useForm, UseField } from '../../../shared_imports'; + +import * as i18n from './translations'; +import { schema } from './schema'; + +const MySpinner = styled(EuiLoadingSpinner)` + position: absolute; + top: 50%; + left: 50%; +`; + +const initialCommentValue: CommentRequest = { + comment: '', +}; + +interface AddCommentProps { + caseId: string; + disabled?: boolean; + insertQuote: string | null; + onCommentSaving?: () => void; + onCommentPosted: (newCase: Case) => void; + showLoading?: boolean; +} + +export const AddComment = React.memo( + ({ caseId, disabled, insertQuote, showLoading = true, onCommentPosted, onCommentSaving }) => { + const { isLoading, postComment } = usePostComment(caseId); + const { form } = useForm({ + defaultValue: initialCommentValue, + options: { stripEmptyFields: false }, + schema, + }); + const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline( + form, + 'comment' + ); + + useEffect(() => { + if (insertQuote !== null) { + const { comment } = form.getFormData(); + form.setFieldValue( + 'comment', + `${comment}${comment.length > 0 ? '\n\n' : ''}${insertQuote}` + ); + } + }, [insertQuote]); + + const onSubmit = useCallback(async () => { + const { isValid, data } = await form.submit(); + if (isValid) { + if (onCommentSaving != null) { + onCommentSaving(); + } + await postComment(data, onCommentPosted); + form.reset(); + } + }, [form, onCommentPosted, onCommentSaving]); + return ( + + {isLoading && showLoading && } +
+ + {i18n.ADD_COMMENT} + + ), + topRightContent: ( + + ), + }} + /> + +
+ ); + } +); + +AddComment.displayName = 'AddComment'; diff --git a/x-pack/plugins/siem/public/cases/components/add_comment/schema.tsx b/x-pack/plugins/siem/public/cases/components/add_comment/schema.tsx new file mode 100644 index 00000000000000..eb11357cd7ce94 --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/add_comment/schema.tsx @@ -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 { CommentRequest } from '../../../../../case/common/api'; +import { FIELD_TYPES, fieldValidators, FormSchema } from '../../../shared_imports'; +import * as i18n from './translations'; + +const { emptyField } = fieldValidators; + +export const schema: FormSchema = { + comment: { + type: FIELD_TYPES.TEXTAREA, + validations: [ + { + validator: emptyField(i18n.COMMENT_REQUIRED), + }, + ], + }, +}; diff --git a/x-pack/plugins/siem/public/cases/components/add_comment/translations.ts b/x-pack/plugins/siem/public/cases/components/add_comment/translations.ts new file mode 100644 index 00000000000000..704b8db48c1d3b --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/add_comment/translations.ts @@ -0,0 +1,7 @@ +/* + * 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 * from '../../translations'; diff --git a/x-pack/plugins/siem/public/cases/components/all_cases/actions.tsx b/x-pack/plugins/siem/public/cases/components/all_cases/actions.tsx new file mode 100644 index 00000000000000..9f7e2e73c5bbcf --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/all_cases/actions.tsx @@ -0,0 +1,61 @@ +/* + * 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 { DefaultItemIconButtonAction } from '@elastic/eui/src/components/basic_table/action_types'; +import { Dispatch } from 'react'; +import { Case } from '../../containers/types'; + +import * as i18n from './translations'; +import { UpdateCase } from '../../containers/use_get_cases'; + +interface GetActions { + caseStatus: string; + dispatchUpdate: Dispatch>; + deleteCaseOnClick: (deleteCase: Case) => void; +} + +export const getActions = ({ + caseStatus, + dispatchUpdate, + deleteCaseOnClick, +}: GetActions): Array> => [ + { + description: i18n.DELETE_CASE, + icon: 'trash', + name: i18n.DELETE_CASE, + onClick: deleteCaseOnClick, + type: 'icon', + 'data-test-subj': 'action-delete', + }, + caseStatus === 'open' + ? { + description: i18n.CLOSE_CASE, + icon: 'folderCheck', + name: i18n.CLOSE_CASE, + onClick: (theCase: Case) => + dispatchUpdate({ + updateKey: 'status', + updateValue: 'closed', + caseId: theCase.id, + version: theCase.version, + }), + type: 'icon', + 'data-test-subj': 'action-close', + } + : { + description: i18n.REOPEN_CASE, + icon: 'folderExclamation', + name: i18n.REOPEN_CASE, + onClick: (theCase: Case) => + dispatchUpdate({ + updateKey: 'status', + updateValue: 'open', + caseId: theCase.id, + version: theCase.version, + }), + type: 'icon', + 'data-test-subj': 'action-open', + }, +]; diff --git a/x-pack/plugins/siem/public/pages/case/components/all_cases/columns.test.tsx b/x-pack/plugins/siem/public/cases/components/all_cases/columns.test.tsx similarity index 94% rename from x-pack/plugins/siem/public/pages/case/components/all_cases/columns.test.tsx rename to x-pack/plugins/siem/public/cases/components/all_cases/columns.test.tsx index 2a06fa6eb51acb..8316823591f3f9 100644 --- a/x-pack/plugins/siem/public/pages/case/components/all_cases/columns.test.tsx +++ b/x-pack/plugins/siem/public/cases/components/all_cases/columns.test.tsx @@ -9,7 +9,7 @@ import { mount } from 'enzyme'; import { ExternalServiceColumn } from './columns'; -import { useGetCasesMockState } from '../../../../containers/case/mock'; +import { useGetCasesMockState } from '../../containers/mock'; describe('ExternalServiceColumn ', () => { it('Not pushed render', () => { diff --git a/x-pack/plugins/siem/public/cases/components/all_cases/columns.tsx b/x-pack/plugins/siem/public/cases/components/all_cases/columns.tsx new file mode 100644 index 00000000000000..ddd860a8720c57 --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/all_cases/columns.tsx @@ -0,0 +1,215 @@ +/* + * 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, { useCallback } from 'react'; +import { + EuiBadge, + EuiTableFieldDataColumnType, + EuiTableComputedColumnType, + EuiTableActionsColumnType, + EuiAvatar, + EuiLink, +} from '@elastic/eui'; +import styled from 'styled-components'; +import { DefaultItemIconButtonAction } from '@elastic/eui/src/components/basic_table/action_types'; +import { getEmptyTagValue } from '../../../common/components/empty_value'; +import { Case } from '../../containers/types'; +import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date'; +import { CaseDetailsLink } from '../../../common/components/links'; +import { TruncatableText } from '../../../common/components/truncatable_text'; +import * as i18n from './translations'; + +export type CasesColumns = + | EuiTableFieldDataColumnType + | EuiTableComputedColumnType + | EuiTableActionsColumnType; + +const MediumShadeText = styled.p` + color: ${({ theme }) => theme.eui.euiColorMediumShade}; +`; + +const Spacer = styled.span` + margin-left: ${({ theme }) => theme.eui.paddingSizes.s}; +`; + +const renderStringField = (field: string, dataTestSubj: string) => + field != null ? {field} : getEmptyTagValue(); + +export const getCasesColumns = ( + actions: Array>, + filterStatus: string +): CasesColumns[] => [ + { + name: i18n.NAME, + render: (theCase: Case) => { + if (theCase.id != null && theCase.title != null) { + const caseDetailsLinkComponent = ( + + {theCase.title} + + ); + return theCase.status === 'open' ? ( + caseDetailsLinkComponent + ) : ( + <> + + {caseDetailsLinkComponent} + {i18n.CLOSED} + + + ); + } + return getEmptyTagValue(); + }, + }, + { + field: 'createdBy', + name: i18n.REPORTER, + render: (createdBy: Case['createdBy']) => { + if (createdBy != null) { + return ( + <> + + + {createdBy.fullName ? createdBy.fullName : createdBy.username ?? ''} + + + ); + } + return getEmptyTagValue(); + }, + }, + { + field: 'tags', + name: i18n.TAGS, + render: (tags: Case['tags']) => { + if (tags != null && tags.length > 0) { + return ( + + {tags.map((tag: string, i: number) => ( + + {tag} + + ))} + + ); + } + return getEmptyTagValue(); + }, + truncateText: true, + }, + { + align: 'right', + field: 'totalComment', + name: i18n.COMMENTS, + sortable: true, + render: (totalComment: Case['totalComment']) => + totalComment != null + ? renderStringField(`${totalComment}`, `case-table-column-commentCount`) + : getEmptyTagValue(), + }, + filterStatus === 'open' + ? { + field: 'createdAt', + name: i18n.OPENED_ON, + sortable: true, + render: (createdAt: Case['createdAt']) => { + if (createdAt != null) { + return ( + + + + ); + } + return getEmptyTagValue(); + }, + } + : { + field: 'closedAt', + name: i18n.CLOSED_ON, + sortable: true, + render: (closedAt: Case['closedAt']) => { + if (closedAt != null) { + return ( + + + + ); + } + return getEmptyTagValue(); + }, + }, + { + name: i18n.EXTERNAL_INCIDENT, + render: (theCase: Case) => { + if (theCase.id != null) { + return ; + } + return getEmptyTagValue(); + }, + }, + { + name: i18n.INCIDENT_MANAGEMENT_SYSTEM, + render: (theCase: Case) => { + if (theCase.externalService != null) { + return renderStringField( + `${theCase.externalService.connectorName}`, + `case-table-column-connector` + ); + } + return getEmptyTagValue(); + }, + }, + { + name: i18n.ACTIONS, + actions, + }, +]; + +interface Props { + theCase: Case; +} + +export const ExternalServiceColumn: React.FC = ({ theCase }) => { + const handleRenderDataToPush = useCallback(() => { + const lastCaseUpdate = theCase.updatedAt != null ? new Date(theCase.updatedAt) : null; + const lastCasePush = + theCase.externalService?.pushedAt != null + ? new Date(theCase.externalService?.pushedAt) + : null; + const hasDataToPush = + lastCasePush === null || + (lastCasePush != null && + lastCaseUpdate != null && + lastCasePush.getTime() < lastCaseUpdate?.getTime()); + return ( +

+ + {theCase.externalService?.externalTitle} + + {hasDataToPush + ? renderStringField(i18n.REQUIRES_UPDATE, `case-table-column-external-requiresUpdate`) + : renderStringField(i18n.UP_TO_DATE, `case-table-column-external-upToDate`)} +

+ ); + }, [theCase]); + if (theCase.externalService !== null) { + return handleRenderDataToPush(); + } + return renderStringField(i18n.NOT_PUSHED, `case-table-column-external-notPushed`); +}; diff --git a/x-pack/plugins/siem/public/cases/components/all_cases/index.test.tsx b/x-pack/plugins/siem/public/cases/components/all_cases/index.test.tsx new file mode 100644 index 00000000000000..1dbd008277b347 --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/all_cases/index.test.tsx @@ -0,0 +1,349 @@ +/* + * 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 '../../../common/mock'; +import { useGetCasesMockState } from '../../containers/mock'; +import * as i18n from './translations'; + +import { getEmptyTagValue } from '../../../common/components/empty_value'; +import { useDeleteCases } from '../../containers/use_delete_cases'; +import { useGetCases } from '../../containers/use_get_cases'; +import { useGetCasesStatus } from '../../containers/use_get_cases_status'; +import { useUpdateCases } from '../../containers/use_bulk_update_case'; +import { getCasesColumns } from './columns'; + +jest.mock('../../containers/use_bulk_update_case'); +jest.mock('../../containers/use_delete_cases'); +jest.mock('../../containers/use_get_cases'); +jest.mock('../../containers/use_get_cases_status'); + +const useDeleteCasesMock = useDeleteCases as jest.Mock; +const useGetCasesMock = useGetCases as jest.Mock; +const useGetCasesStatusMock = useGetCasesStatus as jest.Mock; +const useUpdateCasesMock = useUpdateCases as jest.Mock; + +describe('AllCases', () => { + const dispatchResetIsDeleted = jest.fn(); + const dispatchResetIsUpdated = jest.fn(); + const dispatchUpdateCaseProperty = jest.fn(); + const handleOnDeleteConfirm = jest.fn(); + const handleToggleModal = jest.fn(); + const refetchCases = jest.fn(); + const setFilters = jest.fn(); + const setQueryParams = jest.fn(); + const setSelectedCases = jest.fn(); + const updateBulkStatus = jest.fn(); + const fetchCasesStatus = jest.fn(); + const emptyTag = getEmptyTagValue().props.children; + + const defaultGetCases = { + ...useGetCasesMockState, + dispatchUpdateCaseProperty, + refetchCases, + setFilters, + setQueryParams, + setSelectedCases, + }; + const defaultDeleteCases = { + dispatchResetIsDeleted, + handleOnDeleteConfirm, + handleToggleModal, + isDeleted: false, + isDisplayConfirmDeleteModal: false, + isLoading: false, + }; + const defaultCasesStatus = { + countClosedCases: 0, + countOpenCases: 5, + fetchCasesStatus, + isError: false, + isLoading: true, + }; + const defaultUpdateCases = { + isUpdated: false, + isLoading: false, + isError: false, + dispatchResetIsUpdated, + updateBulkStatus, + }; + /* eslint-disable no-console */ + // Silence until enzyme fixed to use ReactTestUtils.act() + const originalError = console.error; + beforeAll(() => { + console.error = jest.fn(); + }); + afterAll(() => { + console.error = originalError; + }); + /* eslint-enable no-console */ + beforeEach(() => { + jest.resetAllMocks(); + useUpdateCasesMock.mockImplementation(() => defaultUpdateCases); + useGetCasesMock.mockImplementation(() => defaultGetCases); + useDeleteCasesMock.mockImplementation(() => defaultDeleteCases); + useGetCasesStatusMock.mockImplementation(() => defaultCasesStatus); + 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].id}?timerange=(global:(linkTo:!(timeline),timerange:(from:0,fromStr:now-24h,kind:relative,to:1,toStr:now)),timeline:(linkTo:!(global),timerange:(from:0,fromStr:now-24h,kind:relative,to:1,toStr:now)))` + ); + expect( + wrapper + .find(`a[data-test-subj="case-details-link"]`) + .first() + .text() + ).toEqual(useGetCasesMockState.data.cases[0].title); + 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-createdBy"]`) + .first() + .text() + ).toEqual(useGetCasesMockState.data.cases[0].createdBy.fullName); + expect( + wrapper + .find(`[data-test-subj="case-table-column-createdAt"]`) + .first() + .childAt(0) + .prop('value') + ).toBe(useGetCasesMockState.data.cases[0].createdAt); + expect( + wrapper + .find(`[data-test-subj="case-table-case-count"]`) + .first() + .text() + ).toEqual('Showing 10 cases'); + }); + it('should render empty fields', () => { + useGetCasesMock.mockImplementation(() => ({ + ...defaultGetCases, + data: { + ...defaultGetCases.data, + cases: [ + { + ...defaultGetCases.data.cases[0], + id: null, + createdAt: null, + createdBy: null, + tags: null, + title: null, + totalComment: null, + }, + ], + }, + })); + const wrapper = mount( + + + + ); + const checkIt = (columnName: string, key: number) => { + const column = wrapper.find('[data-test-subj="cases-table"] tbody .euiTableRowCell').at(key); + if (columnName === i18n.ACTIONS) { + return; + } + expect(column.find('.euiTableRowCell--hideForDesktop').text()).toEqual(columnName); + expect(column.find('span').text()).toEqual(emptyTag); + }; + getCasesColumns([], 'open').map((i, key) => i.name != null && checkIt(`${i.name}`, key)); + }); + it('should tableHeaderSortButton AllCases', () => { + const wrapper = mount( + + + + ); + wrapper + .find('[data-test-subj="tableHeaderSortButton"]') + .first() + .simulate('click'); + expect(setQueryParams).toBeCalledWith({ + page: 1, + perPage: 5, + sortField: 'createdAt', + sortOrder: 'asc', + }); + }); + it('closes case when row action icon clicked', () => { + const wrapper = mount( + + + + ); + wrapper + .find('[data-test-subj="action-close"]') + .first() + .simulate('click'); + const firstCase = useGetCasesMockState.data.cases[0]; + expect(dispatchUpdateCaseProperty).toBeCalledWith({ + caseId: firstCase.id, + updateKey: 'status', + updateValue: 'closed', + refetchCasesStatus: fetchCasesStatus, + version: firstCase.version, + }); + }); + it('opens case when row action icon clicked', () => { + useGetCasesMock.mockImplementation(() => ({ + ...defaultGetCases, + filterOptions: { ...defaultGetCases.filterOptions, status: 'closed' }, + })); + + const wrapper = mount( + + + + ); + wrapper + .find('[data-test-subj="action-open"]') + .first() + .simulate('click'); + const firstCase = useGetCasesMockState.data.cases[0]; + expect(dispatchUpdateCaseProperty).toBeCalledWith({ + caseId: firstCase.id, + updateKey: 'status', + updateValue: 'open', + refetchCasesStatus: fetchCasesStatus, + version: firstCase.version, + }); + }); + it('Bulk delete', () => { + useGetCasesMock.mockImplementation(() => ({ + ...defaultGetCases, + selectedCases: useGetCasesMockState.data.cases, + })); + useDeleteCasesMock + .mockReturnValueOnce({ + ...defaultDeleteCases, + isDisplayConfirmDeleteModal: false, + }) + .mockReturnValue({ + ...defaultDeleteCases, + isDisplayConfirmDeleteModal: true, + }); + + const wrapper = mount( + + + + ); + wrapper + .find('[data-test-subj="case-table-bulk-actions"] button') + .first() + .simulate('click'); + wrapper + .find('[data-test-subj="cases-bulk-delete-button"]') + .first() + .simulate('click'); + expect(handleToggleModal).toBeCalled(); + + wrapper + .find( + '[data-test-subj="confirm-delete-case-modal"] [data-test-subj="confirmModalConfirmButton"]' + ) + .last() + .simulate('click'); + expect(handleOnDeleteConfirm.mock.calls[0][0]).toStrictEqual( + useGetCasesMockState.data.cases.map(({ id }) => ({ id })) + ); + }); + it('Bulk close status update', () => { + useGetCasesMock.mockImplementation(() => ({ + ...defaultGetCases, + selectedCases: useGetCasesMockState.data.cases, + })); + + const wrapper = mount( + + + + ); + wrapper + .find('[data-test-subj="case-table-bulk-actions"] button') + .first() + .simulate('click'); + wrapper + .find('[data-test-subj="cases-bulk-close-button"]') + .first() + .simulate('click'); + expect(updateBulkStatus).toBeCalledWith(useGetCasesMockState.data.cases, 'closed'); + }); + it('Bulk open status update', () => { + useGetCasesMock.mockImplementation(() => ({ + ...defaultGetCases, + selectedCases: useGetCasesMockState.data.cases, + filterOptions: { + ...defaultGetCases.filterOptions, + status: 'closed', + }, + })); + + const wrapper = mount( + + + + ); + wrapper + .find('[data-test-subj="case-table-bulk-actions"] button') + .first() + .simulate('click'); + wrapper + .find('[data-test-subj="cases-bulk-open-button"]') + .first() + .simulate('click'); + expect(updateBulkStatus).toBeCalledWith(useGetCasesMockState.data.cases, 'open'); + }); + it('isDeleted is true, refetch', () => { + useDeleteCasesMock.mockImplementation(() => ({ + ...defaultDeleteCases, + isDeleted: true, + })); + + mount( + + + + ); + expect(refetchCases).toBeCalled(); + expect(fetchCasesStatus).toBeCalled(); + expect(dispatchResetIsDeleted).toBeCalled(); + }); + it('isUpdated is true, refetch', () => { + useUpdateCasesMock.mockImplementation(() => ({ + ...defaultUpdateCases, + isUpdated: true, + })); + + mount( + + + + ); + expect(refetchCases).toBeCalled(); + expect(fetchCasesStatus).toBeCalled(); + expect(dispatchResetIsUpdated).toBeCalled(); + }); +}); diff --git a/x-pack/plugins/siem/public/cases/components/all_cases/index.tsx b/x-pack/plugins/siem/public/cases/components/all_cases/index.tsx new file mode 100644 index 00000000000000..e86953c84336c8 --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/all_cases/index.tsx @@ -0,0 +1,439 @@ +/* + * 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, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { + EuiBasicTable, + EuiButton, + EuiContextMenuPanel, + EuiEmptyPrompt, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingContent, + EuiProgress, + EuiTableSortingType, +} from '@elastic/eui'; +import { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types'; +import { isEmpty } from 'lodash/fp'; +import styled, { css } from 'styled-components'; +import * as i18n from './translations'; + +import { getCasesColumns } from './columns'; +import { Case, DeleteCase, FilterOptions, SortFieldCase } from '../../containers/types'; +import { useGetCases, UpdateCase } from '../../containers/use_get_cases'; +import { useGetCasesStatus } from '../../containers/use_get_cases_status'; +import { useDeleteCases } from '../../containers/use_delete_cases'; +import { EuiBasicTableOnChange } from '../../../alerts/pages/detection_engine/rules/types'; +import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search'; +import { Panel } from '../../../common/components/panel'; +import { + UtilityBar, + UtilityBarAction, + UtilityBarGroup, + UtilityBarSection, + UtilityBarText, +} from '../../../common/components/utility_bar'; +import { getCreateCaseUrl } from '../../../common/components/link_to'; +import { getBulkItems } from '../bulk_actions'; +import { CaseHeaderPage } from '../case_header_page'; +import { ConfirmDeleteCaseModal } from '../confirm_delete_case'; +import { OpenClosedStats } from '../open_closed_stats'; +import { navTabs } from '../../../app/home/home_navigations'; + +import { getActions } from './actions'; +import { CasesTableFilters } from './table_filters'; +import { useUpdateCases } from '../../containers/use_bulk_update_case'; +import { useGetActionLicense } from '../../containers/use_get_action_license'; +import { getActionLicenseError } from '../use_push_to_service/helpers'; +import { CaseCallOut } from '../callout'; +import { ConfigureCaseButton } from '../configure_cases/button'; +import { ERROR_PUSH_SERVICE_CALLOUT_TITLE } from '../use_push_to_service/translations'; + +const Div = styled.div` + margin-top: ${({ theme }) => theme.eui.paddingSizes.m}; +`; +const FlexItemDivider = styled(EuiFlexItem)` + ${({ theme }) => css` + .euiFlexGroup--gutterMedium > &.euiFlexItem { + border-right: ${theme.eui.euiBorderThin}; + padding-right: ${theme.eui.euiSize}; + margin-right: ${theme.eui.euiSize}; + } + `} +`; + +const ProgressLoader = styled(EuiProgress)` + ${({ theme }) => css` + top: 2px; + border-radius: ${theme.eui.euiBorderRadius}; + z-index: ${theme.eui.euiZHeader}; + `} +`; + +const getSortField = (field: string): SortFieldCase => { + if (field === SortFieldCase.createdAt) { + return SortFieldCase.createdAt; + } else if (field === SortFieldCase.closedAt) { + return SortFieldCase.closedAt; + } + return SortFieldCase.createdAt; +}; + +interface AllCasesProps { + userCanCrud: boolean; +} +export const AllCases = React.memo(({ userCanCrud }) => { + const urlSearch = useGetUrlSearch(navTabs.case); + const { actionLicense } = useGetActionLicense(); + const { + countClosedCases, + countOpenCases, + isLoading: isCasesStatusLoading, + fetchCasesStatus, + } = useGetCasesStatus(); + const { + data, + dispatchUpdateCaseProperty, + filterOptions, + loading, + queryParams, + selectedCases, + refetchCases, + setFilters, + setQueryParams, + setSelectedCases, + } = useGetCases(); + + // Delete case + const { + dispatchResetIsDeleted, + handleOnDeleteConfirm, + handleToggleModal, + isLoading: isDeleting, + isDeleted, + isDisplayConfirmDeleteModal, + } = useDeleteCases(); + + // Update case + const { + dispatchResetIsUpdated, + isLoading: isUpdating, + isUpdated, + updateBulkStatus, + } = useUpdateCases(); + const [deleteThisCase, setDeleteThisCase] = useState({ + title: '', + id: '', + }); + const [deleteBulk, setDeleteBulk] = useState([]); + const filterRefetch = useRef<() => void>(); + const setFilterRefetch = useCallback( + (refetchFilter: () => void) => { + filterRefetch.current = refetchFilter; + }, + [filterRefetch.current] + ); + const refreshCases = useCallback( + (dataRefresh = true) => { + if (dataRefresh) refetchCases(); + fetchCasesStatus(); + setSelectedCases([]); + setDeleteBulk([]); + if (filterRefetch.current != null) { + filterRefetch.current(); + } + }, + [filterOptions, queryParams, filterRefetch.current] + ); + + useEffect(() => { + if (isDeleted) { + refreshCases(); + dispatchResetIsDeleted(); + } + if (isUpdated) { + refreshCases(); + dispatchResetIsUpdated(); + } + }, [isDeleted, isUpdated]); + const confirmDeleteModal = useMemo( + () => ( + 0} + onCancel={handleToggleModal} + onConfirm={handleOnDeleteConfirm.bind( + null, + deleteBulk.length > 0 ? deleteBulk : [deleteThisCase] + )} + /> + ), + [deleteBulk, deleteThisCase, isDisplayConfirmDeleteModal] + ); + + const toggleDeleteModal = useCallback((deleteCase: Case) => { + handleToggleModal(); + setDeleteThisCase(deleteCase); + }, []); + + const toggleBulkDeleteModal = useCallback( + (caseIds: string[]) => { + handleToggleModal(); + if (caseIds.length === 1) { + const singleCase = selectedCases.find(theCase => theCase.id === caseIds[0]); + if (singleCase) { + return setDeleteThisCase({ id: singleCase.id, title: singleCase.title }); + } + } + const convertToDeleteCases: DeleteCase[] = caseIds.map(id => ({ id })); + setDeleteBulk(convertToDeleteCases); + }, + [selectedCases] + ); + + const handleUpdateCaseStatus = useCallback( + (status: string) => { + updateBulkStatus(selectedCases, status); + }, + [selectedCases] + ); + + const selectedCaseIds = useMemo( + (): string[] => selectedCases.map((caseObj: Case) => caseObj.id), + [selectedCases] + ); + + const getBulkItemsPopoverContent = useCallback( + (closePopover: () => void) => ( + + ), + [selectedCaseIds, filterOptions.status, toggleBulkDeleteModal] + ); + const handleDispatchUpdate = useCallback( + (args: Omit) => { + dispatchUpdateCaseProperty({ ...args, refetchCasesStatus: fetchCasesStatus }); + }, + [dispatchUpdateCaseProperty, fetchCasesStatus] + ); + + const actions = useMemo( + () => + getActions({ + caseStatus: filterOptions.status, + deleteCaseOnClick: toggleDeleteModal, + dispatchUpdate: handleDispatchUpdate, + }), + [filterOptions.status, toggleDeleteModal, handleDispatchUpdate] + ); + + const actionsErrors = useMemo(() => getActionLicenseError(actionLicense), [actionLicense]); + + const tableOnChangeCallback = useCallback( + ({ page, sort }: EuiBasicTableOnChange) => { + let newQueryParams = queryParams; + if (sort) { + newQueryParams = { + ...newQueryParams, + sortField: getSortField(sort.field), + sortOrder: sort.direction, + }; + } + if (page) { + newQueryParams = { + ...newQueryParams, + page: page.index + 1, + perPage: page.size, + }; + } + setQueryParams(newQueryParams); + refreshCases(false); + }, + [queryParams] + ); + + const onFilterChangedCallback = useCallback( + (newFilterOptions: Partial) => { + if (newFilterOptions.status && newFilterOptions.status === 'closed') { + setQueryParams({ sortField: SortFieldCase.closedAt }); + } else if (newFilterOptions.status && newFilterOptions.status === 'open') { + setQueryParams({ sortField: SortFieldCase.createdAt }); + } + setFilters(newFilterOptions); + refreshCases(false); + }, + [filterOptions, queryParams] + ); + + const memoizedGetCasesColumns = useMemo( + () => getCasesColumns(userCanCrud ? actions : [], filterOptions.status), + [actions, filterOptions.status, userCanCrud] + ); + const memoizedPagination = useMemo( + () => ({ + pageIndex: queryParams.page - 1, + pageSize: queryParams.perPage, + totalItemCount: data.total, + pageSizeOptions: [5, 10, 15, 20, 25], + }), + [data, queryParams] + ); + + const sorting: EuiTableSortingType = { + sort: { field: queryParams.sortField, direction: queryParams.sortOrder }, + }; + const euiBasicTableSelectionProps = useMemo>( + () => ({ onSelectionChange: setSelectedCases }), + [selectedCases] + ); + const isCasesLoading = useMemo( + () => loading.indexOf('cases') > -1 || loading.indexOf('caseUpdate') > -1, + [loading] + ); + const isDataEmpty = useMemo(() => data.total === 0, [data]); + + return ( + <> + {!isEmpty(actionsErrors) && ( + + )} + + + + + + + + + + } + titleTooltip={!isEmpty(actionsErrors) ? actionsErrors[0].title : ''} + urlSearch={urlSearch} + /> + + + + {i18n.CREATE_TITLE} + + + + + {(isCasesLoading || isDeleting || isUpdating) && !isDataEmpty && ( + + )} + + + {isCasesLoading && isDataEmpty ? ( +
+ +
+ ) : ( +
+ + + + + {i18n.SHOWING_CASES(data.total ?? 0)} + + + + + {i18n.SHOWING_SELECTED_CASES(selectedCases.length)} + + {userCanCrud && ( + + {i18n.BULK_ACTIONS} + + )} + + {i18n.REFRESH} + + + + + {i18n.NO_CASES}} + titleSize="xs" + body={i18n.NO_CASES_BODY} + actions={ + + {i18n.ADD_NEW_CASE} + + } + /> + } + onChange={tableOnChangeCallback} + pagination={memoizedPagination} + selection={userCanCrud ? euiBasicTableSelectionProps : {}} + sorting={sorting} + /> +
+ )} +
+ {confirmDeleteModal} + + ); +}); + +AllCases.displayName = 'AllCases'; diff --git a/x-pack/plugins/siem/public/pages/case/components/all_cases/table_filters.test.tsx b/x-pack/plugins/siem/public/cases/components/all_cases/table_filters.test.tsx similarity index 90% rename from x-pack/plugins/siem/public/pages/case/components/all_cases/table_filters.test.tsx rename to x-pack/plugins/siem/public/cases/components/all_cases/table_filters.test.tsx index 21dcc9732440d3..05702e931fc253 100644 --- a/x-pack/plugins/siem/public/pages/case/components/all_cases/table_filters.test.tsx +++ b/x-pack/plugins/siem/public/cases/components/all_cases/table_filters.test.tsx @@ -8,14 +8,14 @@ import React from 'react'; import { mount } from 'enzyme'; import { CasesTableFilters } from './table_filters'; -import { TestProviders } from '../../../../mock'; +import { TestProviders } from '../../../common/mock'; -import { useGetTags } from '../../../../containers/case/use_get_tags'; -import { useGetReporters } from '../../../../containers/case/use_get_reporters'; -import { DEFAULT_FILTER_OPTIONS } from '../../../../containers/case/use_get_cases'; -jest.mock('../../../../components/timeline/insert_timeline_popover/use_insert_timeline'); -jest.mock('../../../../containers/case/use_get_reporters'); -jest.mock('../../../../containers/case/use_get_tags'); +import { useGetTags } from '../../containers/use_get_tags'; +import { useGetReporters } from '../../containers/use_get_reporters'; +import { DEFAULT_FILTER_OPTIONS } from '../../containers/use_get_cases'; +jest.mock('../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline'); +jest.mock('../../containers/use_get_reporters'); +jest.mock('../../containers/use_get_tags'); const onFilterChanged = jest.fn(); const fetchReporters = jest.fn(); diff --git a/x-pack/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx b/x-pack/plugins/siem/public/cases/components/all_cases/table_filters.tsx similarity index 94% rename from x-pack/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx rename to x-pack/plugins/siem/public/cases/components/all_cases/table_filters.tsx index 901fb133753e82..55713c201743a1 100644 --- a/x-pack/plugins/siem/public/pages/case/components/all_cases/table_filters.tsx +++ b/x-pack/plugins/siem/public/cases/components/all_cases/table_filters.tsx @@ -15,10 +15,10 @@ import { } from '@elastic/eui'; import * as i18n from './translations'; -import { FilterOptions } from '../../../../containers/case/types'; -import { useGetTags } from '../../../../containers/case/use_get_tags'; -import { useGetReporters } from '../../../../containers/case/use_get_reporters'; -import { FilterPopover } from '../../../../components/filter_popover'; +import { FilterOptions } from '../../containers/types'; +import { useGetTags } from '../../containers/use_get_tags'; +import { useGetReporters } from '../../containers/use_get_reporters'; +import { FilterPopover } from '../filter_popover'; interface CasesTableFiltersProps { countClosedCases: number | null; diff --git a/x-pack/plugins/siem/public/pages/case/components/all_cases/translations.ts b/x-pack/plugins/siem/public/cases/components/all_cases/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/case/components/all_cases/translations.ts rename to x-pack/plugins/siem/public/cases/components/all_cases/translations.ts diff --git a/x-pack/plugins/siem/public/pages/case/components/bulk_actions/index.tsx b/x-pack/plugins/siem/public/cases/components/bulk_actions/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/case/components/bulk_actions/index.tsx rename to x-pack/plugins/siem/public/cases/components/bulk_actions/index.tsx diff --git a/x-pack/plugins/siem/public/pages/case/components/bulk_actions/translations.ts b/x-pack/plugins/siem/public/cases/components/bulk_actions/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/case/components/bulk_actions/translations.ts rename to x-pack/plugins/siem/public/cases/components/bulk_actions/translations.ts diff --git a/x-pack/plugins/siem/public/pages/case/components/callout/helpers.tsx b/x-pack/plugins/siem/public/cases/components/callout/helpers.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/case/components/callout/helpers.tsx rename to x-pack/plugins/siem/public/cases/components/callout/helpers.tsx diff --git a/x-pack/plugins/siem/public/cases/components/callout/index.test.tsx b/x-pack/plugins/siem/public/cases/components/callout/index.test.tsx new file mode 100644 index 00000000000000..0ab90d8a731264 --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/callout/index.test.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mount } from 'enzyme'; + +import { CaseCallOut } from '.'; + +const defaultProps = { + title: 'hey title', +}; + +describe('CaseCallOut ', () => { + it('Renders single message callout', () => { + const props = { + ...defaultProps, + message: 'we have one message', + }; + const wrapper = mount(); + expect( + wrapper + .find(`[data-test-subj="callout-message"]`) + .last() + .exists() + ).toBeTruthy(); + expect( + wrapper + .find(`[data-test-subj="callout-messages"]`) + .last() + .exists() + ).toBeFalsy(); + }); + it('Renders multi message callout', () => { + const props = { + ...defaultProps, + messages: [ + { ...defaultProps, description:

{'we have two messages'}

}, + { ...defaultProps, description:

{'for real'}

}, + ], + }; + const wrapper = mount(); + expect( + wrapper + .find(`[data-test-subj="callout-message"]`) + .last() + .exists() + ).toBeFalsy(); + expect( + wrapper + .find(`[data-test-subj="callout-messages"]`) + .last() + .exists() + ).toBeTruthy(); + }); + it('Dismisses callout', () => { + const props = { + ...defaultProps, + message: 'we have one message', + }; + const wrapper = mount(); + expect(wrapper.find(`[data-test-subj="case-call-out"]`).exists()).toBeTruthy(); + wrapper + .find(`[data-test-subj="callout-dismiss"]`) + .last() + .simulate('click'); + expect(wrapper.find(`[data-test-subj="case-call-out"]`).exists()).toBeFalsy(); + }); +}); diff --git a/x-pack/plugins/siem/public/pages/case/components/callout/index.tsx b/x-pack/plugins/siem/public/cases/components/callout/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/case/components/callout/index.tsx rename to x-pack/plugins/siem/public/cases/components/callout/index.tsx diff --git a/x-pack/plugins/siem/public/pages/case/components/callout/translations.ts b/x-pack/plugins/siem/public/cases/components/callout/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/case/components/callout/translations.ts rename to x-pack/plugins/siem/public/cases/components/callout/translations.ts diff --git a/x-pack/plugins/siem/public/cases/components/case_header_page/index.tsx b/x-pack/plugins/siem/public/cases/components/case_header_page/index.tsx new file mode 100644 index 00000000000000..4c7cfabe757cf0 --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/case_header_page/index.tsx @@ -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 React from 'react'; + +import { HeaderPage, HeaderPageProps } from '../../../common/components/header_page'; +import * as i18n from './translations'; + +const CaseHeaderPageComponent: React.FC = props => ; + +CaseHeaderPageComponent.defaultProps = { + badgeOptions: { + beta: true, + text: i18n.PAGE_BADGE_LABEL, + tooltip: i18n.PAGE_BADGE_TOOLTIP, + }, +}; + +export const CaseHeaderPage = React.memo(CaseHeaderPageComponent); diff --git a/x-pack/plugins/siem/public/pages/case/components/case_header_page/translations.ts b/x-pack/plugins/siem/public/cases/components/case_header_page/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/case/components/case_header_page/translations.ts rename to x-pack/plugins/siem/public/cases/components/case_header_page/translations.ts diff --git a/x-pack/plugins/siem/public/cases/components/case_status/index.tsx b/x-pack/plugins/siem/public/cases/components/case_status/index.tsx new file mode 100644 index 00000000000000..a37c9052c2ff38 --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/case_status/index.tsx @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import styled, { css } from 'styled-components'; +import { + EuiBadge, + EuiButtonEmpty, + EuiButtonToggle, + EuiDescriptionList, + EuiDescriptionListDescription, + EuiDescriptionListTitle, + EuiFlexGroup, + EuiFlexItem, +} from '@elastic/eui'; +import * as i18n from '../case_view/translations'; +import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date'; +import { CaseViewActions } from '../case_view/actions'; +import { Case } from '../../containers/types'; +import { CaseService } from '../../containers/use_get_case_user_actions'; + +const MyDescriptionList = styled(EuiDescriptionList)` + ${({ theme }) => css` + & { + padding-right: ${theme.eui.euiSizeL}; + border-right: ${theme.eui.euiBorderThin}; + } + `} +`; + +interface CaseStatusProps { + 'data-test-subj': string; + badgeColor: string; + buttonLabel: string; + caseData: Case; + currentExternalIncident: CaseService | null; + disabled?: boolean; + icon: string; + isLoading: boolean; + isSelected: boolean; + onRefresh: () => void; + status: string; + title: string; + toggleStatusCase: (evt: unknown) => void; + value: string | null; +} +const CaseStatusComp: React.FC = ({ + 'data-test-subj': dataTestSubj, + badgeColor, + buttonLabel, + caseData, + currentExternalIncident, + disabled = false, + icon, + isLoading, + isSelected, + onRefresh, + status, + title, + toggleStatusCase, + value, +}) => ( + + + + + + {i18n.STATUS} + + + {status} + + + + + {title} + + + + + + + + + + + + {i18n.CASE_REFRESH} + + + + + + + + + + + +); + +export const CaseStatus = React.memo(CaseStatusComp); diff --git a/x-pack/plugins/siem/public/cases/components/case_view/actions.test.tsx b/x-pack/plugins/siem/public/cases/components/case_view/actions.test.tsx new file mode 100644 index 00000000000000..1f8d3230f42a84 --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/case_view/actions.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 { useDeleteCases } from '../../containers/use_delete_cases'; +import { TestProviders } from '../../../common/mock'; +import { basicCase, basicPush } from '../../containers/mock'; +import { CaseViewActions } from './actions'; +import * as i18n from './translations'; +jest.mock('../../containers/use_delete_cases'); +const useDeleteCasesMock = useDeleteCases as jest.Mock; + +describe('CaseView actions', () => { + const handleOnDeleteConfirm = jest.fn(); + const handleToggleModal = jest.fn(); + const dispatchResetIsDeleted = jest.fn(); + const defaultDeleteState = { + dispatchResetIsDeleted, + handleToggleModal, + handleOnDeleteConfirm, + isLoading: false, + isError: false, + isDeleted: false, + isDisplayConfirmDeleteModal: false, + }; + beforeEach(() => { + jest.resetAllMocks(); + useDeleteCasesMock.mockImplementation(() => defaultDeleteState); + }); + it('clicking trash toggles modal', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="confirm-delete-case-modal"]').exists()).toBeFalsy(); + + wrapper + .find('button[data-test-subj="property-actions-ellipses"]') + .first() + .simulate('click'); + wrapper.find('button[data-test-subj="property-actions-trash"]').simulate('click'); + expect(handleToggleModal).toHaveBeenCalled(); + }); + it('toggle delete modal and confirm', () => { + useDeleteCasesMock.mockImplementation(() => ({ + ...defaultDeleteState, + isDisplayConfirmDeleteModal: true, + })); + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="confirm-delete-case-modal"]').exists()).toBeTruthy(); + wrapper.find('button[data-test-subj="confirmModalConfirmButton"]').simulate('click'); + expect(handleOnDeleteConfirm.mock.calls[0][0]).toEqual([ + { id: basicCase.id, title: basicCase.title }, + ]); + }); + it('displays active incident link', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="confirm-delete-case-modal"]').exists()).toBeFalsy(); + + wrapper + .find('button[data-test-subj="property-actions-ellipses"]') + .first() + .simulate('click'); + expect( + wrapper + .find('[data-test-subj="property-actions-popout"]') + .first() + .prop('aria-label') + ).toEqual(i18n.VIEW_INCIDENT(basicPush.externalTitle)); + }); +}); diff --git a/x-pack/plugins/siem/public/cases/components/case_view/actions.tsx b/x-pack/plugins/siem/public/cases/components/case_view/actions.tsx new file mode 100644 index 00000000000000..cd9318a355e3c7 --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/case_view/actions.tsx @@ -0,0 +1,81 @@ +/* + * 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 { isEmpty } from 'lodash/fp'; +import React, { useMemo } from 'react'; +import { Redirect } from 'react-router-dom'; +import * as i18n from './translations'; +import { useDeleteCases } from '../../containers/use_delete_cases'; +import { ConfirmDeleteCaseModal } from '../confirm_delete_case'; +import { SiemPageName } from '../../../app/types'; +import { PropertyActions } from '../property_actions'; +import { Case } from '../../containers/types'; +import { CaseService } from '../../containers/use_get_case_user_actions'; + +interface CaseViewActions { + caseData: Case; + currentExternalIncident: CaseService | null; + disabled?: boolean; +} + +const CaseViewActionsComponent: React.FC = ({ + caseData, + currentExternalIncident, + disabled = false, +}) => { + // Delete case + const { + handleToggleModal, + handleOnDeleteConfirm, + isDeleted, + isDisplayConfirmDeleteModal, + } = useDeleteCases(); + + const confirmDeleteModal = useMemo( + () => ( + + ), + [isDisplayConfirmDeleteModal, caseData] + ); + const propertyActions = useMemo( + () => [ + { + disabled, + iconType: 'trash', + label: i18n.DELETE_CASE, + onClick: handleToggleModal, + }, + ...(currentExternalIncident != null && !isEmpty(currentExternalIncident?.externalUrl) + ? [ + { + iconType: 'popout', + label: i18n.VIEW_INCIDENT(currentExternalIncident?.externalTitle ?? ''), + onClick: () => window.open(currentExternalIncident?.externalUrl, '_blank'), + }, + ] + : []), + ], + [disabled, handleToggleModal, currentExternalIncident] + ); + + if (isDeleted) { + return ; + } + return ( + <> + + {confirmDeleteModal} + + ); +}; + +export const CaseViewActions = React.memo(CaseViewActionsComponent); diff --git a/x-pack/plugins/siem/public/cases/components/case_view/index.test.tsx b/x-pack/plugins/siem/public/cases/components/case_view/index.test.tsx new file mode 100644 index 00000000000000..70d2dc97f3f455 --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/case_view/index.test.tsx @@ -0,0 +1,432 @@ +/* + * 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 { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; +import { CaseComponent, CaseProps, CaseView } from '.'; +import { basicCase, basicCaseClosed, caseUserActions } from '../../containers/mock'; +import { TestProviders } from '../../../common/mock'; +import { useUpdateCase } from '../../containers/use_update_case'; +import { useGetCase } from '../../containers/use_get_case'; +import { useGetCaseUserActions } from '../../containers/use_get_case_user_actions'; +import { wait } from '../../../common/lib/helpers'; +import { usePushToService } from '../use_push_to_service'; +jest.mock('../../containers/use_update_case'); +jest.mock('../../containers/use_get_case_user_actions'); +jest.mock('../../containers/use_get_case'); +jest.mock('../use_push_to_service'); +const useUpdateCaseMock = useUpdateCase as jest.Mock; +const useGetCaseUserActionsMock = useGetCaseUserActions as jest.Mock; +const usePushToServiceMock = usePushToService as jest.Mock; + +export const caseProps: CaseProps = { + caseId: basicCase.id, + userCanCrud: true, + caseData: basicCase, + fetchCase: jest.fn(), + updateCase: jest.fn(), +}; + +export const caseClosedProps: CaseProps = { + ...caseProps, + caseData: basicCaseClosed, +}; + +describe('CaseView ', () => { + const updateCaseProperty = jest.fn(); + const fetchCaseUserActions = jest.fn(); + const fetchCase = jest.fn(); + const updateCase = jest.fn(); + const data = caseProps.caseData; + const defaultGetCase = { + isLoading: false, + isError: false, + data, + updateCase, + fetchCase, + }; + /* eslint-disable no-console */ + // Silence until enzyme fixed to use ReactTestUtils.act() + const originalError = console.error; + beforeAll(() => { + console.error = jest.fn(); + }); + afterAll(() => { + console.error = originalError; + }); + /* eslint-enable no-console */ + + const defaultUpdateCaseState = { + isLoading: false, + isError: false, + updateKey: null, + updateCaseProperty, + }; + + const defaultUseGetCaseUserActions = { + caseUserActions, + caseServices: {}, + fetchCaseUserActions, + firstIndexPushToService: -1, + hasDataToPush: false, + isLoading: false, + isError: false, + lastIndexPushToService: -1, + participants: [data.createdBy], + }; + + beforeEach(() => { + jest.resetAllMocks(); + useUpdateCaseMock.mockImplementation(() => defaultUpdateCaseState); + jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); + useGetCaseUserActionsMock.mockImplementation(() => defaultUseGetCaseUserActions); + usePushToServiceMock.mockImplementation(({ updateCase: updateCaseMockCall }) => ({ + pushButton: ( + + ), + pushCallouts: null, + })); + }); + + it('should render CaseComponent', async () => { + const wrapper = mount( + + + + + + ); + await wait(); + expect( + wrapper + .find(`[data-test-subj="case-view-title"]`) + .first() + .prop('title') + ).toEqual(data.title); + expect( + wrapper + .find(`[data-test-subj="case-view-status"]`) + .first() + .text() + ).toEqual(data.status); + expect( + wrapper + .find(`[data-test-subj="case-view-tag-list"] [data-test-subj="case-tag"]`) + .first() + .text() + ).toEqual(data.tags[0]); + expect( + wrapper + .find(`[data-test-subj="case-view-username"]`) + .first() + .text() + ).toEqual(data.createdBy.username); + expect(wrapper.contains(`[data-test-subj="case-view-closedAt"]`)).toBe(false); + expect( + wrapper + .find(`[data-test-subj="case-view-createdAt"]`) + .first() + .prop('value') + ).toEqual(data.createdAt); + expect( + wrapper + .find(`[data-test-subj="description-action"] [data-test-subj="user-action-markdown"]`) + .first() + .prop('raw') + ).toEqual(data.description); + }); + + it('should show closed indicators in header when case is closed', async () => { + useUpdateCaseMock.mockImplementation(() => ({ + ...defaultUpdateCaseState, + caseData: basicCaseClosed, + })); + const wrapper = mount( + + + + + + ); + await wait(); + expect(wrapper.contains(`[data-test-subj="case-view-createdAt"]`)).toBe(false); + expect( + wrapper + .find(`[data-test-subj="case-view-closedAt"]`) + .first() + .prop('value') + ).toEqual(basicCaseClosed.closedAt); + expect( + wrapper + .find(`[data-test-subj="case-view-status"]`) + .first() + .text() + ).toEqual(basicCaseClosed.status); + }); + + it('should dispatch update state when button is toggled', async () => { + const wrapper = mount( + + + + + + ); + await wait(); + wrapper + .find('input[data-test-subj="toggle-case-status"]') + .simulate('change', { target: { checked: true } }); + expect(updateCaseProperty).toHaveBeenCalled(); + }); + + it('should display EditableTitle isLoading', () => { + useUpdateCaseMock.mockImplementation(() => ({ + ...defaultUpdateCaseState, + isLoading: true, + updateKey: 'title', + })); + const wrapper = mount( + + + + + + ); + expect( + wrapper + .find('[data-test-subj="editable-title-loading"]') + .first() + .exists() + ).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="editable-title-edit-icon"]') + .first() + .exists() + ).toBeFalsy(); + }); + + it('should display Toggle Status isLoading', () => { + useUpdateCaseMock.mockImplementation(() => ({ + ...defaultUpdateCaseState, + isLoading: true, + updateKey: 'status', + })); + const wrapper = mount( + + + + + + ); + expect( + wrapper + .find('[data-test-subj="toggle-case-status"]') + .first() + .prop('isLoading') + ).toBeTruthy(); + }); + + it('should display description isLoading', () => { + useUpdateCaseMock.mockImplementation(() => ({ + ...defaultUpdateCaseState, + isLoading: true, + updateKey: 'description', + })); + const wrapper = mount( + + + + + + ); + expect( + wrapper + .find('[data-test-subj="description-action"] [data-test-subj="user-action-title-loading"]') + .first() + .exists() + ).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="description-action"] [data-test-subj="property-actions"]') + .first() + .exists() + ).toBeFalsy(); + }); + + it('should display tags isLoading', () => { + useUpdateCaseMock.mockImplementation(() => ({ + ...defaultUpdateCaseState, + isLoading: true, + updateKey: 'tags', + })); + const wrapper = mount( + + + + + + ); + expect( + wrapper + .find('[data-test-subj="case-view-tag-list"] [data-test-subj="tag-list-loading"]') + .first() + .exists() + ).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="tag-list-edit"]') + .first() + .exists() + ).toBeFalsy(); + }); + + it('should update title', () => { + const wrapper = mount( + + + + + + ); + const newTitle = 'The new title'; + wrapper + .find(`[data-test-subj="editable-title-edit-icon"]`) + .first() + .simulate('click'); + wrapper.update(); + wrapper + .find(`[data-test-subj="editable-title-input-field"]`) + .last() + .simulate('change', { target: { value: newTitle } }); + + wrapper.update(); + wrapper + .find(`[data-test-subj="editable-title-submit-btn"]`) + .first() + .simulate('click'); + + wrapper.update(); + const updateObject = updateCaseProperty.mock.calls[0][0]; + expect(updateObject.updateKey).toEqual('title'); + expect(updateObject.updateValue).toEqual(newTitle); + }); + + it('should push updates on button click', async () => { + useGetCaseUserActionsMock.mockImplementation(() => ({ + ...defaultUseGetCaseUserActions, + hasDataToPush: true, + })); + const wrapper = mount( + + + + + + ); + expect( + wrapper + .find('[data-test-subj="has-data-to-push-button"]') + .first() + .exists() + ).toBeTruthy(); + wrapper + .find('[data-test-subj="mock-button"]') + .first() + .simulate('click'); + wrapper.update(); + await wait(); + expect(updateCase).toBeCalledWith(caseProps.caseData); + expect(fetchCaseUserActions).toBeCalledWith(caseProps.caseData.id); + }); + + it('should return null if error', () => { + (useGetCase as jest.Mock).mockImplementation(() => ({ + ...defaultGetCase, + isError: true, + })); + const wrapper = mount( + + + + + + ); + expect(wrapper).toEqual({}); + }); + + it('should return spinner if loading', () => { + (useGetCase as jest.Mock).mockImplementation(() => ({ + ...defaultGetCase, + isLoading: true, + })); + const wrapper = mount( + + + + + + ); + expect(wrapper.find('[data-test-subj="case-view-loading"]').exists()).toBeTruthy(); + }); + + it('should return case view when data is there', () => { + (useGetCase as jest.Mock).mockImplementation(() => defaultGetCase); + const wrapper = mount( + + + + + + ); + expect(wrapper.find('[data-test-subj="case-view-title"]').exists()).toBeTruthy(); + }); + + it('should refresh data on refresh', () => { + (useGetCase as jest.Mock).mockImplementation(() => defaultGetCase); + const wrapper = mount( + + + + + + ); + wrapper + .find('[data-test-subj="case-refresh"]') + .first() + .simulate('click'); + expect(fetchCaseUserActions).toBeCalledWith(caseProps.caseData.id); + expect(fetchCase).toBeCalled(); + }); +}); diff --git a/x-pack/plugins/siem/public/cases/components/case_view/index.tsx b/x-pack/plugins/siem/public/cases/components/case_view/index.tsx new file mode 100644 index 00000000000000..d02119580a75ab --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/case_view/index.tsx @@ -0,0 +1,382 @@ +/* + * 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 { + EuiButtonToggle, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingContent, + EuiLoadingSpinner, + EuiHorizontalRule, +} from '@elastic/eui'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import styled from 'styled-components'; + +import * as i18n from './translations'; +import { Case } from '../../containers/types'; +import { getCaseUrl } from '../../../common/components/link_to'; +import { HeaderPage } from '../../../common/components/header_page'; +import { EditableTitle } from '../../../common/components/header_page/editable_title'; +import { TagList } from '../tag_list'; +import { useGetCase } from '../../containers/use_get_case'; +import { UserActionTree } from '../user_action_tree'; +import { UserList } from '../user_list'; +import { useUpdateCase } from '../../containers/use_update_case'; +import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search'; +import { WrapperPage } from '../../../common/components/wrapper_page'; +import { getTypedPayload } from '../../containers/utils'; +import { WhitePageWrapper } from '../wrappers'; +import { useBasePath } from '../../../common/lib/kibana'; +import { CaseStatus } from '../case_status'; +import { navTabs } from '../../../app/home/home_navigations'; +import { SpyRoute } from '../../../common/utils/route/spy_routes'; +import { useGetCaseUserActions } from '../../containers/use_get_case_user_actions'; +import { usePushToService } from '../use_push_to_service'; +import { EditConnector } from '../edit_connector'; +import { useConnectors } from '../../containers/configure/use_connectors'; + +interface Props { + caseId: string; + userCanCrud: boolean; +} + +const MyWrapper = styled(WrapperPage)` + padding-bottom: 0; +`; + +const MyEuiFlexGroup = styled(EuiFlexGroup)` + height: 100%; +`; + +const MyEuiHorizontalRule = styled(EuiHorizontalRule)` + margin-left: 48px; + &.euiHorizontalRule--full { + width: calc(100% - 48px); + } +`; + +export interface CaseProps extends Props { + fetchCase: () => void; + caseData: Case; + updateCase: (newCase: Case) => void; +} + +export const CaseComponent = React.memo( + ({ caseId, caseData, fetchCase, updateCase, userCanCrud }) => { + const basePath = window.location.origin + useBasePath(); + const caseLink = `${basePath}/app/siem#/case/${caseId}`; + const search = useGetUrlSearch(navTabs.case); + const [initLoadingData, setInitLoadingData] = useState(true); + const { + caseUserActions, + fetchCaseUserActions, + caseServices, + hasDataToPush, + isLoading: isLoadingUserActions, + participants, + } = useGetCaseUserActions(caseId, caseData.connectorId); + const { isLoading, updateKey, updateCaseProperty } = useUpdateCase({ + caseId, + }); + + // Update Fields + const onUpdateField = useCallback( + (newUpdateKey: keyof Case, updateValue: Case[keyof Case]) => { + const handleUpdateNewCase = (newCase: Case) => + updateCase({ ...newCase, comments: caseData.comments }); + switch (newUpdateKey) { + case 'title': + const titleUpdate = getTypedPayload(updateValue); + if (titleUpdate.length > 0) { + updateCaseProperty({ + fetchCaseUserActions, + updateKey: 'title', + updateValue: titleUpdate, + updateCase: handleUpdateNewCase, + version: caseData.version, + }); + } + break; + case 'connectorId': + const connectorId = getTypedPayload(updateValue); + if (connectorId.length > 0) { + updateCaseProperty({ + fetchCaseUserActions, + updateKey: 'connector_id', + updateValue: connectorId, + updateCase: handleUpdateNewCase, + version: caseData.version, + }); + } + break; + case 'description': + const descriptionUpdate = getTypedPayload(updateValue); + if (descriptionUpdate.length > 0) { + updateCaseProperty({ + fetchCaseUserActions, + updateKey: 'description', + updateValue: descriptionUpdate, + updateCase: handleUpdateNewCase, + version: caseData.version, + }); + } + break; + case 'tags': + const tagsUpdate = getTypedPayload(updateValue); + updateCaseProperty({ + fetchCaseUserActions, + updateKey: 'tags', + updateValue: tagsUpdate, + updateCase: handleUpdateNewCase, + version: caseData.version, + }); + break; + case 'status': + const statusUpdate = getTypedPayload(updateValue); + if (caseData.status !== updateValue) { + updateCaseProperty({ + fetchCaseUserActions, + updateKey: 'status', + updateValue: statusUpdate, + updateCase: handleUpdateNewCase, + version: caseData.version, + }); + } + default: + return null; + } + }, + [fetchCaseUserActions, updateCaseProperty, updateCase, caseData] + ); + const handleUpdateCase = useCallback( + (newCase: Case) => { + updateCase(newCase); + fetchCaseUserActions(newCase.id); + }, + [updateCase, fetchCaseUserActions] + ); + + const { loading: isLoadingConnectors, connectors } = useConnectors(); + const caseConnectorName = useMemo( + () => connectors.find(c => c.id === caseData.connectorId)?.name ?? 'none', + [connectors, caseData.connectorId] + ); + + const currentExternalIncident = useMemo( + () => + caseServices != null && caseServices[caseData.connectorId] != null + ? caseServices[caseData.connectorId] + : null, + [caseServices, caseData.connectorId] + ); + + const { pushButton, pushCallouts } = usePushToService({ + caseConnectorId: caseData.connectorId, + caseConnectorName, + caseServices, + caseId: caseData.id, + caseStatus: caseData.status, + connectors, + updateCase: handleUpdateCase, + userCanCrud, + }); + + const onSubmitConnector = useCallback( + connectorId => onUpdateField('connectorId', connectorId), + [onUpdateField] + ); + const onSubmitTags = useCallback(newTags => onUpdateField('tags', newTags), [onUpdateField]); + const onSubmitTitle = useCallback(newTitle => onUpdateField('title', newTitle), [ + onUpdateField, + ]); + const toggleStatusCase = useCallback( + e => onUpdateField('status', e.target.checked ? 'closed' : 'open'), + [onUpdateField] + ); + const handleRefresh = useCallback(() => { + fetchCaseUserActions(caseData.id); + fetchCase(); + }, [caseData.id, fetchCase, fetchCaseUserActions]); + + const spyState = useMemo(() => ({ caseTitle: caseData.title }), [caseData.title]); + + const caseStatusData = useMemo( + () => + caseData.status === 'open' + ? { + 'data-test-subj': 'case-view-createdAt', + value: caseData.createdAt, + title: i18n.CASE_OPENED, + buttonLabel: i18n.CLOSE_CASE, + status: caseData.status, + icon: 'folderCheck', + badgeColor: 'secondary', + isSelected: false, + } + : { + 'data-test-subj': 'case-view-closedAt', + value: caseData.closedAt ?? '', + title: i18n.CASE_CLOSED, + buttonLabel: i18n.REOPEN_CASE, + status: caseData.status, + icon: 'folderExclamation', + badgeColor: 'danger', + isSelected: true, + }, + [caseData.closedAt, caseData.createdAt, caseData.status] + ); + const emailContent = useMemo( + () => ({ + subject: i18n.EMAIL_SUBJECT(caseData.title), + body: i18n.EMAIL_BODY(caseLink), + }), + [caseLink, caseData.title] + ); + + useEffect(() => { + if (initLoadingData && !isLoadingUserActions) { + setInitLoadingData(false); + } + }, [initLoadingData, isLoadingUserActions]); + + return ( + <> + + + } + title={caseData.title} + > + + + + + + {!initLoadingData && pushCallouts != null && pushCallouts} + + + {initLoadingData && } + {!initLoadingData && ( + <> + + + + + + + {hasDataToPush && ( + + {pushButton} + + )} + + + )} + + + + + + + + + + + + + ); + } +); + +export const CaseView = React.memo(({ caseId, userCanCrud }: Props) => { + const { data, isLoading, isError, fetchCase, updateCase } = useGetCase(caseId); + if (isError) { + return null; + } + if (isLoading) { + return ( + + + + + + ); + } + + return ( + + ); +}); + +CaseComponent.displayName = 'CaseComponent'; +CaseView.displayName = 'CaseView'; diff --git a/x-pack/plugins/siem/public/pages/case/components/case_view/translations.ts b/x-pack/plugins/siem/public/cases/components/case_view/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/case/components/case_view/translations.ts rename to x-pack/plugins/siem/public/cases/components/case_view/translations.ts diff --git a/x-pack/plugins/siem/public/cases/components/configure_cases/__mock__/index.tsx b/x-pack/plugins/siem/public/cases/components/configure_cases/__mock__/index.tsx new file mode 100644 index 00000000000000..23c76953a6a0f9 --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/configure_cases/__mock__/index.tsx @@ -0,0 +1,55 @@ +/* + * 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 { Connector } from '../../../containers/configure/types'; +import { ReturnConnectors } from '../../../containers/configure/use_connectors'; +import { connectorsMock } from '../../../containers/configure/mock'; +import { ReturnUseCaseConfigure } from '../../../containers/configure/use_configure'; +import { createUseKibanaMock } from '../../../../common/mock/kibana_react'; +export { mapping } from '../../../containers/configure/mock'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { actionTypeRegistryMock } from '../../../../../../triggers_actions_ui/public/application/action_type_registry.mock'; + +export const connectors: Connector[] = connectorsMock; + +// x - pack / plugins / triggers_actions_ui; +export const searchURL = + '?timerange=(global:(linkTo:!(),timerange:(from:1585487656371,fromStr:now-24h,kind:relative,to:1585574056371,toStr:now)),timeline:(linkTo:!(),timerange:(from:1585227005527,kind:absolute,to:1585313405527)))'; + +export const useCaseConfigureResponse: ReturnUseCaseConfigure = { + closureType: 'close-by-user', + connectorId: 'none', + connectorName: 'none', + currentConfiguration: { + connectorId: 'none', + closureType: 'close-by-user', + connectorName: 'none', + }, + firstLoad: false, + loading: false, + mapping: null, + persistCaseConfigure: jest.fn(), + persistLoading: false, + refetchCaseConfigure: jest.fn(), + setClosureType: jest.fn(), + setConnector: jest.fn(), + setCurrentConfiguration: jest.fn(), + setMapping: jest.fn(), + version: '', +}; + +export const useConnectorsResponse: ReturnConnectors = { + loading: false, + connectors, + refetchConnectors: jest.fn(), +}; + +export const kibanaMockImplementationArgs = { + services: { + ...createUseKibanaMock()().services, + triggers_actions_ui: { actionTypeRegistry: actionTypeRegistryMock.create() }, + }, +}; diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/button.test.tsx b/x-pack/plugins/siem/public/cases/components/configure_cases/button.test.tsx similarity index 98% rename from x-pack/plugins/siem/public/pages/case/components/configure_cases/button.test.tsx rename to x-pack/plugins/siem/public/cases/components/configure_cases/button.test.tsx index cf52fef94ed17e..550b9bd9896a3b 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/button.test.tsx +++ b/x-pack/plugins/siem/public/cases/components/configure_cases/button.test.tsx @@ -9,7 +9,7 @@ import { ReactWrapper, mount } from 'enzyme'; import { EuiText } from '@elastic/eui'; import { ConfigureCaseButton, ConfigureCaseButtonProps } from './button'; -import { TestProviders } from '../../../../mock'; +import { TestProviders } from '../../../common/mock'; import { searchURL } from './__mock__'; describe('Configuration button', () => { diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/button.tsx b/x-pack/plugins/siem/public/cases/components/configure_cases/button.tsx similarity index 94% rename from x-pack/plugins/siem/public/pages/case/components/configure_cases/button.tsx rename to x-pack/plugins/siem/public/cases/components/configure_cases/button.tsx index 844ffea28415f6..a6d78d4a2a6202 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/button.tsx +++ b/x-pack/plugins/siem/public/cases/components/configure_cases/button.tsx @@ -6,7 +6,7 @@ import { EuiButton, EuiToolTip } from '@elastic/eui'; import React, { memo, useMemo } from 'react'; -import { getConfigureCasesUrl } from '../../../../components/link_to'; +import { getConfigureCasesUrl } from '../../../common/components/link_to'; export interface ConfigureCaseButtonProps { label: string; diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/closure_options.test.tsx b/x-pack/plugins/siem/public/cases/components/configure_cases/closure_options.test.tsx similarity index 97% rename from x-pack/plugins/siem/public/pages/case/components/configure_cases/closure_options.test.tsx rename to x-pack/plugins/siem/public/cases/components/configure_cases/closure_options.test.tsx index eaef524b13da86..6192fd0ee9fff2 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/closure_options.test.tsx +++ b/x-pack/plugins/siem/public/cases/components/configure_cases/closure_options.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { mount, ReactWrapper } from 'enzyme'; import { ClosureOptions, ClosureOptionsProps } from './closure_options'; -import { TestProviders } from '../../../../mock'; +import { TestProviders } from '../../../common/mock'; import { ClosureOptionsRadio } from './closure_options_radio'; describe('ClosureOptions', () => { diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/closure_options.tsx b/x-pack/plugins/siem/public/cases/components/configure_cases/closure_options.tsx similarity index 95% rename from x-pack/plugins/siem/public/pages/case/components/configure_cases/closure_options.tsx rename to x-pack/plugins/siem/public/cases/components/configure_cases/closure_options.tsx index 6fa97818dd0ce3..b845b423449ea2 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/closure_options.tsx +++ b/x-pack/plugins/siem/public/cases/components/configure_cases/closure_options.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; -import { ClosureType } from '../../../../containers/case/configure/types'; +import { ClosureType } from '../../containers/configure/types'; import { ClosureOptionsRadio } from './closure_options_radio'; import * as i18n from './translations'; diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.test.tsx b/x-pack/plugins/siem/public/cases/components/configure_cases/closure_options_radio.test.tsx similarity index 97% rename from x-pack/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.test.tsx rename to x-pack/plugins/siem/public/cases/components/configure_cases/closure_options_radio.test.tsx index f2ef2c2d55c288..dae2204bc46653 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.test.tsx +++ b/x-pack/plugins/siem/public/cases/components/configure_cases/closure_options_radio.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { ReactWrapper, mount } from 'enzyme'; import { ClosureOptionsRadio, ClosureOptionsRadioComponentProps } from './closure_options_radio'; -import { TestProviders } from '../../../../mock'; +import { TestProviders } from '../../../common/mock'; describe('ClosureOptionsRadio', () => { let wrapper: ReactWrapper; diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.tsx b/x-pack/plugins/siem/public/cases/components/configure_cases/closure_options_radio.tsx similarity index 95% rename from x-pack/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.tsx rename to x-pack/plugins/siem/public/cases/components/configure_cases/closure_options_radio.tsx index d2cdb7ecda7ba4..673c8fbcc70d0f 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/closure_options_radio.tsx +++ b/x-pack/plugins/siem/public/cases/components/configure_cases/closure_options_radio.tsx @@ -7,7 +7,7 @@ import React, { ReactNode, useCallback } from 'react'; import { EuiRadioGroup } from '@elastic/eui'; -import { ClosureType } from '../../../../containers/case/configure/types'; +import { ClosureType } from '../../containers/configure/types'; import * as i18n from './translations'; interface ClosureRadios { diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors.test.tsx b/x-pack/plugins/siem/public/cases/components/configure_cases/connectors.test.tsx similarity index 98% rename from x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors.test.tsx rename to x-pack/plugins/siem/public/cases/components/configure_cases/connectors.test.tsx index b0271f6849ac58..41cd3e549415d5 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors.test.tsx +++ b/x-pack/plugins/siem/public/cases/components/configure_cases/connectors.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { mount, ReactWrapper } from 'enzyme'; import { Connectors, Props } from './connectors'; -import { TestProviders } from '../../../../mock'; +import { TestProviders } from '../../../common/mock'; import { ConnectorsDropdown } from './connectors_dropdown'; import { connectors } from './__mock__'; diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx b/x-pack/plugins/siem/public/cases/components/configure_cases/connectors.tsx similarity index 97% rename from x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx rename to x-pack/plugins/siem/public/cases/components/configure_cases/connectors.tsx index 1b1439d3bac438..3916ce297a0a47 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors.tsx +++ b/x-pack/plugins/siem/public/cases/components/configure_cases/connectors.tsx @@ -19,7 +19,7 @@ import styled from 'styled-components'; import { ConnectorsDropdown } from './connectors_dropdown'; import * as i18n from './translations'; -import { Connector } from '../../../../containers/case/configure/types'; +import { Connector } from '../../containers/configure/types'; const EuiFormRowExtended = styled(EuiFormRow)` .euiFormRow__labelWrapper { diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.test.tsx b/x-pack/plugins/siem/public/cases/components/configure_cases/connectors_dropdown.test.tsx similarity index 97% rename from x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.test.tsx rename to x-pack/plugins/siem/public/cases/components/configure_cases/connectors_dropdown.test.tsx index 6abe4f1ac00adb..da20078dde0d01 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.test.tsx +++ b/x-pack/plugins/siem/public/cases/components/configure_cases/connectors_dropdown.test.tsx @@ -9,7 +9,7 @@ import { mount, ReactWrapper } from 'enzyme'; import { EuiSuperSelect } from '@elastic/eui'; import { ConnectorsDropdown, Props } from './connectors_dropdown'; -import { TestProviders } from '../../../../mock'; +import { TestProviders } from '../../../common/mock'; import { connectors } from './__mock__'; describe('ConnectorsDropdown', () => { diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx b/x-pack/plugins/siem/public/cases/components/configure_cases/connectors_dropdown.tsx similarity index 94% rename from x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx rename to x-pack/plugins/siem/public/cases/components/configure_cases/connectors_dropdown.tsx index 2f73c8c5dba058..b2b2edb04bd296 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/connectors_dropdown.tsx +++ b/x-pack/plugins/siem/public/cases/components/configure_cases/connectors_dropdown.tsx @@ -8,8 +8,8 @@ import React, { useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSuperSelect } from '@elastic/eui'; import styled from 'styled-components'; -import { Connector } from '../../../../containers/case/configure/types'; -import { connectorsConfiguration } from '../../../../lib/connectors/config'; +import { Connector } from '../../containers/configure/types'; +import { connectorsConfiguration } from '../../../common/lib/connectors/config'; import * as i18n from './translations'; export interface Props { diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/field_mapping.test.tsx b/x-pack/plugins/siem/public/cases/components/configure_cases/field_mapping.test.tsx similarity index 93% rename from x-pack/plugins/siem/public/pages/case/components/configure_cases/field_mapping.test.tsx rename to x-pack/plugins/siem/public/cases/components/configure_cases/field_mapping.test.tsx index 498757a34b78d2..7f9ad877066939 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/field_mapping.test.tsx +++ b/x-pack/plugins/siem/public/cases/components/configure_cases/field_mapping.test.tsx @@ -7,13 +7,13 @@ import React from 'react'; import { mount, ReactWrapper } from 'enzyme'; -import { connectorsConfiguration } from '../../../../lib/connectors/config'; -import { createDefaultMapping } from '../../../../lib/connectors/utils'; +import { connectorsConfiguration } from '../../../common/lib/connectors/config'; +import { createDefaultMapping } from '../../../common/lib/connectors/utils'; import { FieldMapping, FieldMappingProps } from './field_mapping'; import { mapping } from './__mock__'; import { FieldMappingRow } from './field_mapping_row'; -import { TestProviders } from '../../../../mock'; +import { TestProviders } from '../../../common/mock'; describe('FieldMappingRow', () => { let wrapper: ReactWrapper; diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/field_mapping.tsx b/x-pack/plugins/siem/public/cases/components/configure_cases/field_mapping.tsx similarity index 94% rename from x-pack/plugins/siem/public/pages/case/components/configure_cases/field_mapping.tsx rename to x-pack/plugins/siem/public/cases/components/configure_cases/field_mapping.tsx index 41a6fbca3c0072..0eab690915f40a 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/field_mapping.tsx +++ b/x-pack/plugins/siem/public/cases/components/configure_cases/field_mapping.tsx @@ -13,17 +13,17 @@ import { CaseField, ActionType, ThirdPartyField, -} from '../../../../containers/case/configure/types'; +} from '../../containers/configure/types'; import { FieldMappingRow } from './field_mapping_row'; import * as i18n from './translations'; -import { connectorsConfiguration } from '../../../../lib/connectors/config'; +import { connectorsConfiguration } from '../../../common/lib/connectors/config'; import { setActionTypeToMapping, setThirdPartyToMapping } from './utils'; import { ThirdPartyField as ConnectorConfigurationThirdPartyField, AllThirdPartyFields, -} from '../../../../lib/connectors/types'; -import { createDefaultMapping } from '../../../../lib/connectors/utils'; +} from '../../../common/lib/connectors/types'; +import { createDefaultMapping } from '../../../common/lib/connectors/utils'; const FieldRowWrapper = styled.div` margin-top: 8px; diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/field_mapping_row.test.tsx b/x-pack/plugins/siem/public/cases/components/configure_cases/field_mapping_row.test.tsx similarity index 95% rename from x-pack/plugins/siem/public/pages/case/components/configure_cases/field_mapping_row.test.tsx rename to x-pack/plugins/siem/public/cases/components/configure_cases/field_mapping_row.test.tsx index e30096cc7eb62a..4d0401fdf1bfd7 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/field_mapping_row.test.tsx +++ b/x-pack/plugins/siem/public/cases/components/configure_cases/field_mapping_row.test.tsx @@ -9,8 +9,8 @@ import { mount, ReactWrapper } from 'enzyme'; import { EuiSuperSelectOption, EuiSuperSelect } from '@elastic/eui'; import { FieldMappingRow, RowProps } from './field_mapping_row'; -import { TestProviders } from '../../../../mock'; -import { ThirdPartyField, ActionType } from '../../../../containers/case/configure/types'; +import { TestProviders } from '../../../common/mock'; +import { ThirdPartyField, ActionType } from '../../containers/configure/types'; const thirdPartyOptions: Array> = [ { diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/field_mapping_row.tsx b/x-pack/plugins/siem/public/cases/components/configure_cases/field_mapping_row.tsx similarity index 92% rename from x-pack/plugins/siem/public/pages/case/components/configure_cases/field_mapping_row.tsx rename to x-pack/plugins/siem/public/cases/components/configure_cases/field_mapping_row.tsx index 687b0517326eb0..922ea7222efce3 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/field_mapping_row.tsx +++ b/x-pack/plugins/siem/public/cases/components/configure_cases/field_mapping_row.tsx @@ -14,13 +14,8 @@ import { } from '@elastic/eui'; import { capitalize } from 'lodash/fp'; - -import { - CaseField, - ActionType, - ThirdPartyField, -} from '../../../../containers/case/configure/types'; -import { AllThirdPartyFields } from '../../../../lib/connectors/types'; +import { CaseField, ActionType, ThirdPartyField } from '../../containers/configure/types'; +import { AllThirdPartyFields } from '../../../common/lib/connectors/types'; export interface RowProps { id: string; diff --git a/x-pack/plugins/siem/public/cases/components/configure_cases/index.test.tsx b/x-pack/plugins/siem/public/cases/components/configure_cases/index.test.tsx new file mode 100644 index 00000000000000..fcacb6dedff7d1 --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/configure_cases/index.test.tsx @@ -0,0 +1,860 @@ +/* + * 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 { ReactWrapper, mount } from 'enzyme'; + +import { ConfigureCases } from '.'; +import { TestProviders } from '../../../common/mock'; +import { Connectors } from './connectors'; +import { ClosureOptions } from './closure_options'; +import { + ActionsConnectorsContextProvider, + ConnectorAddFlyout, + ConnectorEditFlyout, +} from '../../../../../triggers_actions_ui/public'; + +import { useKibana } from '../../../common/lib/kibana'; +import { useConnectors } from '../../containers/configure/use_connectors'; +import { useCaseConfigure } from '../../containers/configure/use_configure'; +import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search'; + +import { + connectors, + searchURL, + useCaseConfigureResponse, + useConnectorsResponse, + kibanaMockImplementationArgs, +} from './__mock__'; + +jest.mock('../../../common/lib/kibana'); +jest.mock('../../containers/configure/use_connectors'); +jest.mock('../../containers/configure/use_configure'); +jest.mock('../../../common/components/navigation/use_get_url_search'); + +const useKibanaMock = useKibana as jest.Mock; +const useConnectorsMock = useConnectors as jest.Mock; +const useCaseConfigureMock = useCaseConfigure as jest.Mock; +const useGetUrlSearchMock = useGetUrlSearch as jest.Mock; +describe('ConfigureCases', () => { + describe('rendering', () => { + let wrapper: ReactWrapper; + beforeEach(() => { + jest.resetAllMocks(); + useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse); + useConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, connectors: [] })); + useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); + useGetUrlSearchMock.mockImplementation(() => searchURL); + + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it renders the Connectors', () => { + expect(wrapper.find('[data-test-subj="dropdown-connectors"]').exists()).toBeTruthy(); + }); + + test('it renders the ClosureType', () => { + expect(wrapper.find('[data-test-subj="closure-options-radio-group"]').exists()).toBeTruthy(); + }); + + test('it renders the ActionsConnectorsContextProvider', () => { + // Components from triggers_actions_ui do not have a data-test-subj + expect(wrapper.find(ActionsConnectorsContextProvider).exists()).toBeTruthy(); + }); + + test('it renders the ConnectorAddFlyout', () => { + // Components from triggers_actions_ui do not have a data-test-subj + expect(wrapper.find(ConnectorAddFlyout).exists()).toBeTruthy(); + }); + + test('it does NOT render the ConnectorEditFlyout', () => { + // Components from triggers_actions_ui do not have a data-test-subj + expect(wrapper.find(ConnectorEditFlyout).exists()).toBeFalsy(); + }); + + test('it does NOT render the EuiCallOut', () => { + expect( + wrapper.find('[data-test-subj="configure-cases-warning-callout"]').exists() + ).toBeFalsy(); + }); + + test('it does NOT render the EuiBottomBar', () => { + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeFalsy(); + }); + + test('it disables correctly ClosureOptions when the connector is set to none', () => { + expect(wrapper.find(ClosureOptions).prop('disabled')).toBe(true); + }); + }); + + describe('Unhappy path', () => { + let wrapper: ReactWrapper; + beforeEach(() => { + jest.resetAllMocks(); + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + closureType: 'close-by-user', + connectorId: 'not-id', + connectorName: 'unchanged', + currentConfiguration: { + connectorName: 'unchanged', + connectorId: 'not-id', + closureType: 'close-by-user', + }, + })); + useConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, connectors: [] })); + useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); + useGetUrlSearchMock.mockImplementation(() => searchURL); + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it shows the warning callout when configuration is invalid', () => { + expect( + wrapper.find('[data-test-subj="configure-cases-warning-callout"]').exists() + ).toBeTruthy(); + }); + + test('it hides the update connector button when the connectorId is invalid', () => { + expect( + wrapper + .find('button[data-test-subj="case-configure-update-selected-connector-button"]') + .exists() + ).toBeFalsy(); + }); + }); + + describe('Happy path', () => { + let wrapper: ReactWrapper; + + beforeEach(() => { + jest.resetAllMocks(); + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping: connectors[0].config.casesConfiguration.mapping, + closureType: 'close-by-user', + connectorId: 'servicenow-1', + connectorName: 'unchanged', + currentConfiguration: { + connectorName: 'unchanged', + connectorId: 'servicenow-1', + closureType: 'close-by-user', + }, + })); + useConnectorsMock.mockImplementation(() => useConnectorsResponse); + useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); + useGetUrlSearchMock.mockImplementation(() => searchURL); + + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it renders the ConnectorEditFlyout', () => { + expect(wrapper.find(ConnectorEditFlyout).exists()).toBeTruthy(); + }); + + test('it renders with correct props', () => { + // Connector + expect(wrapper.find(Connectors).prop('connectors')).toEqual(connectors); + expect(wrapper.find(Connectors).prop('disabled')).toBe(false); + expect(wrapper.find(Connectors).prop('isLoading')).toBe(false); + expect(wrapper.find(Connectors).prop('selectedConnector')).toBe('servicenow-1'); + + // ClosureOptions + expect(wrapper.find(ClosureOptions).prop('disabled')).toBe(false); + expect(wrapper.find(ClosureOptions).prop('closureTypeSelected')).toBe('close-by-user'); + + // Flyouts + expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(false); + expect(wrapper.find(ConnectorAddFlyout).prop('actionTypes')).toEqual([ + expect.objectContaining({ + id: '.servicenow', + }), + expect.objectContaining({ + id: '.jira', + }), + ]); + + expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(false); + expect(wrapper.find(ConnectorEditFlyout).prop('initialConnector')).toEqual(connectors[0]); + }); + + test('it does not shows the action bar when there is no change', () => { + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeFalsy(); + }); + + test('it disables correctly when the user cannot crud', () => { + const newWrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect(newWrapper.find('button[data-test-subj="dropdown-connectors"]').prop('disabled')).toBe( + true + ); + + expect( + newWrapper + .find('button[data-test-subj="case-configure-add-connector-button"]') + .prop('disabled') + ).toBe(true); + + expect( + newWrapper + .find('button[data-test-subj="case-configure-update-selected-connector-button"]') + .prop('disabled') + ).toBe(true); + + // Two closure options + expect( + newWrapper + .find('[data-test-subj="closure-options-radio-group"] input') + .first() + .prop('disabled') + ).toBe(true); + + expect( + newWrapper + .find('[data-test-subj="closure-options-radio-group"] input') + .at(1) + .prop('disabled') + ).toBe(true); + }); + }); + + describe('loading connectors', () => { + let wrapper: ReactWrapper; + + beforeEach(() => { + jest.resetAllMocks(); + jest.restoreAllMocks(); + jest.clearAllMocks(); + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping: connectors[1].config.casesConfiguration.mapping, + closureType: 'close-by-user', + connectorId: 'servicenow-2', + connectorName: 'unchanged', + currentConfiguration: { + connectorName: 'unchanged', + connectorId: 'servicenow-1', + closureType: 'close-by-user', + }, + })); + useConnectorsMock.mockImplementation(() => ({ + ...useConnectorsResponse, + loading: true, + })); + useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); + useGetUrlSearchMock.mockImplementation(() => searchURL); + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it disables correctly Connector when loading connectors', () => { + expect( + wrapper.find('button[data-test-subj="dropdown-connectors"]').prop('disabled') + ).toBeTruthy(); + }); + + test('it pass the correct value to isLoading attribute on Connector', () => { + expect(wrapper.find(Connectors).prop('isLoading')).toBe(true); + }); + + test('it disables correctly ClosureOptions when loading connectors', () => { + expect(wrapper.find(ClosureOptions).prop('disabled')).toBe(true); + }); + + test('it hides the update connector button when loading the connectors', () => { + expect( + wrapper + .find('button[data-test-subj="case-configure-update-selected-connector-button"]') + .prop('disabled') + ).toBe(true); + }); + + test('it disables the buttons of action bar when loading connectors', () => { + const newWrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect( + newWrapper + .find('button[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') + .first() + .prop('disabled') + ).toBe(true); + + expect( + newWrapper + .find('button[data-test-subj="case-configure-action-bottom-bar-save-button"]') + .first() + .prop('disabled') + ).toBe(true); + }); + }); + + describe('saving configuration', () => { + let wrapper: ReactWrapper; + + beforeEach(() => { + jest.resetAllMocks(); + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + connectorId: 'servicenow-1', + persistLoading: true, + })); + + useConnectorsMock.mockImplementation(() => useConnectorsResponse); + useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); + useGetUrlSearchMock.mockImplementation(() => searchURL); + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it disables correctly Connector when saving configuration', () => { + expect(wrapper.find(Connectors).prop('disabled')).toBe(true); + }); + + test('it disables correctly ClosureOptions when saving configuration', () => { + expect( + wrapper + .find('[data-test-subj="closure-options-radio-group"] input') + .first() + .prop('disabled') + ).toBe(true); + + expect( + wrapper + .find('[data-test-subj="closure-options-radio-group"] input') + .at(1) + .prop('disabled') + ).toBe(true); + }); + + test('it disables the update connector button when saving the configuration', () => { + expect( + wrapper + .find('button[data-test-subj="case-configure-update-selected-connector-button"]') + .prop('disabled') + ).toBe(true); + }); + + test('it disables the buttons of action bar when saving configuration', () => { + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping: connectors[1].config.casesConfiguration.mapping, + closureType: 'close-by-user', + connectorId: 'servicenow-2', + connectorName: 'unchanged', + currentConfiguration: { + connectorName: 'unchanged', + connectorId: 'servicenow-1', + closureType: 'close-by-user', + }, + persistLoading: true, + })); + + const newWrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect( + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') + .first() + .prop('isDisabled') + ).toBe(true); + + expect( + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') + .first() + .prop('isDisabled') + ).toBe(true); + }); + + test('it shows the loading spinner when saving configuration', () => { + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping: connectors[1].config.casesConfiguration.mapping, + closureType: 'close-by-user', + connectorId: 'servicenow-2', + connectorName: 'unchanged', + currentConfiguration: { + connectorName: 'unchanged', + connectorId: 'servicenow-1', + closureType: 'close-by-user', + }, + persistLoading: true, + })); + + const newWrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect( + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') + .first() + .prop('isLoading') + ).toBe(true); + + expect( + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') + .first() + .prop('isLoading') + ).toBe(true); + }); + }); + + describe('loading configuration', () => { + let wrapper: ReactWrapper; + + beforeEach(() => { + jest.resetAllMocks(); + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + loading: true, + })); + useConnectorsMock.mockImplementation(() => ({ + ...useConnectorsResponse, + })); + useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); + useGetUrlSearchMock.mockImplementation(() => searchURL); + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it hides the update connector button when loading the configuration', () => { + expect( + wrapper + .find('button[data-test-subj="case-configure-update-selected-connector-button"]') + .exists() + ).toBeFalsy(); + }); + }); + + describe('update connector', () => { + let wrapper: ReactWrapper; + const persistCaseConfigure = jest.fn(); + + beforeEach(() => { + jest.resetAllMocks(); + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping: connectors[1].config.casesConfiguration.mapping, + closureType: 'close-by-user', + connectorId: 'servicenow-2', + connectorName: 'unchanged', + currentConfiguration: { + connectorName: 'unchanged', + connectorId: 'servicenow-1', + closureType: 'close-by-user', + }, + persistCaseConfigure, + })); + useConnectorsMock.mockImplementation(() => useConnectorsResponse); + useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); + useGetUrlSearchMock.mockImplementation(() => searchURL); + + wrapper = mount(, { wrappingComponent: TestProviders }); + }); + + test('it submits the configuration correctly', () => { + wrapper + .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') + .first() + .simulate('click'); + + wrapper.update(); + + expect(persistCaseConfigure).toHaveBeenCalled(); + expect(persistCaseConfigure).toHaveBeenCalledWith({ + connectorId: 'servicenow-2', + connectorName: 'My Connector 2', + closureType: 'close-by-user', + }); + }); + + test('it has the correct url on cancel button', () => { + expect( + wrapper + .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') + .first() + .prop('href') + ).toBe(`#/link-to/case${searchURL}`); + }); + + test('it disables the buttons of action bar when loading configuration', () => { + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping: connectors[1].config.casesConfiguration.mapping, + closureType: 'close-by-user', + connectorId: 'servicenow-2', + connectorName: 'unchanged', + currentConfiguration: { + connectorName: 'unchanged', + connectorId: 'servicenow-1', + closureType: 'close-by-user', + }, + loading: true, + })); + const newWrapper = mount(, { + wrappingComponent: TestProviders, + }); + + expect( + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') + .first() + .prop('isDisabled') + ).toBe(true); + + expect( + newWrapper + .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') + .first() + .prop('isDisabled') + ).toBe(true); + }); + }); + + describe('user interactions', () => { + beforeEach(() => { + jest.resetAllMocks(); + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping: connectors[1].config.casesConfiguration.mapping, + closureType: 'close-by-user', + connectorId: 'servicenow-2', + connectorName: 'unchanged', + currentConfiguration: { + connectorName: 'unchanged', + connectorId: 'servicenow-2', + closureType: 'close-by-user', + }, + })); + useConnectorsMock.mockImplementation(() => useConnectorsResponse); + useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); + useGetUrlSearchMock.mockImplementation(() => searchURL); + }); + + test('it show the add flyout when pressing the add connector button', () => { + const wrapper = mount(, { wrappingComponent: TestProviders }); + wrapper + .find('button[data-test-subj="case-configure-add-connector-button"]') + .simulate('click'); + wrapper.update(); + + expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(true); + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeFalsy(); + }); + + test('it show the edit flyout when pressing the update connector button', () => { + const wrapper = mount(, { wrappingComponent: TestProviders }); + wrapper + .find('button[data-test-subj="case-configure-update-selected-connector-button"]') + .simulate('click'); + wrapper.update(); + + expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(true); + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeFalsy(); + }); + + test('it tracks the changes successfully', () => { + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping: connectors[1].config.casesConfiguration.mapping, + closureType: 'close-by-user', + connectorId: 'servicenow-2', + connectorName: 'unchanged', + currentConfiguration: { + connectorName: 'unchanged', + connectorId: 'servicenow-1', + closureType: 'close-by-pushing', + }, + })); + const wrapper = mount(, { wrappingComponent: TestProviders }); + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.update(); + wrapper.find('button[data-test-subj="dropdown-connector-servicenow-2"]').simulate('click'); + wrapper.update(); + wrapper.find('input[id="close-by-pushing"]').simulate('change'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') + .first() + .text() + ).toBe('2 unsaved changes'); + }); + + test('it tracks the changes successfully when name changes', () => { + useCaseConfigureMock.mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping: connectors[1].config.casesConfiguration.mapping, + closureType: 'close-by-user', + connectorId: 'servicenow-2', + connectorName: 'nameChange', + currentConfiguration: { + connectorId: 'servicenow-1', + closureType: 'close-by-pushing', + connectorName: 'before', + }, + })); + + const wrapper = mount(, { wrappingComponent: TestProviders }); + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.update(); + wrapper.find('button[data-test-subj="dropdown-connector-servicenow-2"]').simulate('click'); + wrapper.update(); + wrapper.find('input[id="close-by-pushing"]').simulate('change'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') + .first() + .text() + ).toBe('2 unsaved changes'); + }); + + test('it tracks and reverts the changes successfully ', () => { + const wrapper = mount(, { wrappingComponent: TestProviders }); + // change settings + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.update(); + wrapper.find('button[data-test-subj="dropdown-connector-servicenow-2"]').simulate('click'); + wrapper.update(); + wrapper.find('input[id="close-by-pushing"]').simulate('change'); + wrapper.update(); + + // revert back to initial settings + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.update(); + wrapper.find('button[data-test-subj="dropdown-connector-servicenow-1"]').simulate('click'); + wrapper.update(); + wrapper.find('input[id="close-by-user"]').simulate('change'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeFalsy(); + }); + + test('it close and restores the action bar when the add connector button is pressed', () => { + useCaseConfigureMock + .mockImplementationOnce(() => ({ + ...useCaseConfigureResponse, + mapping: connectors[1].config.casesConfiguration.mapping, + closureType: 'close-by-user', + connectorId: 'servicenow-2', + currentConfiguration: { connectorId: 'servicenow-2', closureType: 'close-by-user' }, + })) + .mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping: connectors[1].config.casesConfiguration.mapping, + closureType: 'close-by-pushing', + connectorId: 'servicenow-2', + currentConfiguration: { connectorId: 'servicenow-2', closureType: 'close-by-user' }, + })); + const wrapper = mount(, { wrappingComponent: TestProviders }); + // Change closure type + wrapper.find('input[id="close-by-pushing"]').simulate('change'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeTruthy(); + + // Press add connector button + wrapper + .find('button[data-test-subj="case-configure-add-connector-button"]') + .simulate('click'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeFalsy(); + + expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(true); + + // Close the add flyout + wrapper.find('button[data-test-subj="euiFlyoutCloseButton"]').simulate('click'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeTruthy(); + + expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(false); + + expect( + wrapper + .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') + .first() + .text() + ).toBe('1 unsaved changes'); + }); + + test('it close and restores the action bar when the update connector button is pressed', () => { + useCaseConfigureMock + .mockImplementationOnce(() => ({ + ...useCaseConfigureResponse, + mapping: connectors[1].config.casesConfiguration.mapping, + closureType: 'close-by-user', + connectorId: 'servicenow-2', + currentConfiguration: { connectorId: 'servicenow-2', closureType: 'close-by-user' }, + })) + .mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping: connectors[1].config.casesConfiguration.mapping, + closureType: 'close-by-pushing', + connectorId: 'servicenow-2', + currentConfiguration: { connectorId: 'servicenow-2', closureType: 'close-by-user' }, + })); + const wrapper = mount(, { wrappingComponent: TestProviders }); + + // Change closure type + wrapper.find('input[id="close-by-pushing"]').simulate('change'); + wrapper.update(); + + // Press update connector button + wrapper + .find('button[data-test-subj="case-configure-update-selected-connector-button"]') + .simulate('click'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeFalsy(); + + expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(true); + + // Close the edit flyout + wrapper.find('button[data-test-subj="euiFlyoutCloseButton"]').simulate('click'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeTruthy(); + + expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(false); + + expect( + wrapper + .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') + .first() + .text() + ).toBe('1 unsaved changes'); + }); + + test('it shows the action bar when the connector is changed', () => { + useCaseConfigureMock + .mockImplementationOnce(() => ({ + ...useCaseConfigureResponse, + mapping: connectors[0].config.casesConfiguration.mapping, + closureType: 'close-by-user', + connectorId: 'servicenow-1', + currentConfiguration: { connectorId: 'servicenow-1', closureType: 'close-by-user' }, + })) + .mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping: connectors[1].config.casesConfiguration.mapping, + closureType: 'close-by-user', + connectorId: 'servicenow-2', + currentConfiguration: { connectorId: 'servicenow-1', closureType: 'close-by-user' }, + })); + const wrapper = mount(, { wrappingComponent: TestProviders }); + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.update(); + wrapper.find('button[data-test-subj="dropdown-connector-servicenow-2"]').simulate('click'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeTruthy(); + expect( + wrapper + .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') + .first() + .text() + ).toBe('1 unsaved changes'); + }); + + test('it closes the action bar when pressing save', () => { + useCaseConfigureMock + .mockImplementationOnce(() => ({ + ...useCaseConfigureResponse, + mapping: connectors[1].config.casesConfiguration.mapping, + closureType: 'close-by-user', + connectorId: 'servicenow-2', + currentConfiguration: { connectorId: 'servicenow-2', closureType: 'close-by-user' }, + })) + .mockImplementation(() => ({ + ...useCaseConfigureResponse, + mapping: connectors[1].config.casesConfiguration.mapping, + closureType: 'close-by-pushing', + connectorId: 'servicenow-2', + currentConfiguration: { connectorId: 'servicenow-2', closureType: 'close-by-user' }, + })); + const wrapper = mount(, { wrappingComponent: TestProviders }); + wrapper.find('input[id="close-by-pushing"]').simulate('change'); + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeTruthy(); + + wrapper + .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') + .first() + .simulate('click'); + + wrapper.update(); + + expect( + wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() + ).toBeFalsy(); + }); + + test('the text of the update button is changed successfully', () => { + useCaseConfigureMock + .mockImplementationOnce(() => ({ + ...useCaseConfigureResponse, + connectorId: 'servicenow-1', + })) + .mockImplementation(() => ({ + ...useCaseConfigureResponse, + connectorId: 'servicenow-2', + })); + + const wrapper = mount(, { wrappingComponent: TestProviders }); + + wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); + wrapper.update(); + wrapper.find('button[data-test-subj="dropdown-connector-servicenow-2"]').simulate('click'); + wrapper.update(); + + expect( + wrapper + .find('button[data-test-subj="case-configure-update-selected-connector-button"]') + .text() + ).toBe('Update My Connector 2'); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/cases/components/configure_cases/index.tsx b/x-pack/plugins/siem/public/cases/components/configure_cases/index.tsx new file mode 100644 index 00000000000000..d5c6cc671433bf --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/configure_cases/index.tsx @@ -0,0 +1,288 @@ +/* + * 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, { useCallback, useEffect, useState, Dispatch, SetStateAction } from 'react'; +import styled, { css } from 'styled-components'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiCallOut, + EuiBottomBar, + EuiButtonEmpty, + EuiText, +} from '@elastic/eui'; +import { difference } from 'lodash/fp'; +import { useKibana } from '../../../common/lib/kibana'; +import { useConnectors } from '../../containers/configure/use_connectors'; +import { useCaseConfigure } from '../../containers/configure/use_configure'; +import { + ActionsConnectorsContextProvider, + ActionType, + ConnectorAddFlyout, + ConnectorEditFlyout, +} from '../../../../../triggers_actions_ui/public'; + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ActionConnectorTableItem } from '../../../../../triggers_actions_ui/public/types'; +import { getCaseUrl } from '../../../common/components/link_to'; +import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search'; +import { connectorsConfiguration } from '../../../common/lib/connectors/config'; + +import { Connectors } from './connectors'; +import { ClosureOptions } from './closure_options'; +import { SectionWrapper } from '../wrappers'; +import { navTabs } from '../../../app/home/home_navigations'; +import * as i18n from './translations'; + +const FormWrapper = styled.div` + ${({ theme }) => css` + & > * { + margin-top 40px; + } + + & > :first-child { + margin-top: 0; + } + + padding-top: ${theme.eui.paddingSizes.xl}; + padding-bottom: ${theme.eui.paddingSizes.xl}; + `} +`; + +const actionTypes: ActionType[] = Object.values(connectorsConfiguration); + +interface ConfigureCasesComponentProps { + userCanCrud: boolean; +} + +const ConfigureCasesComponent: React.FC = ({ userCanCrud }) => { + const search = useGetUrlSearch(navTabs.case); + const { http, triggers_actions_ui, notifications, application, docLinks } = useKibana().services; + + const [connectorIsValid, setConnectorIsValid] = useState(true); + const [addFlyoutVisible, setAddFlyoutVisibility] = useState(false); + const [editFlyoutVisible, setEditFlyoutVisibility] = useState(false); + const [editedConnectorItem, setEditedConnectorItem] = useState( + null + ); + + const [actionBarVisible, setActionBarVisible] = useState(false); + const [totalConfigurationChanges, setTotalConfigurationChanges] = useState(0); + + const { + connectorId, + closureType, + currentConfiguration, + loading: loadingCaseConfigure, + persistLoading, + persistCaseConfigure, + setConnector, + setClosureType, + } = useCaseConfigure(); + + const { loading: isLoadingConnectors, connectors, refetchConnectors } = useConnectors(); + + // ActionsConnectorsContextProvider reloadConnectors prop expects a Promise. + // TODO: Fix it if reloadConnectors type change. + const reloadConnectors = useCallback(async () => refetchConnectors(), []); + const isLoadingAny = isLoadingConnectors || persistLoading || loadingCaseConfigure; + const updateConnectorDisabled = isLoadingAny || !connectorIsValid || connectorId === 'none'; + + const handleSubmit = useCallback( + // TO DO give a warning/error to user when field are not mapped so they have chance to do it + () => { + setActionBarVisible(false); + persistCaseConfigure({ + connectorId, + connectorName: connectors.find(c => c.id === connectorId)?.name ?? '', + closureType, + }); + }, + [connectorId, connectors, closureType] + ); + + const onClickAddConnector = useCallback(() => { + setActionBarVisible(false); + setAddFlyoutVisibility(true); + }, []); + + const onClickUpdateConnector = useCallback(() => { + setActionBarVisible(false); + setEditFlyoutVisibility(true); + }, []); + + const handleActionBar = useCallback(() => { + const currentConfigurationMinusName = { + connectorId: currentConfiguration.connectorId, + closureType: currentConfiguration.closureType, + }; + const unsavedChanges = difference(Object.values(currentConfigurationMinusName), [ + connectorId, + closureType, + ]).length; + setActionBarVisible(!(unsavedChanges === 0)); + setTotalConfigurationChanges(unsavedChanges); + }, [currentConfiguration, connectorId, closureType]); + + const handleSetAddFlyoutVisibility = useCallback( + (isVisible: boolean) => { + handleActionBar(); + setAddFlyoutVisibility(isVisible); + }, + [currentConfiguration, connectorId, closureType] + ); + + const handleSetEditFlyoutVisibility = useCallback( + (isVisible: boolean) => { + handleActionBar(); + setEditFlyoutVisibility(isVisible); + }, + [currentConfiguration, connectorId, closureType] + ); + + useEffect(() => { + if ( + !isLoadingConnectors && + connectorId !== 'none' && + !connectors.some(c => c.id === connectorId) + ) { + setConnectorIsValid(false); + } else if ( + !isLoadingConnectors && + (connectorId === 'none' || connectors.some(c => c.id === connectorId)) + ) { + setConnectorIsValid(true); + } + }, [connectors, connectorId]); + + useEffect(() => { + if (!isLoadingConnectors && connectorId !== 'none') { + setEditedConnectorItem( + connectors.find(c => c.id === connectorId) as ActionConnectorTableItem + ); + } + }, [connectors, connectorId]); + + useEffect(() => { + handleActionBar(); + }, [ + connectors, + connectorId, + closureType, + currentConfiguration.connectorId, + currentConfiguration.closureType, + ]); + + return ( + + {!connectorIsValid && ( + + + {i18n.WARNING_NO_CONNECTOR_MESSAGE} + + + )} + + + + + + + {actionBarVisible && ( + + + + + + {i18n.UNSAVED_CHANGES(totalConfigurationChanges)} + + + + + + + + {i18n.CANCEL} + + + + + {i18n.SAVE_CHANGES} + + + + + + + )} + + >} + actionTypes={actionTypes} + /> + {editedConnectorItem && ( + > + } + /> + )} + + + ); +}; + +export const ConfigureCases = React.memo(ConfigureCasesComponent); diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/mapping.test.tsx b/x-pack/plugins/siem/public/cases/components/configure_cases/mapping.test.tsx similarity index 99% rename from x-pack/plugins/siem/public/pages/case/components/configure_cases/mapping.test.tsx rename to x-pack/plugins/siem/public/cases/components/configure_cases/mapping.test.tsx index 083904d303490f..68a35987ecaf60 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/mapping.test.tsx +++ b/x-pack/plugins/siem/public/cases/components/configure_cases/mapping.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { mount, ReactWrapper } from 'enzyme'; -import { TestProviders } from '../../../../mock'; +import { TestProviders } from '../../../common/mock'; import { Mapping, MappingProps } from './mapping'; import { mapping } from './__mock__'; diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/mapping.tsx b/x-pack/plugins/siem/public/cases/components/configure_cases/mapping.tsx similarity index 95% rename from x-pack/plugins/siem/public/pages/case/components/configure_cases/mapping.tsx rename to x-pack/plugins/siem/public/cases/components/configure_cases/mapping.tsx index acbcdac68a1340..2c3172a30f159f 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/mapping.tsx +++ b/x-pack/plugins/siem/public/cases/components/configure_cases/mapping.tsx @@ -18,7 +18,7 @@ import { import * as i18n from './translations'; import { FieldMapping } from './field_mapping'; -import { CasesConfigurationMapping } from '../../../../containers/case/configure/types'; +import { CasesConfigurationMapping } from '../../containers/configure/types'; export interface MappingProps { disabled: boolean; diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/translations.ts b/x-pack/plugins/siem/public/cases/components/configure_cases/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/case/components/configure_cases/translations.ts rename to x-pack/plugins/siem/public/cases/components/configure_cases/translations.ts diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/utils.test.tsx b/x-pack/plugins/siem/public/cases/components/configure_cases/utils.test.tsx similarity index 96% rename from x-pack/plugins/siem/public/pages/case/components/configure_cases/utils.test.tsx rename to x-pack/plugins/siem/public/cases/components/configure_cases/utils.test.tsx index 1c6fc9b2d405f9..d6755f687100f7 100644 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/utils.test.tsx +++ b/x-pack/plugins/siem/public/cases/components/configure_cases/utils.test.tsx @@ -6,7 +6,7 @@ import { mapping } from './__mock__'; import { setActionTypeToMapping, setThirdPartyToMapping } from './utils'; -import { CasesConfigurationMapping } from '../../../../containers/case/configure/types'; +import { CasesConfigurationMapping } from '../../containers/configure/types'; describe('FieldMappingRow', () => { test('it should change the action type', () => { diff --git a/x-pack/plugins/siem/public/cases/components/configure_cases/utils.ts b/x-pack/plugins/siem/public/cases/components/configure_cases/utils.ts new file mode 100644 index 00000000000000..95851ec294e0bc --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/configure_cases/utils.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { + CaseField, + ActionType, + CasesConfigurationMapping, + ThirdPartyField, +} from '../../containers/configure/types'; + +export const setActionTypeToMapping = ( + caseField: CaseField, + newActionType: ActionType, + mapping: CasesConfigurationMapping[] +): CasesConfigurationMapping[] => { + const findItemIndex = mapping.findIndex(item => item.source === caseField); + + if (findItemIndex >= 0) { + return [ + ...mapping.slice(0, findItemIndex), + { ...mapping[findItemIndex], actionType: newActionType }, + ...mapping.slice(findItemIndex + 1), + ]; + } + + return [...mapping]; +}; + +export const setThirdPartyToMapping = ( + caseField: CaseField, + newThirdPartyField: ThirdPartyField, + mapping: CasesConfigurationMapping[] +): CasesConfigurationMapping[] => + mapping.map(item => { + if (item.source !== caseField && item.target === newThirdPartyField) { + return { ...item, target: 'not_mapped' }; + } else if (item.source === caseField) { + return { ...item, target: newThirdPartyField }; + } + return item; + }); diff --git a/x-pack/plugins/siem/public/pages/case/components/confirm_delete_case/index.tsx b/x-pack/plugins/siem/public/cases/components/confirm_delete_case/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/case/components/confirm_delete_case/index.tsx rename to x-pack/plugins/siem/public/cases/components/confirm_delete_case/index.tsx diff --git a/x-pack/plugins/siem/public/pages/case/components/confirm_delete_case/translations.ts b/x-pack/plugins/siem/public/cases/components/confirm_delete_case/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/case/components/confirm_delete_case/translations.ts rename to x-pack/plugins/siem/public/cases/components/confirm_delete_case/translations.ts diff --git a/x-pack/plugins/siem/public/cases/components/connector_selector/form.tsx b/x-pack/plugins/siem/public/cases/components/connector_selector/form.tsx new file mode 100644 index 00000000000000..9e058ee5cf09e1 --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/connector_selector/form.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFormRow } from '@elastic/eui'; +import React, { useCallback, useEffect } from 'react'; + +import { FieldHook, getFieldValidityAndErrorMessage } from '../../../shared_imports'; +import { ConnectorsDropdown } from '../configure_cases/connectors_dropdown'; +import { Connector } from '../../../../../case/common/api/cases'; + +interface ConnectorSelectorProps { + connectors: Connector[]; + dataTestSubj: string; + field: FieldHook; + idAria: string; + defaultValue?: string; + disabled: boolean; + isLoading: boolean; +} +export const ConnectorSelector = ({ + connectors, + dataTestSubj, + defaultValue, + field, + idAria, + disabled = false, + isLoading = false, +}: ConnectorSelectorProps) => { + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + + useEffect(() => { + field.setValue(defaultValue); + }, [defaultValue]); + + const handleContentChange = useCallback( + (newContent: string) => { + field.setValue(newContent); + }, + [field] + ); + + return ( + + + + ); +}; diff --git a/x-pack/plugins/siem/public/cases/components/create/index.test.tsx b/x-pack/plugins/siem/public/cases/components/create/index.test.tsx new file mode 100644 index 00000000000000..647a0d32472591 --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/create/index.test.tsx @@ -0,0 +1,162 @@ +/* + * 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 { Create } from '.'; +import { TestProviders } from '../../../common/mock'; +import { getFormMock } from '../__mock__/form'; +import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; + +import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline'; +import { usePostCase } from '../../containers/use_post_case'; +import { useGetTags } from '../../containers/use_get_tags'; + +jest.mock('../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline'); +jest.mock('../../containers/use_post_case'); +import { useForm } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'; +import { wait } from '../../../common/lib/helpers'; +import { SiemPageName } from '../../../app/types'; +jest.mock( + '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' +); +jest.mock('../../containers/use_get_tags'); +jest.mock( + '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider', + () => ({ + FormDataProvider: ({ children }: { children: ({ tags }: { tags: string[] }) => void }) => + children({ tags: ['rad', 'dude'] }), + }) +); + +export const useFormMock = useForm as jest.Mock; + +const useInsertTimelineMock = useInsertTimeline as jest.Mock; +const usePostCaseMock = usePostCase as jest.Mock; + +const postCase = jest.fn(); +const handleCursorChange = jest.fn(); +const handleOnTimelineChange = jest.fn(); + +const defaultInsertTimeline = { + cursorPosition: { + start: 0, + end: 0, + }, + handleCursorChange, + handleOnTimelineChange, +}; + +const sampleTags = ['coke', 'pepsi']; +const sampleData = { + description: 'what a great description', + tags: sampleTags, + title: 'what a cool title', +}; +const defaultPostCase = { + isLoading: false, + isError: false, + caseData: null, + postCase, +}; +describe('Create case', () => { + // Suppress warnings about "noSuggestions" prop + /* eslint-disable no-console */ + const originalError = console.error; + beforeAll(() => { + console.error = jest.fn(); + }); + afterAll(() => { + console.error = originalError; + }); + /* eslint-enable no-console */ + const fetchTags = jest.fn(); + const formHookMock = getFormMock(sampleData); + beforeEach(() => { + jest.resetAllMocks(); + useInsertTimelineMock.mockImplementation(() => defaultInsertTimeline); + usePostCaseMock.mockImplementation(() => defaultPostCase); + useFormMock.mockImplementation(() => ({ form: formHookMock })); + jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); + (useGetTags as jest.Mock).mockImplementation(() => ({ + tags: sampleTags, + fetchTags, + })); + }); + + it('should post case on submit click', async () => { + const wrapper = mount( + + + + + + ); + wrapper + .find(`[data-test-subj="create-case-submit"]`) + .first() + .simulate('click'); + await wait(); + expect(postCase).toBeCalledWith(sampleData); + }); + + it('should redirect to all cases on cancel click', () => { + const wrapper = mount( + + + + + + ); + wrapper + .find(`[data-test-subj="create-case-cancel"]`) + .first() + .simulate('click'); + expect(mockHistory.replace.mock.calls[0][0].pathname).toEqual(`/${SiemPageName.case}`); + }); + it('should redirect to new case when caseData is there', () => { + const sampleId = '777777'; + usePostCaseMock.mockImplementation(() => ({ ...defaultPostCase, caseData: { id: sampleId } })); + mount( + + + + + + ); + expect(mockHistory.replace.mock.calls[0][0].pathname).toEqual( + `/${SiemPageName.case}/${sampleId}` + ); + }); + + it('should render spinner when loading', () => { + usePostCaseMock.mockImplementation(() => ({ ...defaultPostCase, isLoading: true })); + const wrapper = mount( + + + + + + ); + expect(wrapper.find(`[data-test-subj="create-case-loading-spinner"]`).exists()).toBeTruthy(); + }); + it('Tag options render with new tags added', () => { + const wrapper = mount( + + + + + + ); + expect( + wrapper + .find(`[data-test-subj="caseTags"] [data-test-subj="input"]`) + .first() + .prop('options') + ).toEqual([{ label: 'coke' }, { label: 'pepsi' }, { label: 'rad' }, { label: 'dude' }]); + }); +}); diff --git a/x-pack/plugins/siem/public/cases/components/create/index.tsx b/x-pack/plugins/siem/public/cases/components/create/index.tsx new file mode 100644 index 00000000000000..655536faa171db --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/create/index.tsx @@ -0,0 +1,215 @@ +/* + * 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, { useCallback, useEffect, useState } from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiPanel, +} from '@elastic/eui'; +import styled, { css } from 'styled-components'; +import { Redirect } from 'react-router-dom'; + +import { isEqual } from 'lodash/fp'; +import { CasePostRequest } from '../../../../../case/common/api'; +import { + Field, + Form, + getUseField, + useForm, + UseField, + FormDataProvider, +} from '../../../shared_imports'; +import { usePostCase } from '../../containers/use_post_case'; +import { schema } from './schema'; +import { InsertTimelinePopover } from '../../../timelines/components/timeline/insert_timeline_popover'; +import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline'; +import * as i18n from '../../translations'; +import { SiemPageName } from '../../../app/types'; +import { MarkdownEditorForm } from '../../../common/components//markdown_editor/form'; +import { useGetTags } from '../../containers/use_get_tags'; + +export const CommonUseField = getUseField({ component: Field }); + +const ContainerBig = styled.div` + ${({ theme }) => css` + margin-top: ${theme.eui.euiSizeXL}; + `} +`; + +const Container = styled.div` + ${({ theme }) => css` + margin-top: ${theme.eui.euiSize}; + `} +`; +const MySpinner = styled(EuiLoadingSpinner)` + position: absolute; + top: 50%; + left: 50%; + z-index: 99; +`; + +const initialCaseValue: CasePostRequest = { + description: '', + tags: [], + title: '', +}; + +export const Create = React.memo(() => { + const { caseData, isLoading, postCase } = usePostCase(); + const [isCancel, setIsCancel] = useState(false); + const { form } = useForm({ + defaultValue: initialCaseValue, + options: { stripEmptyFields: false }, + schema, + }); + const { tags: tagOptions } = useGetTags(); + const [options, setOptions] = useState( + tagOptions.map(label => ({ + label, + })) + ); + useEffect( + () => + setOptions( + tagOptions.map(label => ({ + label, + })) + ), + [tagOptions] + ); + const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline( + form, + 'description' + ); + + const onSubmit = useCallback(async () => { + const { isValid, data } = await form.submit(); + if (isValid) { + await postCase(data); + } + }, [form]); + + const handleSetIsCancel = useCallback(() => { + setIsCancel(true); + }, []); + + if (caseData != null && caseData.id) { + return ; + } + + if (isCancel) { + return ; + } + + return ( + + {isLoading && } +
+ + + + + + + ), + }} + /> + + + {({ tags: anotherTags }) => { + const current: string[] = options.map(opt => opt.label); + const newOptions = anotherTags.reduce((acc: string[], item: string) => { + if (!acc.includes(item)) { + return [...acc, item]; + } + return acc; + }, current); + if (!isEqual(current, newOptions)) { + setOptions( + newOptions.map((label: string) => ({ + label, + })) + ); + } + return null; + }} + + + + + + + {i18n.CANCEL} + + + + + {i18n.CREATE_CASE} + + + + +
+ ); +}); + +Create.displayName = 'Create'; diff --git a/x-pack/plugins/siem/public/pages/case/components/create/optional_field_label/index.tsx b/x-pack/plugins/siem/public/cases/components/create/optional_field_label/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/case/components/create/optional_field_label/index.tsx rename to x-pack/plugins/siem/public/cases/components/create/optional_field_label/index.tsx diff --git a/x-pack/plugins/siem/public/cases/components/create/schema.tsx b/x-pack/plugins/siem/public/cases/components/create/schema.tsx new file mode 100644 index 00000000000000..ce38033271d043 --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/create/schema.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CasePostRequest } from '../../../../../case/common/api'; +import { FIELD_TYPES, fieldValidators, FormSchema } from '../../../shared_imports'; +import * as i18n from '../../translations'; + +import { OptionalFieldLabel } from './optional_field_label'; +const { emptyField } = fieldValidators; + +export const schemaTags = { + type: FIELD_TYPES.COMBO_BOX, + label: i18n.TAGS, + helpText: i18n.TAGS_HELP, + labelAppend: OptionalFieldLabel, +}; + +export const schema: FormSchema = { + title: { + type: FIELD_TYPES.TEXT, + label: i18n.NAME, + validations: [ + { + validator: emptyField(i18n.TITLE_REQUIRED), + }, + ], + }, + description: { + label: i18n.DESCRIPTION, + validations: [ + { + validator: emptyField(i18n.DESCRIPTION_REQUIRED), + }, + ], + }, + tags: schemaTags, +}; diff --git a/x-pack/plugins/siem/public/cases/components/edit_connector/index.test.tsx b/x-pack/plugins/siem/public/cases/components/edit_connector/index.test.tsx new file mode 100644 index 00000000000000..5dfed80baa8edb --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/edit_connector/index.test.tsx @@ -0,0 +1,154 @@ +/* + * 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 { EditConnector } from './index'; +import { getFormMock, useFormMock } from '../__mock__/form'; +import { TestProviders } from '../../../common/mock'; +import { connectorsMock } from '../../containers/configure/mock'; +import { wait } from '../../../common/lib/helpers'; +import { act } from 'react-dom/test-utils'; +jest.mock( + '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' +); +const onSubmit = jest.fn(); +const defaultProps = { + connectors: connectorsMock, + disabled: false, + isLoading: false, + onSubmit, + selectedConnector: 'none', +}; + +describe('EditConnector ', () => { + const sampleConnector = '123'; + const formHookMock = getFormMock({ connector: sampleConnector }); + beforeEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + useFormMock.mockImplementation(() => ({ form: formHookMock })); + }); + it('Renders no connector, and then edit', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find(`[data-test-subj="dropdown-connectors"]`) + .last() + .prop('disabled') + ).toBeTruthy(); + + expect( + wrapper + .find(`span[data-test-subj="dropdown-connector-no-connector"]`) + .last() + .exists() + ).toBeTruthy(); + wrapper + .find(`[data-test-subj="connector-edit-button"]`) + .last() + .simulate('click'); + + expect( + wrapper + .find(`[data-test-subj="edit-connectors-submit"]`) + .last() + .exists() + ).toBeTruthy(); + + expect( + wrapper + .find(`[data-test-subj="dropdown-connectors"]`) + .last() + .prop('disabled') + ).toBeFalsy(); + }); + it('Edit external service on submit', async () => { + const wrapper = mount( + + + + ); + wrapper + .find(`[data-test-subj="connector-edit-button"]`) + .last() + .simulate('click'); + expect( + wrapper + .find(`[data-test-subj="edit-connectors-submit"]`) + .last() + .exists() + ).toBeTruthy(); + await act(async () => { + wrapper + .find(`[data-test-subj="edit-connectors-submit"]`) + .last() + .simulate('click'); + await wait(); + expect(onSubmit).toBeCalledWith(sampleConnector); + }); + }); + it('Resets selector on cancel', async () => { + const props = { + ...defaultProps, + }; + const wrapper = mount( + + + + ); + wrapper + .find(`[data-test-subj="connector-edit-button"]`) + .last() + .simulate('click'); + await act(async () => { + wrapper + .find(`[data-test-subj="edit-connectors-cancel"]`) + .last() + .simulate('click'); + await wait(); + wrapper.update(); + expect(formHookMock.setFieldValue).toBeCalledWith( + 'connector', + defaultProps.selectedConnector + ); + }); + }); + it('Renders disabled button', () => { + const props = { ...defaultProps, disabled: true }; + const wrapper = mount( + + + + ); + expect( + wrapper + .find(`[data-test-subj="connector-edit-button"]`) + .last() + .prop('disabled') + ).toBeTruthy(); + }); + it('Renders loading spinner', () => { + const props = { ...defaultProps, isLoading: true }; + const wrapper = mount( + + + + ); + expect( + wrapper + .find(`[data-test-subj="connector-loading"]`) + .last() + .exists() + ).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/siem/public/cases/components/edit_connector/index.tsx b/x-pack/plugins/siem/public/cases/components/edit_connector/index.tsx new file mode 100644 index 00000000000000..29f06532a4ab4a --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/edit_connector/index.tsx @@ -0,0 +1,149 @@ +/* + * 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, { useCallback, useState } from 'react'; +import { + EuiText, + EuiHorizontalRule, + EuiFlexGroup, + EuiFlexItem, + EuiButton, + EuiButtonEmpty, + EuiButtonIcon, + EuiLoadingSpinner, +} from '@elastic/eui'; +import styled, { css } from 'styled-components'; +import * as i18n from '../../translations'; +import { Form, UseField, useForm } from '../../../shared_imports'; +import { schema } from './schema'; +import { ConnectorSelector } from '../connector_selector/form'; +import { Connector } from '../../../../../case/common/api/cases'; + +interface EditConnectorProps { + connectors: Connector[]; + disabled?: boolean; + isLoading: boolean; + onSubmit: (a: string[]) => void; + selectedConnector: string; +} + +const MyFlexGroup = styled(EuiFlexGroup)` + ${({ theme }) => css` + margin-top: ${theme.eui.euiSizeM}; + p { + font-size: ${theme.eui.euiSizeM}; + } + `} +`; + +export const EditConnector = React.memo( + ({ + connectors, + disabled = false, + isLoading, + onSubmit, + selectedConnector, + }: EditConnectorProps) => { + const { form } = useForm({ + defaultValue: { connectors }, + options: { stripEmptyFields: false }, + schema, + }); + const [isEditConnector, setIsEditConnector] = useState(false); + const handleOnClick = useCallback(() => { + setIsEditConnector(true); + }, []); + + const onCancelConnector = useCallback(() => { + form.setFieldValue('connector', selectedConnector); + setIsEditConnector(false); + }, [form, selectedConnector]); + + const onSubmitConnector = useCallback(async () => { + const { isValid, data: newData } = await form.submit(); + if (isValid && newData.connector) { + onSubmit(newData.connector); + setIsEditConnector(false); + } + }, [form, onSubmit]); + return ( + + + +

{i18n.CONNECTORS}

+
+ {isLoading && } + {!isLoading && ( + + + + )} +
+ + + + +
+ + + + + +
+
+ {isEditConnector && ( + + + + + {i18n.SAVE} + + + + + {i18n.CANCEL} + + + + + )} +
+
+
+ ); + } +); + +EditConnector.displayName = 'EditConnector'; diff --git a/x-pack/plugins/siem/public/cases/components/edit_connector/schema.tsx b/x-pack/plugins/siem/public/cases/components/edit_connector/schema.tsx new file mode 100644 index 00000000000000..cdc50c7d28e4f1 --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/edit_connector/schema.tsx @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { FormSchema } from '../../../shared_imports'; + +export const schema: FormSchema = { + connector: { + defaultValue: 'none', + }, +}; diff --git a/x-pack/plugins/siem/public/components/filter_popover/index.tsx b/x-pack/plugins/siem/public/cases/components/filter_popover/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/filter_popover/index.tsx rename to x-pack/plugins/siem/public/cases/components/filter_popover/index.tsx diff --git a/x-pack/plugins/siem/public/pages/case/components/open_closed_stats/index.tsx b/x-pack/plugins/siem/public/cases/components/open_closed_stats/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/case/components/open_closed_stats/index.tsx rename to x-pack/plugins/siem/public/cases/components/open_closed_stats/index.tsx diff --git a/x-pack/plugins/siem/public/pages/case/components/property_actions/constants.ts b/x-pack/plugins/siem/public/cases/components/property_actions/constants.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/case/components/property_actions/constants.ts rename to x-pack/plugins/siem/public/cases/components/property_actions/constants.ts diff --git a/x-pack/plugins/siem/public/pages/case/components/property_actions/index.tsx b/x-pack/plugins/siem/public/cases/components/property_actions/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/case/components/property_actions/index.tsx rename to x-pack/plugins/siem/public/cases/components/property_actions/index.tsx diff --git a/x-pack/plugins/siem/public/pages/case/components/property_actions/translations.ts b/x-pack/plugins/siem/public/cases/components/property_actions/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/case/components/property_actions/translations.ts rename to x-pack/plugins/siem/public/cases/components/property_actions/translations.ts diff --git a/x-pack/plugins/siem/public/cases/components/tag_list/index.test.tsx b/x-pack/plugins/siem/public/cases/components/tag_list/index.test.tsx new file mode 100644 index 00000000000000..0b7b4211f6a3b5 --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/tag_list/index.test.tsx @@ -0,0 +1,180 @@ +/* + * 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 { act } from 'react-dom/test-utils'; + +import { TagList } from '.'; +import { getFormMock } from '../__mock__/form'; +import { TestProviders } from '../../../common/mock'; +import { wait } from '../../../common/lib/helpers'; +import { useForm } from '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'; +import { useGetTags } from '../../containers/use_get_tags'; + +jest.mock( + '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' +); +jest.mock('../../containers/use_get_tags'); +jest.mock( + '../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider', + () => ({ + FormDataProvider: ({ children }: { children: ({ tags }: { tags: string[] }) => void }) => + children({ tags: ['rad', 'dude'] }), + }) +); +const onSubmit = jest.fn(); +const defaultProps = { + disabled: false, + isLoading: false, + onSubmit, + tags: [], +}; + +describe('TagList ', () => { + // Suppress warnings about "noSuggestions" prop + /* eslint-disable no-console */ + const originalError = console.error; + beforeAll(() => { + console.error = jest.fn(); + }); + afterAll(() => { + console.error = originalError; + }); + /* eslint-enable no-console */ + const sampleTags = ['coke', 'pepsi']; + const fetchTags = jest.fn(); + const formHookMock = getFormMock({ tags: sampleTags }); + beforeEach(() => { + jest.resetAllMocks(); + (useForm as jest.Mock).mockImplementation(() => ({ form: formHookMock })); + + (useGetTags as jest.Mock).mockImplementation(() => ({ + tags: sampleTags, + fetchTags, + })); + }); + it('Renders no tags, and then edit', () => { + const wrapper = mount( + + + + ); + expect( + wrapper + .find(`[data-test-subj="no-tags"]`) + .last() + .exists() + ).toBeTruthy(); + wrapper + .find(`[data-test-subj="tag-list-edit-button"]`) + .last() + .simulate('click'); + expect( + wrapper + .find(`[data-test-subj="no-tags"]`) + .last() + .exists() + ).toBeFalsy(); + expect( + wrapper + .find(`[data-test-subj="edit-tags"]`) + .last() + .exists() + ).toBeTruthy(); + }); + it('Edit tag on submit', async () => { + const wrapper = mount( + + + + ); + wrapper + .find(`[data-test-subj="tag-list-edit-button"]`) + .last() + .simulate('click'); + await act(async () => { + wrapper + .find(`[data-test-subj="edit-tags-submit"]`) + .last() + .simulate('click'); + await wait(); + expect(onSubmit).toBeCalledWith(sampleTags); + }); + }); + it('Tag options render with new tags added', () => { + const wrapper = mount( + + + + ); + wrapper + .find(`[data-test-subj="tag-list-edit-button"]`) + .last() + .simulate('click'); + expect( + wrapper + .find(`[data-test-subj="caseTags"] [data-test-subj="input"]`) + .first() + .prop('options') + ).toEqual([{ label: 'coke' }, { label: 'pepsi' }, { label: 'rad' }, { label: 'dude' }]); + }); + it('Cancels on cancel', async () => { + const props = { + ...defaultProps, + tags: ['pepsi'], + }; + const wrapper = mount( + + + + ); + expect( + wrapper + .find(`[data-test-subj="case-tag"]`) + .last() + .exists() + ).toBeTruthy(); + wrapper + .find(`[data-test-subj="tag-list-edit-button"]`) + .last() + .simulate('click'); + await act(async () => { + expect( + wrapper + .find(`[data-test-subj="case-tag"]`) + .last() + .exists() + ).toBeFalsy(); + wrapper + .find(`[data-test-subj="edit-tags-cancel"]`) + .last() + .simulate('click'); + await wait(); + wrapper.update(); + expect( + wrapper + .find(`[data-test-subj="case-tag"]`) + .last() + .exists() + ).toBeTruthy(); + }); + }); + it('Renders disabled button', () => { + const props = { ...defaultProps, disabled: true }; + const wrapper = mount( + + + + ); + expect( + wrapper + .find(`[data-test-subj="tag-list-edit-button"]`) + .last() + .prop('disabled') + ).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/siem/public/cases/components/tag_list/index.tsx b/x-pack/plugins/siem/public/cases/components/tag_list/index.tsx new file mode 100644 index 00000000000000..259028d9c6363a --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/tag_list/index.tsx @@ -0,0 +1,179 @@ +/* + * 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, { useCallback, useEffect, useState } from 'react'; +import { + EuiText, + EuiHorizontalRule, + EuiFlexGroup, + EuiFlexItem, + EuiBadge, + EuiButton, + EuiButtonEmpty, + EuiButtonIcon, + EuiLoadingSpinner, +} from '@elastic/eui'; +import styled, { css } from 'styled-components'; +import { isEqual } from 'lodash/fp'; +import * as i18n from './translations'; +import { Form, FormDataProvider, useForm } from '../../../shared_imports'; +import { schema } from './schema'; +import { CommonUseField } from '../create'; +import { useGetTags } from '../../containers/use_get_tags'; + +interface TagListProps { + disabled?: boolean; + isLoading: boolean; + onSubmit: (a: string[]) => void; + tags: string[]; +} + +const MyFlexGroup = styled(EuiFlexGroup)` + ${({ theme }) => css` + margin-top: ${theme.eui.euiSizeM}; + p { + font-size: ${theme.eui.euiSizeM}; + } + `} +`; + +export const TagList = React.memo( + ({ disabled = false, isLoading, onSubmit, tags }: TagListProps) => { + const { form } = useForm({ + defaultValue: { tags }, + options: { stripEmptyFields: false }, + schema, + }); + const [isEditTags, setIsEditTags] = useState(false); + + const onSubmitTags = useCallback(async () => { + const { isValid, data: newData } = await form.submit(); + if (isValid && newData.tags) { + onSubmit(newData.tags); + setIsEditTags(false); + } + }, [form, onSubmit]); + const { tags: tagOptions } = useGetTags(); + const [options, setOptions] = useState( + tagOptions.map(label => ({ + label, + })) + ); + + useEffect( + () => + setOptions( + tagOptions.map(label => ({ + label, + })) + ), + [tagOptions] + ); + + return ( + + + +

{i18n.TAGS}

+
+ {isLoading && } + {!isLoading && ( + + + + )} +
+ + + {tags.length === 0 && !isEditTags &&

{i18n.NO_TAGS}

} + {tags.length > 0 && + !isEditTags && + tags.map((tag, key) => ( + + + {tag} + + + ))} + {isEditTags && ( + + +
+ + + {({ tags: anotherTags }) => { + const current: string[] = options.map(opt => opt.label); + const newOptions = anotherTags.reduce((acc: string[], item: string) => { + if (!acc.includes(item)) { + return [...acc, item]; + } + return acc; + }, current); + if (!isEqual(current, newOptions)) { + setOptions( + newOptions.map((label: string) => ({ + label, + })) + ); + } + return null; + }} + + +
+ + + + + {i18n.SAVE} + + + + + {i18n.CANCEL} + + + + +
+ )} +
+
+ ); + } +); + +TagList.displayName = 'TagList'; diff --git a/x-pack/plugins/siem/public/cases/components/tag_list/schema.tsx b/x-pack/plugins/siem/public/cases/components/tag_list/schema.tsx new file mode 100644 index 00000000000000..335a0785ecb046 --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/tag_list/schema.tsx @@ -0,0 +1,11 @@ +/* + * 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 { FormSchema } from '../../../shared_imports'; +import { schemaTags } from '../create/schema'; + +export const schema: FormSchema = { + tags: schemaTags, +}; diff --git a/x-pack/plugins/siem/public/pages/case/components/tag_list/translations.ts b/x-pack/plugins/siem/public/cases/components/tag_list/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/case/components/tag_list/translations.ts rename to x-pack/plugins/siem/public/cases/components/tag_list/translations.ts diff --git a/x-pack/plugins/siem/public/cases/components/use_push_to_service/helpers.tsx b/x-pack/plugins/siem/public/cases/components/use_push_to_service/helpers.tsx new file mode 100644 index 00000000000000..f0ded815fce434 --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/use_push_to_service/helpers.tsx @@ -0,0 +1,59 @@ +/* + * 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 { EuiLink } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; + +import * as i18n from './translations'; +import { ActionLicense } from '../../containers/types'; + +export const getLicenseError = () => ({ + title: i18n.PUSH_DISABLE_BY_LICENSE_TITLE, + description: ( + + {i18n.LINK_CLOUD_DEPLOYMENT} + + ), + }} + /> + ), +}); + +export const getKibanaConfigError = () => ({ + title: i18n.PUSH_DISABLE_BY_KIBANA_CONFIG_TITLE, + description: ( + + {'coming soon...'} + + ), + }} + /> + ), +}); + +export const getActionLicenseError = ( + actionLicense: ActionLicense | null +): Array<{ title: string; description: JSX.Element }> => { + let errors: Array<{ title: string; description: JSX.Element }> = []; + if (actionLicense != null && !actionLicense.enabledInLicense) { + errors = [...errors, getLicenseError()]; + } + if (actionLicense != null && !actionLicense.enabledInConfig) { + errors = [...errors, getKibanaConfigError()]; + } + return errors; +}; diff --git a/x-pack/plugins/siem/public/cases/components/use_push_to_service/index.test.tsx b/x-pack/plugins/siem/public/cases/components/use_push_to_service/index.test.tsx new file mode 100644 index 00000000000000..cb002019423127 --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/use_push_to_service/index.test.tsx @@ -0,0 +1,156 @@ +/* + * 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. + */ +/* eslint-disable react/display-name */ +import React from 'react'; +import { renderHook, act } from '@testing-library/react-hooks'; +import { usePushToService, ReturnUsePushToService, UsePushToService } from '.'; +import { TestProviders } from '../../../common/mock'; +import { usePostPushToService } from '../../containers/use_post_push_to_service'; +import { basicPush, actionLicenses } from '../../containers/mock'; +import * as i18n from './translations'; +import { useGetActionLicense } from '../../containers/use_get_action_license'; +import { getKibanaConfigError, getLicenseError } from './helpers'; +import { connectorsMock } from '../../containers/configure/mock'; +jest.mock('../../containers/use_get_action_license'); +jest.mock('../../containers/use_post_push_to_service'); +jest.mock('../../containers/configure/api'); + +describe('usePushToService', () => { + const caseId = '12345'; + const updateCase = jest.fn(); + const postPushToService = jest.fn(); + const mockPostPush = { + isLoading: false, + postPushToService, + }; + const mockConnector = connectorsMock[0]; + const actionLicense = actionLicenses[0]; + const caseServices = { + '123': { + ...basicPush, + firstPushIndex: 0, + lastPushIndex: 0, + commentsToUpdate: [], + hasDataToPush: true, + }, + }; + const defaultArgs = { + caseConnectorId: mockConnector.id, + caseConnectorName: mockConnector.name, + caseId, + caseServices, + caseStatus: 'open', + connectors: connectorsMock, + updateCase, + userCanCrud: true, + }; + beforeEach(() => { + jest.resetAllMocks(); + (usePostPushToService as jest.Mock).mockImplementation(() => mockPostPush); + (useGetActionLicense as jest.Mock).mockImplementation(() => ({ + isLoading: false, + actionLicense, + })); + }); + it('push case button posts the push with correct args', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => usePushToService(defaultArgs), + { + wrapper: ({ children }) => {children}, + } + ); + await waitForNextUpdate(); + result.current.pushButton.props.children.props.onClick(); + expect(postPushToService).toBeCalledWith({ + caseId, + caseServices, + connectorId: mockConnector.id, + connectorName: mockConnector.name, + updateCase, + }); + expect(result.current.pushCallouts).toBeNull(); + }); + }); + it('Displays message when user does not have premium license', async () => { + (useGetActionLicense as jest.Mock).mockImplementation(() => ({ + isLoading: false, + actionLicense: { + ...actionLicense, + enabledInLicense: false, + }, + })); + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => usePushToService(defaultArgs), + { + wrapper: ({ children }) => {children}, + } + ); + await waitForNextUpdate(); + const errorsMsg = result.current.pushCallouts?.props.messages; + expect(errorsMsg).toHaveLength(1); + expect(errorsMsg[0].title).toEqual(getLicenseError().title); + }); + }); + it('Displays message when user does not have case enabled in config', async () => { + (useGetActionLicense as jest.Mock).mockImplementation(() => ({ + isLoading: false, + actionLicense: { + ...actionLicense, + enabledInConfig: false, + }, + })); + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => usePushToService(defaultArgs), + { + wrapper: ({ children }) => {children}, + } + ); + await waitForNextUpdate(); + const errorsMsg = result.current.pushCallouts?.props.messages; + expect(errorsMsg).toHaveLength(1); + expect(errorsMsg[0].title).toEqual(getKibanaConfigError().title); + }); + }); + it('Displays message when user does not have a connector configured', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + usePushToService({ + ...defaultArgs, + caseConnectorId: 'none', + }), + { + wrapper: ({ children }) => {children}, + } + ); + await waitForNextUpdate(); + const errorsMsg = result.current.pushCallouts?.props.messages; + expect(errorsMsg).toHaveLength(1); + expect(errorsMsg[0].title).toEqual(i18n.PUSH_DISABLE_BY_NO_CASE_CONFIG_TITLE); + }); + }); + it('Displays message when case is closed', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook( + () => + usePushToService({ + ...defaultArgs, + caseStatus: 'closed', + }), + { + wrapper: ({ children }) => {children}, + } + ); + await waitForNextUpdate(); + const errorsMsg = result.current.pushCallouts?.props.messages; + expect(errorsMsg).toHaveLength(1); + expect(errorsMsg[0].title).toEqual(i18n.PUSH_DISABLE_BECAUSE_CASE_CLOSED_TITLE); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/cases/components/use_push_to_service/index.tsx b/x-pack/plugins/siem/public/cases/components/use_push_to_service/index.tsx new file mode 100644 index 00000000000000..157639f011fef8 --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/use_push_to_service/index.tsx @@ -0,0 +1,174 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButton, EuiLink, EuiToolTip } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useCallback, useMemo } from 'react'; + +import { Case } from '../../containers/types'; +import { useGetActionLicense } from '../../containers/use_get_action_license'; +import { usePostPushToService } from '../../containers/use_post_push_to_service'; +import { getConfigureCasesUrl } from '../../../common/components/link_to'; +import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search'; +import { navTabs } from '../../../app/home/home_navigations'; +import { CaseCallOut } from '../callout'; +import { getLicenseError, getKibanaConfigError } from './helpers'; +import * as i18n from './translations'; +import { Connector } from '../../../../../case/common/api/cases'; +import { CaseServices } from '../../containers/use_get_case_user_actions'; + +export interface UsePushToService { + caseId: string; + caseStatus: string; + caseConnectorId: string; + caseConnectorName: string; + caseServices: CaseServices; + connectors: Connector[]; + updateCase: (newCase: Case) => void; + userCanCrud: boolean; +} + +export interface ReturnUsePushToService { + pushButton: JSX.Element; + pushCallouts: JSX.Element | null; +} + +export const usePushToService = ({ + caseConnectorId, + caseConnectorName, + caseId, + caseServices, + caseStatus, + connectors, + updateCase, + userCanCrud, +}: UsePushToService): ReturnUsePushToService => { + const urlSearch = useGetUrlSearch(navTabs.case); + + const { isLoading, postPushToService } = usePostPushToService(); + + const { isLoading: loadingLicense, actionLicense } = useGetActionLicense(); + + const handlePushToService = useCallback(() => { + if (caseConnectorId != null && caseConnectorId !== 'none') { + postPushToService({ + caseId, + caseServices, + connectorId: caseConnectorId, + connectorName: caseConnectorName, + updateCase, + }); + } + }, [caseId, caseServices, caseConnectorId, caseConnectorName, postPushToService, updateCase]); + + const errorsMsg = useMemo(() => { + let errors: Array<{ title: string; description: JSX.Element }> = []; + if (actionLicense != null && !actionLicense.enabledInLicense) { + errors = [...errors, getLicenseError()]; + } + if (connectors.length === 0 && !loadingLicense) { + errors = [ + ...errors, + { + title: i18n.PUSH_DISABLE_BY_NO_CONFIG_TITLE, + description: ( + + {i18n.LINK_CONNECTOR_CONFIGURE} + + ), + }} + /> + ), + }, + ]; + } else if (caseConnectorId === 'none' && !loadingLicense) { + errors = [ + ...errors, + { + title: i18n.PUSH_DISABLE_BY_NO_CASE_CONFIG_TITLE, + description: ( + + ), + }, + ]; + } + if (caseStatus === 'closed') { + errors = [ + ...errors, + { + title: i18n.PUSH_DISABLE_BECAUSE_CASE_CLOSED_TITLE, + description: ( + + ), + }, + ]; + } + if (actionLicense != null && !actionLicense.enabledInConfig) { + errors = [...errors, getKibanaConfigError()]; + } + return errors; + }, [actionLicense, caseStatus, connectors.length, caseConnectorId, loadingLicense, urlSearch]); + + const pushToServiceButton = useMemo(() => { + return ( + 0 || !userCanCrud} + isLoading={isLoading} + > + {caseServices[caseConnectorId] + ? i18n.UPDATE_THIRD(caseConnectorName) + : i18n.PUSH_THIRD(caseConnectorName)} + + ); + }, [ + caseConnectorId, + caseConnectorName, + connectors, + errorsMsg, + handlePushToService, + isLoading, + loadingLicense, + userCanCrud, + ]); + + const objToReturn = useMemo(() => { + return { + pushButton: + errorsMsg.length > 0 ? ( + {errorsMsg[0].description}

} + > + {pushToServiceButton} +
+ ) : ( + <>{pushToServiceButton} + ), + pushCallouts: + errorsMsg.length > 0 ? ( + + ) : null, + }; + }, [errorsMsg, pushToServiceButton]); + + return objToReturn; +}; diff --git a/x-pack/plugins/siem/public/pages/case/components/use_push_to_service/translations.ts b/x-pack/plugins/siem/public/cases/components/use_push_to_service/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/case/components/use_push_to_service/translations.ts rename to x-pack/plugins/siem/public/cases/components/use_push_to_service/translations.ts diff --git a/x-pack/plugins/siem/public/cases/components/user_action_tree/helpers.test.tsx b/x-pack/plugins/siem/public/cases/components/user_action_tree/helpers.test.tsx new file mode 100644 index 00000000000000..678bd54975144b --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/user_action_tree/helpers.test.tsx @@ -0,0 +1,169 @@ +/* + * 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 { basicPush, getUserAction } from '../../containers/mock'; +import { getLabelTitle } from './helpers'; +import * as i18n from '../case_view/translations'; +import { mount } from 'enzyme'; +import { connectorsMock } from '../../containers/configure/mock'; + +describe('User action tree helpers', () => { + const connectors = connectorsMock; + it('label title generated for update tags', () => { + const action = getUserAction(['title'], 'update'); + const result: string | JSX.Element = getLabelTitle({ + action, + connectors, + field: 'tags', + firstPush: false, + }); + + const wrapper = mount(<>{result}); + expect( + wrapper + .find(`[data-test-subj="ua-tags-label"]`) + .first() + .text() + ).toEqual(` ${i18n.TAGS.toLowerCase()}`); + + expect( + wrapper + .find(`[data-test-subj="ua-tag"]`) + .first() + .text() + ).toEqual(action.newValue); + }); + it('label title generated for update title', () => { + const action = getUserAction(['title'], 'update'); + const result: string | JSX.Element = getLabelTitle({ + action, + connectors, + field: 'title', + firstPush: false, + }); + + expect(result).toEqual( + `${i18n.CHANGED_FIELD.toLowerCase()} ${i18n.CASE_NAME.toLowerCase()} ${i18n.TO} "${ + action.newValue + }"` + ); + }); + it('label title generated for update description', () => { + const action = getUserAction(['description'], 'update'); + const result: string | JSX.Element = getLabelTitle({ + action, + connectors, + field: 'description', + firstPush: false, + }); + + expect(result).toEqual(`${i18n.EDITED_FIELD} ${i18n.DESCRIPTION.toLowerCase()}`); + }); + it('label title generated for update status to open', () => { + const action = { ...getUserAction(['status'], 'update'), newValue: 'open' }; + const result: string | JSX.Element = getLabelTitle({ + action, + connectors, + field: 'status', + firstPush: false, + }); + + expect(result).toEqual(`${i18n.REOPENED_CASE.toLowerCase()} ${i18n.CASE}`); + }); + it('label title generated for update status to closed', () => { + const action = { ...getUserAction(['status'], 'update'), newValue: 'closed' }; + const result: string | JSX.Element = getLabelTitle({ + action, + connectors, + field: 'status', + firstPush: false, + }); + + expect(result).toEqual(`${i18n.CLOSED_CASE.toLowerCase()} ${i18n.CASE}`); + }); + it('label title generated for update comment', () => { + const action = getUserAction(['comment'], 'update'); + const result: string | JSX.Element = getLabelTitle({ + action, + connectors, + field: 'comment', + firstPush: false, + }); + + expect(result).toEqual(`${i18n.EDITED_FIELD} ${i18n.COMMENT.toLowerCase()}`); + }); + it('label title generated for pushed incident', () => { + const action = getUserAction(['pushed'], 'push-to-service'); + const result: string | JSX.Element = getLabelTitle({ + action, + connectors, + field: 'pushed', + firstPush: true, + }); + + const wrapper = mount(<>{result}); + expect( + wrapper + .find(`[data-test-subj="pushed-label"]`) + .first() + .text() + ).toEqual(`${i18n.PUSHED_NEW_INCIDENT} ${basicPush.connectorName}`); + expect( + wrapper + .find(`[data-test-subj="pushed-value"]`) + .first() + .prop('href') + ).toEqual(JSON.parse(action.newValue).external_url); + }); + it('label title generated for needs update incident', () => { + const action = getUserAction(['pushed'], 'push-to-service'); + const result: string | JSX.Element = getLabelTitle({ + action, + connectors, + field: 'pushed', + firstPush: false, + }); + + const wrapper = mount(<>{result}); + expect( + wrapper + .find(`[data-test-subj="pushed-label"]`) + .first() + .text() + ).toEqual(`${i18n.UPDATE_INCIDENT} ${basicPush.connectorName}`); + expect( + wrapper + .find(`[data-test-subj="pushed-value"]`) + .first() + .prop('href') + ).toEqual(JSON.parse(action.newValue).external_url); + }); + it('label title generated for update connector', () => { + const action = getUserAction(['connector_id'], 'update'); + const result: string | JSX.Element = getLabelTitle({ + action, + connectors, + field: 'tags', + firstPush: false, + }); + + const wrapper = mount(<>{result}); + expect( + wrapper + .find(`[data-test-subj="ua-tags-label"]`) + .first() + .text() + ).toEqual(` ${i18n.TAGS.toLowerCase()}`); + + expect( + wrapper + .find(`[data-test-subj="ua-tag"]`) + .first() + .text() + ).toEqual(action.newValue); + }); +}); diff --git a/x-pack/plugins/siem/public/cases/components/user_action_tree/helpers.tsx b/x-pack/plugins/siem/public/cases/components/user_action_tree/helpers.tsx new file mode 100644 index 00000000000000..58c176ed96b5d2 --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/user_action_tree/helpers.tsx @@ -0,0 +1,80 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiBadge, EuiLink } from '@elastic/eui'; +import React from 'react'; + +import { CaseFullExternalService, Connector } from '../../../../../case/common/api'; +import { CaseUserActions } from '../../containers/types'; +import * as i18n from '../case_view/translations'; + +interface LabelTitle { + action: CaseUserActions; + connectors: Connector[]; + field: string; + firstPush: boolean; +} + +export const getLabelTitle = ({ action, connectors, field, firstPush }: LabelTitle) => { + if (field === 'tags') { + return getTagsLabelTitle(action); + } else if (field === 'title' && action.action === 'update') { + return `${i18n.CHANGED_FIELD.toLowerCase()} ${i18n.CASE_NAME.toLowerCase()} ${i18n.TO} "${ + action.newValue + }"`; + } else if (field === 'connector_id' && action.action === 'update') { + const newConnector = connectors.find(c => c.id === action.newValue); + return action.newValue != null && action.newValue !== 'none' && newConnector != null + ? i18n.SELECTED_THIRD_PARTY(newConnector.name) + : i18n.REMOVED_THIRD_PARTY; + } else if (field === 'description' && action.action === 'update') { + return `${i18n.EDITED_FIELD} ${i18n.DESCRIPTION.toLowerCase()}`; + } else if (field === 'status' && action.action === 'update') { + return `${ + action.newValue === 'open' ? i18n.REOPENED_CASE.toLowerCase() : i18n.CLOSED_CASE.toLowerCase() + } ${i18n.CASE}`; + } else if (field === 'comment' && action.action === 'update') { + return `${i18n.EDITED_FIELD} ${i18n.COMMENT.toLowerCase()}`; + } else if (field === 'pushed' && action.action === 'push-to-service' && action.newValue != null) { + return getPushedServiceLabelTitle(action, firstPush); + } + return ''; +}; + +const getTagsLabelTitle = (action: CaseUserActions) => ( + + + {action.action === 'add' && i18n.ADDED_FIELD} + {action.action === 'delete' && i18n.REMOVED_FIELD} {i18n.TAGS.toLowerCase()} + + {action.newValue != null && + action.newValue.split(',').map(tag => ( + + + {tag} + + + ))} + +); + +const getPushedServiceLabelTitle = (action: CaseUserActions, firstPush: boolean) => { + const pushedVal = JSON.parse(action.newValue ?? '') as CaseFullExternalService; + return ( + + + {`${firstPush ? i18n.PUSHED_NEW_INCIDENT : i18n.UPDATE_INCIDENT} ${ + pushedVal?.connector_name + }`} + + + + {pushedVal?.external_title} + + + + ); +}; diff --git a/x-pack/plugins/siem/public/cases/components/user_action_tree/index.test.tsx b/x-pack/plugins/siem/public/cases/components/user_action_tree/index.test.tsx new file mode 100644 index 00000000000000..d3e8ea6563b2c5 --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/user_action_tree/index.test.tsx @@ -0,0 +1,343 @@ +/* + * 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 { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; +import { getFormMock, useFormMock } from '../__mock__/form'; +import { useUpdateComment } from '../../containers/use_update_comment'; +import { basicCase, basicPush, getUserAction } from '../../containers/mock'; +import { UserActionTree } from '.'; +import { TestProviders } from '../../../common/mock'; +import { wait } from '../../../common/lib/helpers'; +import { act } from 'react-dom/test-utils'; + +const fetchUserActions = jest.fn(); +const onUpdateField = jest.fn(); +const updateCase = jest.fn(); +const defaultProps = { + caseServices: {}, + caseUserActions: [], + connectors: [], + data: basicCase, + fetchUserActions, + isLoadingDescription: false, + isLoadingUserActions: false, + onUpdateField, + updateCase, + userCanCrud: true, +}; +const useUpdateCommentMock = useUpdateComment as jest.Mock; +jest.mock('../../containers/use_update_comment'); + +const patchComment = jest.fn(); +describe('UserActionTree ', () => { + const sampleData = { + content: 'what a great comment update', + }; + beforeEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + useUpdateCommentMock.mockImplementation(() => ({ + isLoadingIds: [], + patchComment, + })); + const formHookMock = getFormMock(sampleData); + useFormMock.mockImplementation(() => ({ form: formHookMock })); + jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); + }); + + it('Loading spinner when user actions loading and displays fullName/username', () => { + const wrapper = mount( + + + + + + ); + expect(wrapper.find(`[data-test-subj="user-actions-loading"]`).exists()).toBeTruthy(); + + expect( + wrapper + .find(`[data-test-subj="user-action-avatar"]`) + .first() + .prop('name') + ).toEqual(defaultProps.data.createdBy.fullName); + expect( + wrapper + .find(`[data-test-subj="user-action-title"] strong`) + .first() + .text() + ).toEqual(defaultProps.data.createdBy.username); + }); + it('Renders service now update line with top and bottom when push is required', () => { + const ourActions = [ + getUserAction(['pushed'], 'push-to-service'), + getUserAction(['comment'], 'update'), + ]; + const props = { + ...defaultProps, + caseServices: { + '123': { + ...basicPush, + firstPushIndex: 0, + lastPushIndex: 0, + commentsToUpdate: [`${ourActions[ourActions.length - 1].commentId}`], + hasDataToPush: true, + }, + }, + caseUserActions: ourActions, + }; + const wrapper = mount( + + + + + + ); + expect(wrapper.find(`[data-test-subj="show-top-footer"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="show-bottom-footer"]`).exists()).toBeTruthy(); + }); + it('Renders service now update line with top only when push is up to date', () => { + const ourActions = [getUserAction(['pushed'], 'push-to-service')]; + const props = { + ...defaultProps, + caseUserActions: ourActions, + caseServices: { + '123': { + ...basicPush, + firstPushIndex: 0, + lastPushIndex: 0, + commentsToUpdate: [], + hasDataToPush: false, + }, + }, + }; + const wrapper = mount( + + + + + + ); + expect(wrapper.find(`[data-test-subj="show-top-footer"]`).exists()).toBeTruthy(); + expect(wrapper.find(`[data-test-subj="show-bottom-footer"]`).exists()).toBeFalsy(); + }); + + it('Outlines comment when update move to link is clicked', () => { + const ourActions = [getUserAction(['comment'], 'create'), getUserAction(['comment'], 'update')]; + const props = { + ...defaultProps, + caseUserActions: ourActions, + }; + const wrapper = mount( + + + + + + ); + expect( + wrapper + .find(`[data-test-subj="comment-create-action"]`) + .first() + .prop('idToOutline') + ).toEqual(''); + wrapper + .find(`[data-test-subj="comment-update-action"] [data-test-subj="move-to-link"]`) + .first() + .simulate('click'); + expect( + wrapper + .find(`[data-test-subj="comment-create-action"]`) + .first() + .prop('idToOutline') + ).toEqual(ourActions[0].commentId); + }); + + it('Switches to markdown when edit is clicked and back to panel when canceled', () => { + const ourActions = [getUserAction(['comment'], 'create')]; + const props = { + ...defaultProps, + caseUserActions: ourActions, + }; + const wrapper = mount( + + + + + + ); + expect( + wrapper + .find( + `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]` + ) + .exists() + ).toEqual(false); + wrapper + .find(`[data-test-subj="comment-create-action"] [data-test-subj="property-actions-ellipses"]`) + .first() + .simulate('click'); + wrapper + .find(`[data-test-subj="comment-create-action"] [data-test-subj="property-actions-pencil"]`) + .first() + .simulate('click'); + expect( + wrapper + .find( + `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]` + ) + .exists() + ).toEqual(true); + wrapper + .find( + `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-cancel-markdown"]` + ) + .first() + .simulate('click'); + expect( + wrapper + .find( + `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]` + ) + .exists() + ).toEqual(false); + }); + + it('calls update comment when comment markdown is saved', async () => { + const ourActions = [getUserAction(['comment'], 'create')]; + const props = { + ...defaultProps, + caseUserActions: ourActions, + }; + const wrapper = mount( + + + + + + ); + wrapper + .find(`[data-test-subj="comment-create-action"] [data-test-subj="property-actions-ellipses"]`) + .first() + .simulate('click'); + wrapper + .find(`[data-test-subj="comment-create-action"] [data-test-subj="property-actions-pencil"]`) + .first() + .simulate('click'); + wrapper + .find( + `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-save-markdown"]` + ) + .first() + .simulate('click'); + await act(async () => { + await wait(); + wrapper.update(); + expect( + wrapper + .find( + `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]` + ) + .exists() + ).toEqual(false); + expect(patchComment).toBeCalledWith({ + commentUpdate: sampleData.content, + caseId: props.data.id, + commentId: props.data.comments[0].id, + fetchUserActions, + updateCase, + version: props.data.comments[0].version, + }); + }); + }); + + it('calls update description when description markdown is saved', async () => { + const props = defaultProps; + const wrapper = mount( + + + + + + ); + wrapper + .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-ellipses"]`) + .first() + .simulate('click'); + wrapper + .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-pencil"]`) + .first() + .simulate('click'); + wrapper + .find( + `[data-test-subj="user-action-description"] [data-test-subj="user-action-save-markdown"]` + ) + .first() + .simulate('click'); + await act(async () => { + await wait(); + expect( + wrapper + .find( + `[data-test-subj="user-action-${props.data.id}"] [data-test-subj="user-action-markdown-form"]` + ) + .exists() + ).toEqual(false); + expect(onUpdateField).toBeCalledWith('description', sampleData.content); + }); + }); + + it('quotes', async () => { + const commentData = { + comment: '', + }; + const formHookMock = getFormMock(commentData); + const setFieldValue = jest.fn(); + useFormMock.mockImplementation(() => ({ form: { ...formHookMock, setFieldValue } })); + const props = defaultProps; + const wrapper = mount( + + + + + + ); + wrapper + .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-ellipses"]`) + .first() + .simulate('click'); + wrapper + .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-quote"]`) + .first() + .simulate('click'); + expect(setFieldValue).toBeCalledWith('comment', `> ${props.data.description} \n`); + }); + it('Outlines comment when url param is provided', () => { + const commentId = 'neat-comment-id'; + const ourActions = [getUserAction(['comment'], 'create')]; + const props = { + ...defaultProps, + caseUserActions: ourActions, + }; + jest.spyOn(routeData, 'useParams').mockReturnValue({ commentId }); + const wrapper = mount( + + + + + + ); + expect( + wrapper + .find(`[data-test-subj="comment-create-action"]`) + .first() + .prop('idToOutline') + ).toEqual(commentId); + }); +}); diff --git a/x-pack/plugins/siem/public/cases/components/user_action_tree/index.tsx b/x-pack/plugins/siem/public/cases/components/user_action_tree/index.tsx new file mode 100644 index 00000000000000..3a909636bc0484 --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/user_action_tree/index.tsx @@ -0,0 +1,303 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; +import React, { useCallback, useMemo, useRef, useState, useEffect } from 'react'; +import { useParams } from 'react-router-dom'; +import styled from 'styled-components'; + +import * as i18n from '../case_view/translations'; + +import { Case, CaseUserActions } from '../../containers/types'; +import { useUpdateComment } from '../../containers/use_update_comment'; +import { useCurrentUser } from '../../../common/lib/kibana'; +import { AddComment } from '../add_comment'; +import { getLabelTitle } from './helpers'; +import { UserActionItem } from './user_action_item'; +import { UserActionMarkdown } from './user_action_markdown'; +import { Connector } from '../../../../../case/common/api/cases'; +import { CaseServices } from '../../containers/use_get_case_user_actions'; +import { parseString } from '../../containers/utils'; + +export interface UserActionTreeProps { + caseServices: CaseServices; + caseUserActions: CaseUserActions[]; + connectors: Connector[]; + data: Case; + fetchUserActions: () => void; + isLoadingDescription: boolean; + isLoadingUserActions: boolean; + onUpdateField: (updateKey: keyof Case, updateValue: string | string[]) => void; + updateCase: (newCase: Case) => void; + userCanCrud: boolean; +} + +const MyEuiFlexGroup = styled(EuiFlexGroup)` + margin-bottom: 8px; +`; + +const DESCRIPTION_ID = 'description'; +const NEW_ID = 'newComment'; + +export const UserActionTree = React.memo( + ({ + data: caseData, + caseServices, + caseUserActions, + connectors, + fetchUserActions, + isLoadingDescription, + isLoadingUserActions, + onUpdateField, + updateCase, + userCanCrud, + }: UserActionTreeProps) => { + const { commentId } = useParams(); + const handlerTimeoutId = useRef(0); + const [initLoading, setInitLoading] = useState(true); + const [selectedOutlineCommentId, setSelectedOutlineCommentId] = useState(''); + const { isLoadingIds, patchComment } = useUpdateComment(); + const currentUser = useCurrentUser(); + const [manageMarkdownEditIds, setManangeMardownEditIds] = useState([]); + const [insertQuote, setInsertQuote] = useState(null); + const handleManageMarkdownEditId = useCallback( + (id: string) => { + if (!manageMarkdownEditIds.includes(id)) { + setManangeMardownEditIds([...manageMarkdownEditIds, id]); + } else { + setManangeMardownEditIds(manageMarkdownEditIds.filter(myId => id !== myId)); + } + }, + [manageMarkdownEditIds] + ); + + const handleSaveComment = useCallback( + ({ id, version }: { id: string; version: string }, content: string) => { + patchComment({ + caseId: caseData.id, + commentId: id, + commentUpdate: content, + fetchUserActions, + version, + updateCase, + }); + }, + [caseData, handleManageMarkdownEditId, patchComment, updateCase] + ); + + const handleOutlineComment = useCallback( + (id: string) => { + const moveToTarget = document.getElementById(`${id}-permLink`); + if (moveToTarget != null) { + const yOffset = -60; + const y = moveToTarget.getBoundingClientRect().top + window.pageYOffset + yOffset; + window.scrollTo({ + top: y, + behavior: 'smooth', + }); + if (id === 'add-comment') { + moveToTarget.getElementsByTagName('textarea')[0].focus(); + } + } + window.clearTimeout(handlerTimeoutId.current); + setSelectedOutlineCommentId(id); + handlerTimeoutId.current = window.setTimeout(() => { + setSelectedOutlineCommentId(''); + window.clearTimeout(handlerTimeoutId.current); + }, 2400); + }, + [handlerTimeoutId.current] + ); + + const handleManageQuote = useCallback( + (quote: string) => { + const addCarrots = quote.replace(new RegExp('\r?\n', 'g'), ' \n> '); + setInsertQuote(`> ${addCarrots} \n`); + handleOutlineComment('add-comment'); + }, + [handleOutlineComment] + ); + + const handleUpdate = useCallback( + (newCase: Case) => { + updateCase(newCase); + fetchUserActions(); + }, + [fetchUserActions, updateCase] + ); + + const MarkdownDescription = useMemo( + () => ( + { + onUpdateField(DESCRIPTION_ID, content); + }} + onChangeEditable={handleManageMarkdownEditId} + /> + ), + [caseData.description, handleManageMarkdownEditId, manageMarkdownEditIds, onUpdateField] + ); + + const MarkdownNewComment = useMemo( + () => ( + + ), + [caseData.id, handleUpdate, insertQuote, userCanCrud] + ); + + useEffect(() => { + if (initLoading && !isLoadingUserActions && isLoadingIds.length === 0) { + setInitLoading(false); + if (commentId != null) { + handleOutlineComment(commentId); + } + } + }, [commentId, initLoading, isLoadingUserActions, isLoadingIds]); + return ( + <> + {i18n.ADDED_DESCRIPTION}} + fullName={caseData.createdBy.fullName ?? caseData.createdBy.username ?? ''} + markdown={MarkdownDescription} + onEdit={handleManageMarkdownEditId.bind(null, DESCRIPTION_ID)} + onQuote={handleManageQuote.bind(null, caseData.description)} + username={caseData.createdBy.username ?? i18n.UNKNOWN} + /> + + {caseUserActions.map((action, index) => { + if (action.commentId != null && action.action === 'create') { + const comment = caseData.comments.find(c => c.id === action.commentId); + if (comment != null) { + return ( + {i18n.ADDED_COMMENT}} + fullName={comment.createdBy.fullName ?? comment.createdBy.username ?? ''} + markdown={ + + } + onEdit={handleManageMarkdownEditId.bind(null, comment.id)} + onQuote={handleManageQuote.bind(null, comment.comment)} + outlineComment={handleOutlineComment} + username={comment.createdBy.username ?? ''} + updatedAt={comment.updatedAt} + /> + ); + } + } + if (action.actionField.length === 1) { + const myField = action.actionField[0]; + const parsedValue = parseString(`${action.newValue}`); + const { firstPush, parsedConnectorId, parsedConnectorName } = + parsedValue != null + ? { + firstPush: caseServices[parsedValue.connector_id].firstPushIndex === index, + parsedConnectorId: parsedValue.connector_id, + parsedConnectorName: parsedValue.connector_name, + } + : { + firstPush: false, + parsedConnectorId: 'none', + parsedConnectorName: 'none', + }; + const labelTitle: string | JSX.Element = getLabelTitle({ + action, + field: myField, + firstPush, + connectors, + }); + + return ( + {labelTitle}} + linkId={ + action.action === 'update' && action.commentId != null ? action.commentId : null + } + fullName={action.actionBy.fullName ?? action.actionBy.username ?? ''} + outlineComment={handleOutlineComment} + showTopFooter={ + action.action === 'push-to-service' && + index === caseServices[parsedConnectorId].lastPushIndex + } + showBottomFooter={ + action.action === 'push-to-service' && + index === caseServices[parsedConnectorId].lastPushIndex && + caseServices[parsedConnectorId].hasDataToPush + } + username={action.actionBy.username ?? ''} + /> + ); + } + return null; + })} + {(isLoadingUserActions || isLoadingIds.includes(NEW_ID)) && ( + + + + + + )} + + + ); + } +); + +UserActionTree.displayName = 'UserActionTree'; diff --git a/x-pack/plugins/siem/public/pages/case/components/user_action_tree/schema.ts b/x-pack/plugins/siem/public/cases/components/user_action_tree/schema.ts similarity index 96% rename from x-pack/plugins/siem/public/pages/case/components/user_action_tree/schema.ts rename to x-pack/plugins/siem/public/cases/components/user_action_tree/schema.ts index a9e6bf84a1a1e0..7a2777037023ad 100644 --- a/x-pack/plugins/siem/public/pages/case/components/user_action_tree/schema.ts +++ b/x-pack/plugins/siem/public/cases/components/user_action_tree/schema.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FIELD_TYPES, fieldValidators, FormSchema } from '../../../../shared_imports'; +import { FIELD_TYPES, fieldValidators, FormSchema } from '../../../shared_imports'; import * as i18n from '../../translations'; const { emptyField } = fieldValidators; diff --git a/x-pack/plugins/siem/public/pages/case/components/user_action_tree/translations.ts b/x-pack/plugins/siem/public/cases/components/user_action_tree/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/case/components/user_action_tree/translations.ts rename to x-pack/plugins/siem/public/cases/components/user_action_tree/translations.ts diff --git a/x-pack/plugins/siem/public/pages/case/components/user_action_tree/user_action_avatar.tsx b/x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_avatar.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/case/components/user_action_tree/user_action_avatar.tsx rename to x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_avatar.tsx diff --git a/x-pack/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx b/x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_item.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/case/components/user_action_tree/user_action_item.tsx rename to x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_item.tsx diff --git a/x-pack/plugins/siem/public/pages/case/components/user_action_tree/user_action_markdown.tsx b/x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_markdown.tsx similarity index 87% rename from x-pack/plugins/siem/public/pages/case/components/user_action_tree/user_action_markdown.tsx rename to x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_markdown.tsx index 827fe2df120abd..23d8d8f1a7e680 100644 --- a/x-pack/plugins/siem/public/pages/case/components/user_action_tree/user_action_markdown.tsx +++ b/x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_markdown.tsx @@ -9,12 +9,12 @@ import React, { useCallback } from 'react'; import styled, { css } from 'styled-components'; import * as i18n from '../case_view/translations'; -import { Markdown } from '../../../../components/markdown'; -import { Form, useForm, UseField } from '../../../../shared_imports'; +import { Markdown } from '../../../common/components/markdown'; +import { Form, useForm, UseField } from '../../../shared_imports'; import { schema, Content } from './schema'; -import { InsertTimelinePopover } from '../../../../components/timeline/insert_timeline_popover'; -import { useInsertTimeline } from '../../../../components/timeline/insert_timeline_popover/use_insert_timeline'; -import { MarkdownEditorForm } from '../../../../components/markdown_editor/form'; +import { InsertTimelinePopover } from '../../../timelines/components/timeline/insert_timeline_popover'; +import { useInsertTimeline } from '../../../timelines/components/timeline/insert_timeline_popover/use_insert_timeline'; +import { MarkdownEditorForm } from '../../../common/components//markdown_editor/form'; const ContentWrapper = styled.div` ${({ theme }) => css` diff --git a/x-pack/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.test.tsx b/x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_title.test.tsx similarity index 92% rename from x-pack/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.test.tsx rename to x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_title.test.tsx index 8a1e8a80f664de..cf29fa061e4192 100644 --- a/x-pack/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.test.tsx +++ b/x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_title.test.tsx @@ -8,9 +8,9 @@ import React from 'react'; import { mount } from 'enzyme'; import copy from 'copy-to-clipboard'; import { Router, routeData, mockHistory } from '../__mock__/router'; -import { caseUserActions as basicUserActions } from '../../../../containers/case/mock'; +import { caseUserActions as basicUserActions } from '../../containers/mock'; import { UserActionTitle } from './user_action_title'; -import { TestProviders } from '../../../../mock'; +import { TestProviders } from '../../../common/mock'; const outlineComment = jest.fn(); const onEdit = jest.fn(); diff --git a/x-pack/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx b/x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_title.tsx similarity index 95% rename from x-pack/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx rename to x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_title.tsx index fc2a74466dedc6..307790194421d3 100644 --- a/x-pack/plugins/siem/public/pages/case/components/user_action_tree/user_action_title.tsx +++ b/x-pack/plugins/siem/public/cases/components/user_action_tree/user_action_title.tsx @@ -19,11 +19,11 @@ import React, { useMemo, useCallback } from 'react'; import styled from 'styled-components'; import { useParams } from 'react-router-dom'; -import { LocalizedDateTooltip } from '../../../../components/localized_date_tooltip'; -import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; -import { navTabs } from '../../../home/home_navigations'; +import { LocalizedDateTooltip } from '../../../common/components/localized_date_tooltip'; +import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search'; +import { navTabs } from '../../../app/home/home_navigations'; import { PropertyActions } from '../property_actions'; -import { SiemPageName } from '../../../home/types'; +import { SiemPageName } from '../../../app/types'; import * as i18n from './translations'; const MySpinner = styled(EuiLoadingSpinner)` diff --git a/x-pack/plugins/siem/public/cases/components/user_list/index.test.tsx b/x-pack/plugins/siem/public/cases/components/user_list/index.test.tsx new file mode 100644 index 00000000000000..7916a72d591ad3 --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/user_list/index.test.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; +import { UserList } from '.'; +import * as i18n from '../case_view/translations'; + +describe('UserList ', () => { + const title = 'Case Title'; + const caseLink = 'http://reddit.com'; + const user = { username: 'username', fullName: 'Full Name', email: 'testemail@elastic.co' }; + const open = jest.fn(); + beforeAll(() => { + window.open = open; + }); + beforeEach(() => { + jest.resetAllMocks(); + }); + it('triggers mailto when email icon clicked', () => { + const wrapper = shallow( + + ); + wrapper.find('[data-test-subj="user-list-email-button"]').simulate('click'); + expect(open).toBeCalledWith( + `mailto:${user.email}?subject=${i18n.EMAIL_SUBJECT(title)}&body=${i18n.EMAIL_BODY(caseLink)}`, + '_blank' + ); + }); +}); diff --git a/x-pack/plugins/siem/public/cases/components/user_list/index.tsx b/x-pack/plugins/siem/public/cases/components/user_list/index.tsx new file mode 100644 index 00000000000000..0606da371d16ab --- /dev/null +++ b/x-pack/plugins/siem/public/cases/components/user_list/index.tsx @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useCallback } from 'react'; +import { isEmpty } from 'lodash/fp'; + +import { + EuiButtonIcon, + EuiText, + EuiHorizontalRule, + EuiAvatar, + EuiFlexGroup, + EuiFlexItem, + EuiLoadingSpinner, + EuiToolTip, +} from '@elastic/eui'; + +import styled, { css } from 'styled-components'; + +import { ElasticUser } from '../../containers/types'; +import * as i18n from './translations'; + +interface UserListProps { + email: { + subject: string; + body: string; + }; + headline: string; + loading?: boolean; + users: ElasticUser[]; +} + +const MyAvatar = styled(EuiAvatar)` + top: -4px; +`; + +const MyFlexGroup = styled(EuiFlexGroup)` + ${({ theme }) => css` + margin-top: ${theme.eui.euiSizeM}; + `} +`; + +const renderUsers = ( + users: ElasticUser[], + handleSendEmail: (emailAddress: string | undefined | null) => void +) => + users.map(({ fullName, username, email }, key) => ( + + + + + + + + {fullName ? fullName : username ?? ''}

}> +

+ + {username} + +

+
+
+
+
+ + + +
+ )); + +export const UserList = React.memo(({ email, headline, loading, users }: UserListProps) => { + const handleSendEmail = useCallback( + (emailAddress: string | undefined | null) => { + if (emailAddress && emailAddress != null) { + window.open(`mailto:${emailAddress}?subject=${email.subject}&body=${email.body}`, '_blank'); + } + }, + [email.subject] + ); + return users.filter(({ username }) => username != null && username !== '').length > 0 ? ( + +

{headline}

+ + {loading && ( + + + + + + )} + {renderUsers( + users.filter(({ username }) => username != null && username !== ''), + handleSendEmail + )} +
+ ) : null; +}); + +UserList.displayName = 'UserList'; diff --git a/x-pack/plugins/siem/public/pages/case/components/user_list/translations.ts b/x-pack/plugins/siem/public/cases/components/user_list/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/case/components/user_list/translations.ts rename to x-pack/plugins/siem/public/cases/components/user_list/translations.ts diff --git a/x-pack/plugins/siem/public/pages/case/components/wrappers/index.tsx b/x-pack/plugins/siem/public/cases/components/wrappers/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/case/components/wrappers/index.tsx rename to x-pack/plugins/siem/public/cases/components/wrappers/index.tsx diff --git a/x-pack/plugins/siem/public/containers/case/__mocks__/api.ts b/x-pack/plugins/siem/public/cases/containers/__mocks__/api.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/case/__mocks__/api.ts rename to x-pack/plugins/siem/public/cases/containers/__mocks__/api.ts diff --git a/x-pack/plugins/siem/public/containers/case/api.test.tsx b/x-pack/plugins/siem/public/cases/containers/api.test.tsx similarity index 99% rename from x-pack/plugins/siem/public/containers/case/api.test.tsx rename to x-pack/plugins/siem/public/cases/containers/api.test.tsx index 174738098fa107..b4f0c2198b4588 100644 --- a/x-pack/plugins/siem/public/containers/case/api.test.tsx +++ b/x-pack/plugins/siem/public/cases/containers/api.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { KibanaServices } from '../../lib/kibana'; +import { KibanaServices } from '../../common/lib/kibana'; import { CASES_URL } from '../../../../case/common/constants'; @@ -54,7 +54,7 @@ import * as i18n from './translations'; const abortCtrl = new AbortController(); const mockKibanaServices = KibanaServices.get as jest.Mock; -jest.mock('../../lib/kibana'); +jest.mock('../../common/lib/kibana'); const fetchMock = jest.fn(); mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } }); diff --git a/x-pack/plugins/siem/public/cases/containers/api.ts b/x-pack/plugins/siem/public/cases/containers/api.ts new file mode 100644 index 00000000000000..678286c0634d4c --- /dev/null +++ b/x-pack/plugins/siem/public/cases/containers/api.ts @@ -0,0 +1,268 @@ +/* + * 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 { + CaseResponse, + CasesResponse, + CasesFindResponse, + CasePatchRequest, + CasePostRequest, + CasesStatusResponse, + CommentRequest, + User, + CaseUserActionsResponse, + CaseExternalServiceRequest, + ServiceConnectorCaseParams, + ServiceConnectorCaseResponse, + ActionTypeExecutorResult, +} from '../../../../case/common/api'; + +import { + CASE_STATUS_URL, + CASES_URL, + CASE_TAGS_URL, + CASE_REPORTERS_URL, + ACTION_TYPES_URL, + ACTION_URL, +} from '../../../../case/common/constants'; + +import { + getCaseDetailsUrl, + getCaseUserActionUrl, + getCaseCommentsUrl, +} from '../../../../case/common/api/helpers'; + +import { KibanaServices } from '../../common/lib/kibana'; + +import { + ActionLicense, + AllCases, + BulkUpdateStatus, + Case, + CasesStatus, + FetchCasesProps, + SortFieldCase, + CaseUserActions, +} from './types'; + +import { + convertToCamelCase, + convertAllCasesToCamel, + convertArrayToCamelCase, + decodeCaseResponse, + decodeCasesResponse, + decodeCasesFindResponse, + decodeCasesStatusResponse, + decodeCaseUserActionsResponse, + decodeServiceConnectorCaseResponse, +} from './utils'; + +import * as i18n from './translations'; + +export const getCase = async ( + caseId: string, + includeComments: boolean = true, + signal: AbortSignal +): Promise => { + const response = await KibanaServices.get().http.fetch(getCaseDetailsUrl(caseId), { + method: 'GET', + query: { + includeComments, + }, + signal, + }); + return convertToCamelCase(decodeCaseResponse(response)); +}; + +export const getCasesStatus = async (signal: AbortSignal): Promise => { + const response = await KibanaServices.get().http.fetch(CASE_STATUS_URL, { + method: 'GET', + signal, + }); + return convertToCamelCase(decodeCasesStatusResponse(response)); +}; + +export const getTags = async (signal: AbortSignal): Promise => { + const response = await KibanaServices.get().http.fetch(CASE_TAGS_URL, { + method: 'GET', + signal, + }); + return response ?? []; +}; + +export const getReporters = async (signal: AbortSignal): Promise => { + const response = await KibanaServices.get().http.fetch(CASE_REPORTERS_URL, { + method: 'GET', + signal, + }); + return response ?? []; +}; + +export const getCaseUserActions = async ( + caseId: string, + signal: AbortSignal +): Promise => { + const response = await KibanaServices.get().http.fetch( + getCaseUserActionUrl(caseId), + { + method: 'GET', + signal, + } + ); + return convertArrayToCamelCase(decodeCaseUserActionsResponse(response)) as CaseUserActions[]; +}; + +export const getCases = async ({ + filterOptions = { + search: '', + reporters: [], + status: 'open', + tags: [], + }, + queryParams = { + page: 1, + perPage: 20, + sortField: SortFieldCase.createdAt, + sortOrder: 'desc', + }, + signal, +}: FetchCasesProps): Promise => { + const query = { + reporters: filterOptions.reporters.map(r => r.username ?? '').filter(r => r !== ''), + tags: filterOptions.tags, + ...(filterOptions.status !== '' ? { status: filterOptions.status } : {}), + ...(filterOptions.search.length > 0 ? { search: filterOptions.search } : {}), + ...queryParams, + }; + const response = await KibanaServices.get().http.fetch(`${CASES_URL}/_find`, { + method: 'GET', + query, + signal, + }); + return convertAllCasesToCamel(decodeCasesFindResponse(response)); +}; + +export const postCase = async (newCase: CasePostRequest, signal: AbortSignal): Promise => { + const response = await KibanaServices.get().http.fetch(CASES_URL, { + method: 'POST', + body: JSON.stringify(newCase), + signal, + }); + return convertToCamelCase(decodeCaseResponse(response)); +}; + +export const patchCase = async ( + caseId: string, + updatedCase: Pick, + version: string, + signal: AbortSignal +): Promise => { + const response = await KibanaServices.get().http.fetch(CASES_URL, { + method: 'PATCH', + body: JSON.stringify({ cases: [{ ...updatedCase, id: caseId, version }] }), + signal, + }); + return convertToCamelCase(decodeCasesResponse(response)); +}; + +export const patchCasesStatus = async ( + cases: BulkUpdateStatus[], + signal: AbortSignal +): Promise => { + const response = await KibanaServices.get().http.fetch(CASES_URL, { + method: 'PATCH', + body: JSON.stringify({ cases }), + signal, + }); + return convertToCamelCase(decodeCasesResponse(response)); +}; + +export const postComment = async ( + newComment: CommentRequest, + caseId: string, + signal: AbortSignal +): Promise => { + const response = await KibanaServices.get().http.fetch( + `${CASES_URL}/${caseId}/comments`, + { + method: 'POST', + body: JSON.stringify(newComment), + signal, + } + ); + return convertToCamelCase(decodeCaseResponse(response)); +}; + +export const patchComment = async ( + caseId: string, + commentId: string, + commentUpdate: string, + version: string, + signal: AbortSignal +): Promise => { + const response = await KibanaServices.get().http.fetch(getCaseCommentsUrl(caseId), { + method: 'PATCH', + body: JSON.stringify({ comment: commentUpdate, id: commentId, version }), + signal, + }); + return convertToCamelCase(decodeCaseResponse(response)); +}; + +export const deleteCases = async (caseIds: string[], signal: AbortSignal): Promise => { + const response = await KibanaServices.get().http.fetch(CASES_URL, { + method: 'DELETE', + query: { ids: JSON.stringify(caseIds) }, + signal, + }); + return response; +}; + +export const pushCase = async ( + caseId: string, + push: CaseExternalServiceRequest, + signal: AbortSignal +): Promise => { + const response = await KibanaServices.get().http.fetch( + `${getCaseDetailsUrl(caseId)}/_push`, + { + method: 'POST', + body: JSON.stringify(push), + signal, + } + ); + return convertToCamelCase(decodeCaseResponse(response)); +}; + +export const pushToService = async ( + connectorId: string, + casePushParams: ServiceConnectorCaseParams, + signal: AbortSignal +): Promise => { + const response = await KibanaServices.get().http.fetch( + `${ACTION_URL}/${connectorId}/_execute`, + { + method: 'POST', + body: JSON.stringify({ + params: { subAction: 'pushToService', subActionParams: casePushParams }, + }), + signal, + } + ); + + if (response.status === 'error') { + throw new Error(response.serviceMessage ?? response.message ?? i18n.ERROR_PUSH_TO_SERVICE); + } + + return decodeServiceConnectorCaseResponse(response.data); +}; + +export const getActionLicense = async (signal: AbortSignal): Promise => { + const response = await KibanaServices.get().http.fetch(ACTION_TYPES_URL, { + method: 'GET', + signal, + }); + return response; +}; diff --git a/x-pack/plugins/siem/public/containers/case/configure/__mocks__/api.ts b/x-pack/plugins/siem/public/cases/containers/configure/__mocks__/api.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/case/configure/__mocks__/api.ts rename to x-pack/plugins/siem/public/cases/containers/configure/__mocks__/api.ts diff --git a/x-pack/plugins/siem/public/cases/containers/configure/api.test.ts b/x-pack/plugins/siem/public/cases/containers/configure/api.test.ts new file mode 100644 index 00000000000000..11a293ef437fae --- /dev/null +++ b/x-pack/plugins/siem/public/cases/containers/configure/api.test.ts @@ -0,0 +1,115 @@ +/* + * 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 { KibanaServices } from '../../../common/lib/kibana'; +import { fetchConnectors, getCaseConfigure, postCaseConfigure, patchCaseConfigure } from './api'; +import { + connectorsMock, + caseConfigurationMock, + caseConfigurationResposeMock, + caseConfigurationCamelCaseResponseMock, +} from './mock'; + +const abortCtrl = new AbortController(); +const mockKibanaServices = KibanaServices.get as jest.Mock; +jest.mock('../../../common/lib/kibana'); + +const fetchMock = jest.fn(); +mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } }); + +describe('Case Configuration API', () => { + describe('fetch connectors', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(connectorsMock); + }); + + test('check url, method, signal', async () => { + await fetchConnectors({ signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/cases/configure/connectors/_find', { + method: 'GET', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await fetchConnectors({ signal: abortCtrl.signal }); + expect(resp).toEqual(connectorsMock); + }); + }); + + describe('fetch configuration', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(caseConfigurationResposeMock); + }); + + test('check url, method, signal', async () => { + await getCaseConfigure({ signal: abortCtrl.signal }); + expect(fetchMock).toHaveBeenCalledWith('/api/cases/configure', { + method: 'GET', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await getCaseConfigure({ signal: abortCtrl.signal }); + expect(resp).toEqual(caseConfigurationCamelCaseResponseMock); + }); + + test('return null on empty response', async () => { + fetchMock.mockResolvedValue({}); + const resp = await getCaseConfigure({ signal: abortCtrl.signal }); + expect(resp).toBe(null); + }); + }); + + describe('create configuration', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(caseConfigurationResposeMock); + }); + + test('check url, body, method, signal', async () => { + await postCaseConfigure(caseConfigurationMock, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith('/api/cases/configure', { + body: + '{"connector_id":"123","connector_name":"My Connector","closure_type":"close-by-user"}', + method: 'POST', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await postCaseConfigure(caseConfigurationMock, abortCtrl.signal); + expect(resp).toEqual(caseConfigurationCamelCaseResponseMock); + }); + }); + + describe('update configuration', () => { + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(caseConfigurationResposeMock); + }); + + test('check url, body, method, signal', async () => { + await patchCaseConfigure({ connector_id: '456', version: 'WzHJ12' }, abortCtrl.signal); + expect(fetchMock).toHaveBeenCalledWith('/api/cases/configure', { + body: '{"connector_id":"456","version":"WzHJ12"}', + method: 'PATCH', + signal: abortCtrl.signal, + }); + }); + + test('happy path', async () => { + const resp = await patchCaseConfigure( + { connector_id: '456', version: 'WzHJ12' }, + abortCtrl.signal + ); + expect(resp).toEqual(caseConfigurationCamelCaseResponseMock); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/cases/containers/configure/api.ts b/x-pack/plugins/siem/public/cases/containers/configure/api.ts new file mode 100644 index 00000000000000..4b4b81460ebc2a --- /dev/null +++ b/x-pack/plugins/siem/public/cases/containers/configure/api.ts @@ -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 { isEmpty } from 'lodash/fp'; +import { + Connector, + CasesConfigurePatch, + CasesConfigureResponse, + CasesConfigureRequest, +} from '../../../../../case/common/api'; +import { KibanaServices } from '../../../common/lib/kibana'; + +import { + CASE_CONFIGURE_CONNECTORS_URL, + CASE_CONFIGURE_URL, +} from '../../../../../case/common/constants'; + +import { ApiProps } from '../types'; +import { convertToCamelCase, decodeCaseConfigureResponse } from '../utils'; +import { CaseConfigure } from './types'; + +export const fetchConnectors = async ({ signal }: ApiProps): Promise => { + const response = await KibanaServices.get().http.fetch(`${CASE_CONFIGURE_CONNECTORS_URL}/_find`, { + method: 'GET', + signal, + }); + + return response; +}; + +export const getCaseConfigure = async ({ signal }: ApiProps): Promise => { + const response = await KibanaServices.get().http.fetch( + CASE_CONFIGURE_URL, + { + method: 'GET', + signal, + } + ); + + return !isEmpty(response) + ? convertToCamelCase( + decodeCaseConfigureResponse(response) + ) + : null; +}; + +export const postCaseConfigure = async ( + caseConfiguration: CasesConfigureRequest, + signal: AbortSignal +): Promise => { + const response = await KibanaServices.get().http.fetch( + CASE_CONFIGURE_URL, + { + method: 'POST', + body: JSON.stringify(caseConfiguration), + signal, + } + ); + return convertToCamelCase( + decodeCaseConfigureResponse(response) + ); +}; + +export const patchCaseConfigure = async ( + caseConfiguration: CasesConfigurePatch, + signal: AbortSignal +): Promise => { + const response = await KibanaServices.get().http.fetch( + CASE_CONFIGURE_URL, + { + method: 'PATCH', + body: JSON.stringify(caseConfiguration), + signal, + } + ); + return convertToCamelCase( + decodeCaseConfigureResponse(response) + ); +}; diff --git a/x-pack/plugins/siem/public/containers/case/configure/mock.ts b/x-pack/plugins/siem/public/cases/containers/configure/mock.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/case/configure/mock.ts rename to x-pack/plugins/siem/public/cases/containers/configure/mock.ts diff --git a/x-pack/plugins/siem/public/containers/case/configure/translations.ts b/x-pack/plugins/siem/public/cases/containers/configure/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/case/configure/translations.ts rename to x-pack/plugins/siem/public/cases/containers/configure/translations.ts diff --git a/x-pack/plugins/siem/public/containers/case/configure/types.ts b/x-pack/plugins/siem/public/cases/containers/configure/types.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/case/configure/types.ts rename to x-pack/plugins/siem/public/cases/containers/configure/types.ts diff --git a/x-pack/plugins/siem/public/containers/case/configure/use_configure.test.tsx b/x-pack/plugins/siem/public/cases/containers/configure/use_configure.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/containers/case/configure/use_configure.test.tsx rename to x-pack/plugins/siem/public/cases/containers/configure/use_configure.test.tsx diff --git a/x-pack/plugins/siem/public/containers/case/configure/use_configure.tsx b/x-pack/plugins/siem/public/cases/containers/configure/use_configure.tsx similarity index 98% rename from x-pack/plugins/siem/public/containers/case/configure/use_configure.tsx rename to x-pack/plugins/siem/public/cases/containers/configure/use_configure.tsx index a185d435f71651..5a85a3a0633bc8 100644 --- a/x-pack/plugins/siem/public/containers/case/configure/use_configure.tsx +++ b/x-pack/plugins/siem/public/cases/containers/configure/use_configure.tsx @@ -7,7 +7,11 @@ import { useEffect, useCallback, useReducer } from 'react'; import { getCaseConfigure, patchCaseConfigure, postCaseConfigure } from './api'; -import { useStateToaster, errorToToaster, displaySuccessToast } from '../../../components/toasters'; +import { + useStateToaster, + errorToToaster, + displaySuccessToast, +} from '../../../common/components/toasters'; import * as i18n from './translations'; import { CasesConfigurationMapping, ClosureType } from './types'; diff --git a/x-pack/plugins/siem/public/containers/case/configure/use_connectors.test.tsx b/x-pack/plugins/siem/public/cases/containers/configure/use_connectors.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/containers/case/configure/use_connectors.test.tsx rename to x-pack/plugins/siem/public/cases/containers/configure/use_connectors.test.tsx diff --git a/x-pack/plugins/siem/public/containers/case/configure/use_connectors.tsx b/x-pack/plugins/siem/public/cases/containers/configure/use_connectors.tsx similarity index 95% rename from x-pack/plugins/siem/public/containers/case/configure/use_connectors.tsx rename to x-pack/plugins/siem/public/cases/containers/configure/use_connectors.tsx index 30108ecf33874a..9cd755864d37b2 100644 --- a/x-pack/plugins/siem/public/containers/case/configure/use_connectors.tsx +++ b/x-pack/plugins/siem/public/cases/containers/configure/use_connectors.tsx @@ -6,7 +6,7 @@ import { useState, useEffect, useCallback } from 'react'; -import { useStateToaster, errorToToaster } from '../../../components/toasters'; +import { useStateToaster, errorToToaster } from '../../../common/components/toasters'; import * as i18n from '../translations'; import { fetchConnectors } from './api'; import { Connector } from './types'; diff --git a/x-pack/plugins/siem/public/containers/case/constants.ts b/x-pack/plugins/siem/public/cases/containers/constants.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/case/constants.ts rename to x-pack/plugins/siem/public/cases/containers/constants.ts diff --git a/x-pack/plugins/siem/public/containers/case/mock.ts b/x-pack/plugins/siem/public/cases/containers/mock.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/case/mock.ts rename to x-pack/plugins/siem/public/cases/containers/mock.ts diff --git a/x-pack/plugins/siem/public/containers/case/translations.ts b/x-pack/plugins/siem/public/cases/containers/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/case/translations.ts rename to x-pack/plugins/siem/public/cases/containers/translations.ts diff --git a/x-pack/plugins/siem/public/containers/case/types.ts b/x-pack/plugins/siem/public/cases/containers/types.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/case/types.ts rename to x-pack/plugins/siem/public/cases/containers/types.ts diff --git a/x-pack/plugins/siem/public/containers/case/use_bulk_update_case.test.tsx b/x-pack/plugins/siem/public/cases/containers/use_bulk_update_case.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/containers/case/use_bulk_update_case.test.tsx rename to x-pack/plugins/siem/public/cases/containers/use_bulk_update_case.test.tsx diff --git a/x-pack/plugins/siem/public/containers/case/use_bulk_update_case.tsx b/x-pack/plugins/siem/public/cases/containers/use_bulk_update_case.tsx similarity index 96% rename from x-pack/plugins/siem/public/containers/case/use_bulk_update_case.tsx rename to x-pack/plugins/siem/public/cases/containers/use_bulk_update_case.tsx index d0cc4d99f8f9f5..b9b64aa77493af 100644 --- a/x-pack/plugins/siem/public/containers/case/use_bulk_update_case.tsx +++ b/x-pack/plugins/siem/public/cases/containers/use_bulk_update_case.tsx @@ -5,7 +5,11 @@ */ import { useCallback, useReducer } from 'react'; -import { displaySuccessToast, errorToToaster, useStateToaster } from '../../components/toasters'; +import { + displaySuccessToast, + errorToToaster, + useStateToaster, +} from '../../common/components/toasters'; import * as i18n from './translations'; import { patchCasesStatus } from './api'; import { BulkUpdateStatus, Case } from './types'; diff --git a/x-pack/plugins/siem/public/containers/case/use_delete_cases.test.tsx b/x-pack/plugins/siem/public/cases/containers/use_delete_cases.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/containers/case/use_delete_cases.test.tsx rename to x-pack/plugins/siem/public/cases/containers/use_delete_cases.test.tsx diff --git a/x-pack/plugins/siem/public/containers/case/use_delete_cases.tsx b/x-pack/plugins/siem/public/cases/containers/use_delete_cases.tsx similarity index 97% rename from x-pack/plugins/siem/public/containers/case/use_delete_cases.tsx rename to x-pack/plugins/siem/public/cases/containers/use_delete_cases.tsx index 3c49be551c0640..31a73351de8f55 100644 --- a/x-pack/plugins/siem/public/containers/case/use_delete_cases.tsx +++ b/x-pack/plugins/siem/public/cases/containers/use_delete_cases.tsx @@ -5,7 +5,11 @@ */ import { useCallback, useReducer } from 'react'; -import { displaySuccessToast, errorToToaster, useStateToaster } from '../../components/toasters'; +import { + displaySuccessToast, + errorToToaster, + useStateToaster, +} from '../../common/components/toasters'; import * as i18n from './translations'; import { deleteCases } from './api'; import { DeleteCase } from './types'; diff --git a/x-pack/plugins/siem/public/containers/case/use_get_action_license.test.tsx b/x-pack/plugins/siem/public/cases/containers/use_get_action_license.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/containers/case/use_get_action_license.test.tsx rename to x-pack/plugins/siem/public/cases/containers/use_get_action_license.test.tsx diff --git a/x-pack/plugins/siem/public/containers/case/use_get_action_license.tsx b/x-pack/plugins/siem/public/cases/containers/use_get_action_license.tsx similarity index 96% rename from x-pack/plugins/siem/public/containers/case/use_get_action_license.tsx rename to x-pack/plugins/siem/public/cases/containers/use_get_action_license.tsx index 0d28a1b20c61f0..c09cc8dedd3799 100644 --- a/x-pack/plugins/siem/public/containers/case/use_get_action_license.tsx +++ b/x-pack/plugins/siem/public/cases/containers/use_get_action_license.tsx @@ -6,7 +6,7 @@ import { useCallback, useEffect, useState } from 'react'; -import { errorToToaster, useStateToaster } from '../../components/toasters'; +import { errorToToaster, useStateToaster } from '../../common/components/toasters'; import { getActionLicense } from './api'; import * as i18n from './translations'; import { ActionLicense } from './types'; diff --git a/x-pack/plugins/siem/public/containers/case/use_get_case.test.tsx b/x-pack/plugins/siem/public/cases/containers/use_get_case.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/containers/case/use_get_case.test.tsx rename to x-pack/plugins/siem/public/cases/containers/use_get_case.test.tsx diff --git a/x-pack/plugins/siem/public/containers/case/use_get_case.tsx b/x-pack/plugins/siem/public/cases/containers/use_get_case.tsx similarity index 97% rename from x-pack/plugins/siem/public/containers/case/use_get_case.tsx rename to x-pack/plugins/siem/public/cases/containers/use_get_case.tsx index 06d4c38ddda49a..01ada00ba9b72d 100644 --- a/x-pack/plugins/siem/public/containers/case/use_get_case.tsx +++ b/x-pack/plugins/siem/public/cases/containers/use_get_case.tsx @@ -8,7 +8,7 @@ import { useEffect, useReducer, useCallback } from 'react'; import { Case } from './types'; import * as i18n from './translations'; -import { errorToToaster, useStateToaster } from '../../components/toasters'; +import { errorToToaster, useStateToaster } from '../../common/components/toasters'; import { getCase } from './api'; interface CaseState { diff --git a/x-pack/plugins/siem/public/containers/case/use_get_case_user_actions.test.tsx b/x-pack/plugins/siem/public/cases/containers/use_get_case_user_actions.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/containers/case/use_get_case_user_actions.test.tsx rename to x-pack/plugins/siem/public/cases/containers/use_get_case_user_actions.test.tsx diff --git a/x-pack/plugins/siem/public/containers/case/use_get_case_user_actions.tsx b/x-pack/plugins/siem/public/cases/containers/use_get_case_user_actions.tsx similarity index 98% rename from x-pack/plugins/siem/public/containers/case/use_get_case_user_actions.tsx rename to x-pack/plugins/siem/public/cases/containers/use_get_case_user_actions.tsx index 5afe06a9828e5f..2848d56378cd23 100644 --- a/x-pack/plugins/siem/public/containers/case/use_get_case_user_actions.tsx +++ b/x-pack/plugins/siem/public/cases/containers/use_get_case_user_actions.tsx @@ -7,7 +7,7 @@ import { isEmpty, uniqBy } from 'lodash/fp'; import { useCallback, useEffect, useState } from 'react'; -import { errorToToaster, useStateToaster } from '../../components/toasters'; +import { errorToToaster, useStateToaster } from '../../common/components/toasters'; import { getCaseUserActions } from './api'; import * as i18n from './translations'; import { CaseExternalService, CaseUserActions, ElasticUser } from './types'; diff --git a/x-pack/plugins/siem/public/containers/case/use_get_cases.test.tsx b/x-pack/plugins/siem/public/cases/containers/use_get_cases.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/containers/case/use_get_cases.test.tsx rename to x-pack/plugins/siem/public/cases/containers/use_get_cases.test.tsx diff --git a/x-pack/plugins/siem/public/containers/case/use_get_cases.tsx b/x-pack/plugins/siem/public/cases/containers/use_get_cases.tsx similarity index 98% rename from x-pack/plugins/siem/public/containers/case/use_get_cases.tsx rename to x-pack/plugins/siem/public/cases/containers/use_get_cases.tsx index 465b50dbdc1bc6..b0701c71b857e2 100644 --- a/x-pack/plugins/siem/public/containers/case/use_get_cases.tsx +++ b/x-pack/plugins/siem/public/cases/containers/use_get_cases.tsx @@ -7,7 +7,7 @@ import { useCallback, useEffect, useReducer } from 'react'; import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from './constants'; import { AllCases, SortFieldCase, FilterOptions, QueryParams, Case } from './types'; -import { errorToToaster, useStateToaster } from '../../components/toasters'; +import { errorToToaster, useStateToaster } from '../../common/components/toasters'; import * as i18n from './translations'; import { UpdateByKey } from './use_update_case'; import { getCases, patchCase } from './api'; diff --git a/x-pack/plugins/siem/public/containers/case/use_get_cases_status.test.tsx b/x-pack/plugins/siem/public/cases/containers/use_get_cases_status.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/containers/case/use_get_cases_status.test.tsx rename to x-pack/plugins/siem/public/cases/containers/use_get_cases_status.test.tsx diff --git a/x-pack/plugins/siem/public/containers/case/use_get_cases_status.tsx b/x-pack/plugins/siem/public/cases/containers/use_get_cases_status.tsx similarity index 96% rename from x-pack/plugins/siem/public/containers/case/use_get_cases_status.tsx rename to x-pack/plugins/siem/public/cases/containers/use_get_cases_status.tsx index 07884646023570..476462b7e4c28f 100644 --- a/x-pack/plugins/siem/public/containers/case/use_get_cases_status.tsx +++ b/x-pack/plugins/siem/public/cases/containers/use_get_cases_status.tsx @@ -6,7 +6,7 @@ import { useCallback, useEffect, useState } from 'react'; -import { errorToToaster, useStateToaster } from '../../components/toasters'; +import { errorToToaster, useStateToaster } from '../../common/components/toasters'; import { getCasesStatus } from './api'; import * as i18n from './translations'; import { CasesStatus } from './types'; diff --git a/x-pack/plugins/siem/public/containers/case/use_get_reporters.test.tsx b/x-pack/plugins/siem/public/cases/containers/use_get_reporters.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/containers/case/use_get_reporters.test.tsx rename to x-pack/plugins/siem/public/cases/containers/use_get_reporters.test.tsx diff --git a/x-pack/plugins/siem/public/containers/case/use_get_reporters.tsx b/x-pack/plugins/siem/public/cases/containers/use_get_reporters.tsx similarity index 96% rename from x-pack/plugins/siem/public/containers/case/use_get_reporters.tsx rename to x-pack/plugins/siem/public/cases/containers/use_get_reporters.tsx index 01679ae4ccd82d..5bfc8c84d1ecc3 100644 --- a/x-pack/plugins/siem/public/containers/case/use_get_reporters.tsx +++ b/x-pack/plugins/siem/public/cases/containers/use_get_reporters.tsx @@ -8,7 +8,7 @@ import { useCallback, useEffect, useState } from 'react'; import { isEmpty } from 'lodash/fp'; import { User } from '../../../../case/common/api'; -import { errorToToaster, useStateToaster } from '../../components/toasters'; +import { errorToToaster, useStateToaster } from '../../common/components/toasters'; import { getReporters } from './api'; import * as i18n from './translations'; diff --git a/x-pack/plugins/siem/public/containers/case/use_get_tags.test.tsx b/x-pack/plugins/siem/public/cases/containers/use_get_tags.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/containers/case/use_get_tags.test.tsx rename to x-pack/plugins/siem/public/cases/containers/use_get_tags.test.tsx diff --git a/x-pack/plugins/siem/public/containers/case/use_get_tags.tsx b/x-pack/plugins/siem/public/cases/containers/use_get_tags.tsx similarity index 96% rename from x-pack/plugins/siem/public/containers/case/use_get_tags.tsx rename to x-pack/plugins/siem/public/cases/containers/use_get_tags.tsx index 99bb65fa160f7c..14f5e35bc4976e 100644 --- a/x-pack/plugins/siem/public/containers/case/use_get_tags.tsx +++ b/x-pack/plugins/siem/public/cases/containers/use_get_tags.tsx @@ -6,7 +6,7 @@ import { useEffect, useReducer } from 'react'; -import { errorToToaster, useStateToaster } from '../../components/toasters'; +import { errorToToaster, useStateToaster } from '../../common/components/toasters'; import { getTags } from './api'; import * as i18n from './translations'; diff --git a/x-pack/plugins/siem/public/containers/case/use_post_case.test.tsx b/x-pack/plugins/siem/public/cases/containers/use_post_case.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/containers/case/use_post_case.test.tsx rename to x-pack/plugins/siem/public/cases/containers/use_post_case.test.tsx diff --git a/x-pack/plugins/siem/public/containers/case/use_post_case.tsx b/x-pack/plugins/siem/public/cases/containers/use_post_case.tsx similarity index 96% rename from x-pack/plugins/siem/public/containers/case/use_post_case.tsx rename to x-pack/plugins/siem/public/cases/containers/use_post_case.tsx index b33269f26e97dd..13cfc2738620fc 100644 --- a/x-pack/plugins/siem/public/containers/case/use_post_case.tsx +++ b/x-pack/plugins/siem/public/cases/containers/use_post_case.tsx @@ -7,7 +7,7 @@ import { useReducer, useCallback } from 'react'; import { CasePostRequest } from '../../../../case/common/api'; -import { errorToToaster, useStateToaster } from '../../components/toasters'; +import { errorToToaster, useStateToaster } from '../../common/components/toasters'; import { postCase } from './api'; import * as i18n from './translations'; import { Case } from './types'; diff --git a/x-pack/plugins/siem/public/containers/case/use_post_comment.test.tsx b/x-pack/plugins/siem/public/cases/containers/use_post_comment.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/containers/case/use_post_comment.test.tsx rename to x-pack/plugins/siem/public/cases/containers/use_post_comment.test.tsx diff --git a/x-pack/plugins/siem/public/containers/case/use_post_comment.tsx b/x-pack/plugins/siem/public/cases/containers/use_post_comment.tsx similarity index 96% rename from x-pack/plugins/siem/public/containers/case/use_post_comment.tsx rename to x-pack/plugins/siem/public/cases/containers/use_post_comment.tsx index c7d3b4125aada8..9a52eaaf0db6b3 100644 --- a/x-pack/plugins/siem/public/containers/case/use_post_comment.tsx +++ b/x-pack/plugins/siem/public/cases/containers/use_post_comment.tsx @@ -7,7 +7,7 @@ import { useReducer, useCallback } from 'react'; import { CommentRequest } from '../../../../case/common/api'; -import { errorToToaster, useStateToaster } from '../../components/toasters'; +import { errorToToaster, useStateToaster } from '../../common/components/toasters'; import { postComment } from './api'; import * as i18n from './translations'; diff --git a/x-pack/plugins/siem/public/containers/case/use_post_push_to_service.test.tsx b/x-pack/plugins/siem/public/cases/containers/use_post_push_to_service.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/containers/case/use_post_push_to_service.test.tsx rename to x-pack/plugins/siem/public/cases/containers/use_post_push_to_service.test.tsx diff --git a/x-pack/plugins/siem/public/containers/case/use_post_push_to_service.tsx b/x-pack/plugins/siem/public/cases/containers/use_post_push_to_service.tsx similarity index 98% rename from x-pack/plugins/siem/public/containers/case/use_post_push_to_service.tsx rename to x-pack/plugins/siem/public/cases/containers/use_post_push_to_service.tsx index 7f4c4a42761721..def324dcf442e3 100644 --- a/x-pack/plugins/siem/public/containers/case/use_post_push_to_service.tsx +++ b/x-pack/plugins/siem/public/cases/containers/use_post_push_to_service.tsx @@ -10,7 +10,11 @@ import { ServiceConnectorCaseResponse, ServiceConnectorCaseParams, } from '../../../../case/common/api'; -import { errorToToaster, useStateToaster, displaySuccessToast } from '../../components/toasters'; +import { + errorToToaster, + useStateToaster, + displaySuccessToast, +} from '../../common/components/toasters'; import { getCase, pushToService, pushCase } from './api'; import * as i18n from './translations'; diff --git a/x-pack/plugins/siem/public/containers/case/use_update_case.test.tsx b/x-pack/plugins/siem/public/cases/containers/use_update_case.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/containers/case/use_update_case.test.tsx rename to x-pack/plugins/siem/public/cases/containers/use_update_case.test.tsx diff --git a/x-pack/plugins/siem/public/containers/case/use_update_case.tsx b/x-pack/plugins/siem/public/cases/containers/use_update_case.tsx similarity index 96% rename from x-pack/plugins/siem/public/containers/case/use_update_case.tsx rename to x-pack/plugins/siem/public/cases/containers/use_update_case.tsx index af824674999b9a..77cf53165d9147 100644 --- a/x-pack/plugins/siem/public/containers/case/use_update_case.tsx +++ b/x-pack/plugins/siem/public/cases/containers/use_update_case.tsx @@ -5,7 +5,11 @@ */ import { useReducer, useCallback } from 'react'; -import { displaySuccessToast, errorToToaster, useStateToaster } from '../../components/toasters'; +import { + displaySuccessToast, + errorToToaster, + useStateToaster, +} from '../../common/components/toasters'; import { CasePatchRequest } from '../../../../case/common/api'; import { patchCase } from './api'; diff --git a/x-pack/plugins/siem/public/containers/case/use_update_comment.test.tsx b/x-pack/plugins/siem/public/cases/containers/use_update_comment.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/containers/case/use_update_comment.test.tsx rename to x-pack/plugins/siem/public/cases/containers/use_update_comment.test.tsx diff --git a/x-pack/plugins/siem/public/containers/case/use_update_comment.tsx b/x-pack/plugins/siem/public/cases/containers/use_update_comment.tsx similarity index 97% rename from x-pack/plugins/siem/public/containers/case/use_update_comment.tsx rename to x-pack/plugins/siem/public/cases/containers/use_update_comment.tsx index ffc5cffee7a554..66064faea27d71 100644 --- a/x-pack/plugins/siem/public/containers/case/use_update_comment.tsx +++ b/x-pack/plugins/siem/public/cases/containers/use_update_comment.tsx @@ -6,7 +6,7 @@ import { useReducer, useCallback } from 'react'; -import { errorToToaster, useStateToaster } from '../../components/toasters'; +import { errorToToaster, useStateToaster } from '../../common/components/toasters'; import { patchComment } from './api'; import * as i18n from './translations'; diff --git a/x-pack/plugins/siem/public/cases/containers/utils.ts b/x-pack/plugins/siem/public/cases/containers/utils.ts new file mode 100644 index 00000000000000..ebaba0fe42f788 --- /dev/null +++ b/x-pack/plugins/siem/public/cases/containers/utils.ts @@ -0,0 +1,107 @@ +/* + * 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 { camelCase, isArray, isObject, set } from 'lodash'; +import { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { + CasesFindResponse, + CasesFindResponseRt, + CaseResponse, + CaseResponseRt, + CasesResponse, + CasesResponseRt, + CasesStatusResponseRt, + CasesStatusResponse, + throwErrors, + CasesConfigureResponse, + CaseConfigureResponseRt, + CaseUserActionsResponse, + CaseUserActionsResponseRt, + ServiceConnectorCaseResponseRt, + ServiceConnectorCaseResponse, +} from '../../../../case/common/api'; +import { ToasterError } from '../../common/components/toasters'; +import { AllCases, Case } from './types'; + +export const getTypedPayload = (a: unknown): T => a as T; + +export const parseString = (params: string) => { + try { + return JSON.parse(params); + } catch { + return null; + } +}; + +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: CasesFindResponse): AllCases => ({ + cases: snakeCases.cases.map(snakeCase => convertToCamelCase(snakeCase)), + countClosedCases: snakeCases.count_closed_cases, + countOpenCases: snakeCases.count_open_cases, + page: snakeCases.page, + perPage: snakeCases.per_page, + total: snakeCases.total, +}); + +export const decodeCasesStatusResponse = (respCase?: CasesStatusResponse) => + pipe( + CasesStatusResponseRt.decode(respCase), + fold(throwErrors(createToasterPlainError), identity) + ); + +export const createToasterPlainError = (message: string) => new ToasterError([message]); + +export const decodeCaseResponse = (respCase?: CaseResponse) => + pipe(CaseResponseRt.decode(respCase), fold(throwErrors(createToasterPlainError), identity)); + +export const decodeCasesResponse = (respCase?: CasesResponse) => + pipe(CasesResponseRt.decode(respCase), fold(throwErrors(createToasterPlainError), identity)); + +export const decodeCasesFindResponse = (respCases?: CasesFindResponse) => + pipe(CasesFindResponseRt.decode(respCases), fold(throwErrors(createToasterPlainError), identity)); + +export const decodeCaseConfigureResponse = (respCase?: CasesConfigureResponse) => + pipe( + CaseConfigureResponseRt.decode(respCase), + fold(throwErrors(createToasterPlainError), identity) + ); + +export const decodeCaseUserActionsResponse = (respUserActions?: CaseUserActionsResponse) => + pipe( + CaseUserActionsResponseRt.decode(respUserActions), + fold(throwErrors(createToasterPlainError), identity) + ); + +export const decodeServiceConnectorCaseResponse = (respPushCase?: ServiceConnectorCaseResponse) => + pipe( + ServiceConnectorCaseResponseRt.decode(respPushCase), + fold(throwErrors(createToasterPlainError), identity) + ); diff --git a/x-pack/plugins/siem/public/cases/index.ts b/x-pack/plugins/siem/public/cases/index.ts new file mode 100644 index 00000000000000..1eb8c82532e217 --- /dev/null +++ b/x-pack/plugins/siem/public/cases/index.ts @@ -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. + */ + +import { SecuritySubPlugin } from '../app/types'; +import { getCasesRoutes } from './routes'; + +export class Cases { + public setup() {} + + public start(): SecuritySubPlugin { + return { + routes: getCasesRoutes(), + }; + } +} diff --git a/x-pack/plugins/siem/public/cases/pages/case.tsx b/x-pack/plugins/siem/public/cases/pages/case.tsx new file mode 100644 index 00000000000000..03ebec34c2cdd6 --- /dev/null +++ b/x-pack/plugins/siem/public/cases/pages/case.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { WrapperPage } from '../../common/components/wrapper_page'; +import { useGetUserSavedObjectPermissions } from '../../common/lib/kibana'; +import { SpyRoute } from '../../common/utils/route/spy_routes'; +import { AllCases } from '../components/all_cases'; + +import { savedObjectReadOnly, CaseCallOut } from '../components/callout'; +import { CaseSavedObjectNoPermissions } from './saved_object_no_permissions'; + +export const CasesPage = React.memo(() => { + const userPermissions = useGetUserSavedObjectPermissions(); + + return userPermissions == null || userPermissions?.read ? ( + <> + + {userPermissions != null && !userPermissions?.crud && userPermissions?.read && ( + + )} + + + + + ) : ( + + ); +}); + +CasesPage.displayName = 'CasesPage'; diff --git a/x-pack/plugins/siem/public/cases/pages/case_details.tsx b/x-pack/plugins/siem/public/cases/pages/case_details.tsx new file mode 100644 index 00000000000000..5ea5e52951592a --- /dev/null +++ b/x-pack/plugins/siem/public/cases/pages/case_details.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { useParams, Redirect } from 'react-router-dom'; + +import { useGetUrlSearch } from '../../common/components/navigation/use_get_url_search'; +import { useGetUserSavedObjectPermissions } from '../../common/lib/kibana'; +import { SpyRoute } from '../../common/utils/route/spy_routes'; +import { getCaseUrl } from '../../common/components/link_to'; +import { navTabs } from '../../app/home/home_navigations'; +import { CaseView } from '../components/case_view'; +import { savedObjectReadOnly, CaseCallOut } from '../components/callout'; + +export const CaseDetailsPage = React.memo(() => { + const userPermissions = useGetUserSavedObjectPermissions(); + const { detailName: caseId } = useParams(); + const search = useGetUrlSearch(navTabs.case); + + if (userPermissions != null && !userPermissions.read) { + return ; + } + + return caseId != null ? ( + <> + {userPermissions != null && !userPermissions?.crud && userPermissions?.read && ( + + )} + + + + ) : null; +}); + +CaseDetailsPage.displayName = 'CaseDetailsPage'; diff --git a/x-pack/plugins/siem/public/cases/pages/configure_cases.tsx b/x-pack/plugins/siem/public/cases/pages/configure_cases.tsx new file mode 100644 index 00000000000000..bea3a9fb110ab0 --- /dev/null +++ b/x-pack/plugins/siem/public/cases/pages/configure_cases.tsx @@ -0,0 +1,58 @@ +/* + * 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, { useMemo } from 'react'; +import { Redirect } from 'react-router-dom'; + +import { getCaseUrl } from '../../common/components/link_to'; +import { useGetUrlSearch } from '../../common/components/navigation/use_get_url_search'; +import { WrapperPage } from '../../common/components/wrapper_page'; +import { useGetUserSavedObjectPermissions } from '../../common/lib/kibana'; +import { SpyRoute } from '../../common/utils/route/spy_routes'; +import { navTabs } from '../../app/home/home_navigations'; +import { CaseHeaderPage } from '../components/case_header_page'; +import { ConfigureCases } from '../components/configure_cases'; +import { WhitePageWrapper, SectionWrapper } from '../components/wrappers'; +import * as i18n from './translations'; + +const wrapperPageStyle: Record = { + paddingLeft: '0', + paddingRight: '0', + paddingBottom: '0', +}; + +const ConfigureCasesPageComponent: React.FC = () => { + const userPermissions = useGetUserSavedObjectPermissions(); + const search = useGetUrlSearch(navTabs.case); + + const backOptions = useMemo( + () => ({ + href: getCaseUrl(search), + text: i18n.BACK_TO_ALL, + }), + [search] + ); + + if (userPermissions != null && !userPermissions.read) { + return ; + } + + return ( + <> + + + + + + + + + + + ); +}; + +export const ConfigureCasesPage = React.memo(ConfigureCasesPageComponent); diff --git a/x-pack/plugins/siem/public/cases/pages/create_case.tsx b/x-pack/plugins/siem/public/cases/pages/create_case.tsx new file mode 100644 index 00000000000000..c586a90e5ef9c3 --- /dev/null +++ b/x-pack/plugins/siem/public/cases/pages/create_case.tsx @@ -0,0 +1,47 @@ +/* + * 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, { useMemo } from 'react'; +import { Redirect } from 'react-router-dom'; + +import { getCaseUrl } from '../../common/components/link_to'; +import { useGetUrlSearch } from '../../common/components/navigation/use_get_url_search'; +import { WrapperPage } from '../../common/components/wrapper_page'; +import { useGetUserSavedObjectPermissions } from '../../common/lib/kibana'; +import { SpyRoute } from '../../common/utils/route/spy_routes'; +import { navTabs } from '../../app/home/home_navigations'; +import { CaseHeaderPage } from '../components/case_header_page'; +import { Create } from '../components/create'; +import * as i18n from './translations'; + +export const CreateCasePage = React.memo(() => { + const userPermissions = useGetUserSavedObjectPermissions(); + const search = useGetUrlSearch(navTabs.case); + + const backOptions = useMemo( + () => ({ + href: getCaseUrl(search), + text: i18n.BACK_TO_ALL, + }), + [search] + ); + + if (userPermissions != null && !userPermissions.crud) { + return ; + } + + return ( + <> + + + + + + + ); +}); + +CreateCasePage.displayName = 'CreateCasePage'; diff --git a/x-pack/plugins/siem/public/cases/pages/index.tsx b/x-pack/plugins/siem/public/cases/pages/index.tsx new file mode 100644 index 00000000000000..32f64d2690cba0 --- /dev/null +++ b/x-pack/plugins/siem/public/cases/pages/index.tsx @@ -0,0 +1,42 @@ +/* + * 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 { Route, Switch } from 'react-router-dom'; +import { SiemPageName } from '../../app/types'; +import { CaseDetailsPage } from './case_details'; +import { CasesPage } from './case'; +import { CreateCasePage } from './create_case'; +import { ConfigureCasesPage } from './configure_cases'; + +const casesPagePath = `/:pageName(${SiemPageName.case})`; +const caseDetailsPagePath = `${casesPagePath}/:detailName`; +const caseDetailsPagePathWithCommentId = `${casesPagePath}/:detailName/:commentId`; +const createCasePagePath = `${casesPagePath}/create`; +const configureCasesPagePath = `${casesPagePath}/configure`; + +const CaseContainerComponent: React.FC = () => ( + + + + + + + + + + + + + + + + + +); + +export const Case = React.memo(CaseContainerComponent); diff --git a/x-pack/plugins/siem/public/pages/case/saved_object_no_permissions.tsx b/x-pack/plugins/siem/public/cases/pages/saved_object_no_permissions.tsx similarity index 88% rename from x-pack/plugins/siem/public/pages/case/saved_object_no_permissions.tsx rename to x-pack/plugins/siem/public/cases/pages/saved_object_no_permissions.tsx index 689c290c910191..a560f697de415c 100644 --- a/x-pack/plugins/siem/public/pages/case/saved_object_no_permissions.tsx +++ b/x-pack/plugins/siem/public/cases/pages/saved_object_no_permissions.tsx @@ -6,9 +6,9 @@ import React from 'react'; -import { EmptyPage } from '../../components/empty_page'; +import { EmptyPage } from '../../common/components/empty_page'; import * as i18n from './translations'; -import { useKibana } from '../../lib/kibana'; +import { useKibana } from '../../common/lib/kibana'; export const CaseSavedObjectNoPermissions = React.memo(() => { const docLinks = useKibana().services.docLinks; diff --git a/x-pack/plugins/siem/public/pages/case/translations.ts b/x-pack/plugins/siem/public/cases/pages/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/case/translations.ts rename to x-pack/plugins/siem/public/cases/pages/translations.ts diff --git a/x-pack/plugins/siem/public/cases/pages/utils.ts b/x-pack/plugins/siem/public/cases/pages/utils.ts new file mode 100644 index 00000000000000..0b60d66756d0c8 --- /dev/null +++ b/x-pack/plugins/siem/public/cases/pages/utils.ts @@ -0,0 +1,42 @@ +/* + * 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 { isEmpty } from 'lodash/fp'; + +import { ChromeBreadcrumb } from 'src/core/public'; + +import { getCaseDetailsUrl, getCaseUrl, getCreateCaseUrl } from '../../common/components/link_to'; +import { RouteSpyState } from '../../common/utils/route/types'; +import * as i18n from './translations'; + +export const getBreadcrumbs = (params: RouteSpyState, search: string[]): ChromeBreadcrumb[] => { + const queryParameters = !isEmpty(search[0]) ? search[0] : null; + + let breadcrumb = [ + { + text: i18n.PAGE_TITLE, + href: getCaseUrl(queryParameters), + }, + ]; + if (params.detailName === 'create') { + breadcrumb = [ + ...breadcrumb, + { + text: i18n.CREATE_BC_TITLE, + href: getCreateCaseUrl(queryParameters), + }, + ]; + } else if (params.detailName != null) { + breadcrumb = [ + ...breadcrumb, + { + text: params.state?.caseTitle ?? '', + href: getCaseDetailsUrl({ id: params.detailName, search: queryParameters }), + }, + ]; + } + return breadcrumb; +}; diff --git a/x-pack/plugins/siem/public/cases/routes.tsx b/x-pack/plugins/siem/public/cases/routes.tsx new file mode 100644 index 00000000000000..698350e49bc3eb --- /dev/null +++ b/x-pack/plugins/siem/public/cases/routes.tsx @@ -0,0 +1,17 @@ +/* + * 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 { Route } from 'react-router-dom'; + +import { Case } from './pages'; +import { SiemPageName } from '../app/types'; + +export const getCasesRoutes = () => [ + + + , +]; diff --git a/x-pack/plugins/siem/public/cases/translations.ts b/x-pack/plugins/siem/public/cases/translations.ts new file mode 100644 index 00000000000000..782ba9d9f32dbc --- /dev/null +++ b/x-pack/plugins/siem/public/cases/translations.ts @@ -0,0 +1,205 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const SAVED_OBJECT_NO_PERMISSIONS_TITLE = i18n.translate( + 'xpack.siem.case.caseSavedObjectNoPermissionsTitle', + { + defaultMessage: 'Kibana feature privileges required', + } +); + +export const SAVED_OBJECT_NO_PERMISSIONS_MSG = i18n.translate( + 'xpack.siem.case.caseSavedObjectNoPermissionsMessage', + { + defaultMessage: + 'To view cases, you must have privileges for the Saved Object Management feature in the Kibana space. For more information, contact your Kibana administrator.', + } +); + +export const BACK_TO_ALL = i18n.translate('xpack.siem.case.caseView.backLabel', { + defaultMessage: 'Back to cases', +}); + +export const CANCEL = i18n.translate('xpack.siem.case.caseView.cancel', { + defaultMessage: 'Cancel', +}); + +export const DELETE_CASE = i18n.translate('xpack.siem.case.confirmDeleteCase.deleteCase', { + defaultMessage: 'Delete case', +}); + +export const DELETE_CASES = i18n.translate('xpack.siem.case.confirmDeleteCase.deleteCases', { + defaultMessage: 'Delete cases', +}); + +export const NAME = i18n.translate('xpack.siem.case.caseView.name', { + defaultMessage: 'Name', +}); + +export const OPENED_ON = i18n.translate('xpack.siem.case.caseView.openedOn', { + defaultMessage: 'Opened on', +}); + +export const CLOSED_ON = i18n.translate('xpack.siem.case.caseView.closedOn', { + defaultMessage: 'Closed on', +}); + +export const REPORTER = i18n.translate('xpack.siem.case.caseView.reporterLabel', { + defaultMessage: 'Reporter', +}); + +export const PARTICIPANTS = i18n.translate('xpack.siem.case.caseView.particpantsLabel', { + defaultMessage: 'Participants', +}); + +export const CREATE_BC_TITLE = i18n.translate('xpack.siem.case.caseView.breadcrumb', { + defaultMessage: 'Create', +}); + +export const CREATE_TITLE = i18n.translate('xpack.siem.case.caseView.create', { + defaultMessage: 'Create new case', +}); + +export const DESCRIPTION = i18n.translate('xpack.siem.case.caseView.description', { + defaultMessage: 'Description', +}); + +export const DESCRIPTION_REQUIRED = i18n.translate( + 'xpack.siem.case.createCase.descriptionFieldRequiredError', + { + defaultMessage: 'A description is required.', + } +); + +export const COMMENT_REQUIRED = i18n.translate( + 'xpack.siem.case.caseView.commentFieldRequiredError', + { + defaultMessage: 'A comment is required.', + } +); + +export const REQUIRED_FIELD = i18n.translate('xpack.siem.case.caseView.fieldRequiredError', { + defaultMessage: 'Required field', +}); + +export const EDIT = i18n.translate('xpack.siem.case.caseView.edit', { + defaultMessage: 'Edit', +}); + +export const OPTIONAL = i18n.translate('xpack.siem.case.caseView.optional', { + defaultMessage: 'Optional', +}); + +export const PAGE_TITLE = i18n.translate('xpack.siem.case.pageTitle', { + defaultMessage: 'Cases', +}); + +export const CREATE_CASE = i18n.translate('xpack.siem.case.caseView.createCase', { + defaultMessage: 'Create case', +}); + +export const CLOSED_CASE = i18n.translate('xpack.siem.case.caseView.closedCase', { + defaultMessage: 'Closed case', +}); + +export const CLOSE_CASE = i18n.translate('xpack.siem.case.caseView.closeCase', { + defaultMessage: 'Close case', +}); + +export const REOPEN_CASE = i18n.translate('xpack.siem.case.caseView.reopenCase', { + defaultMessage: 'Reopen case', +}); + +export const REOPENED_CASE = i18n.translate('xpack.siem.case.caseView.reopenedCase', { + defaultMessage: 'Reopened case', +}); + +export const CASE_NAME = i18n.translate('xpack.siem.case.caseView.caseName', { + defaultMessage: 'Case name', +}); + +export const TO = i18n.translate('xpack.siem.case.caseView.to', { + defaultMessage: 'to', +}); + +export const TAGS = i18n.translate('xpack.siem.case.caseView.tags', { + defaultMessage: 'Tags', +}); + +export const ACTIONS = i18n.translate('xpack.siem.case.allCases.actions', { + defaultMessage: 'Actions', +}); + +export const NO_TAGS_AVAILABLE = i18n.translate('xpack.siem.case.allCases.noTagsAvailable', { + defaultMessage: 'No tags available', +}); + +export const NO_REPORTERS_AVAILABLE = i18n.translate( + 'xpack.siem.case.caseView.noReportersAvailable', + { + defaultMessage: 'No reporters available.', + } +); + +export const COMMENTS = i18n.translate('xpack.siem.case.allCases.comments', { + defaultMessage: 'Comments', +}); + +export const TAGS_HELP = i18n.translate('xpack.siem.case.createCase.fieldTagsHelpText', { + defaultMessage: + 'Type one or more custom identifying tags for this case. Press enter after each tag to begin a new one.', +}); + +export const NO_TAGS = i18n.translate('xpack.siem.case.caseView.noTags', { + defaultMessage: 'No tags are currently assigned to this case.', +}); + +export const TITLE_REQUIRED = i18n.translate('xpack.siem.case.createCase.titleFieldRequiredError', { + defaultMessage: 'A title is required.', +}); + +export const CONFIGURE_CASES_PAGE_TITLE = i18n.translate( + 'xpack.siem.case.configureCases.headerTitle', + { + defaultMessage: 'Configure cases', + } +); + +export const CONFIGURE_CASES_BUTTON = i18n.translate('xpack.siem.case.configureCasesButton', { + defaultMessage: 'Edit external connection', +}); + +export const ADD_COMMENT = i18n.translate('xpack.siem.case.caseView.comment.addComment', { + defaultMessage: 'Add comment', +}); + +export const ADD_COMMENT_HELP_TEXT = i18n.translate( + 'xpack.siem.case.caseView.comment.addCommentHelpText', + { + defaultMessage: 'Add a new comment...', + } +); + +export const SAVE = i18n.translate('xpack.siem.case.caseView.description.save', { + defaultMessage: 'Save', +}); + +export const GO_TO_DOCUMENTATION = i18n.translate( + 'xpack.siem.case.caseView.goToDocumentationButton', + { + defaultMessage: 'View documentation', + } +); + +export const CONNECTORS = i18n.translate('xpack.siem.case.caseView.connectors', { + defaultMessage: 'External incident management system', +}); + +export const EDIT_CONNECTOR = i18n.translate('xpack.siem.case.caseView.editConnector', { + defaultMessage: 'Change external incident management system', +}); diff --git a/x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/common/components/add_filter_to_global_search_bar/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/add_filter_to_global_search_bar/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.test.tsx b/x-pack/plugins/siem/public/common/components/add_filter_to_global_search_bar/helpers.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.test.tsx rename to x-pack/plugins/siem/public/common/components/add_filter_to_global_search_bar/helpers.test.tsx diff --git a/x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.ts b/x-pack/plugins/siem/public/common/components/add_filter_to_global_search_bar/helpers.ts similarity index 100% rename from x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/helpers.ts rename to x-pack/plugins/siem/public/common/components/add_filter_to_global_search_bar/helpers.ts diff --git a/x-pack/plugins/siem/public/common/components/add_filter_to_global_search_bar/index.test.tsx b/x-pack/plugins/siem/public/common/components/add_filter_to_global_search_bar/index.test.tsx new file mode 100644 index 00000000000000..18c0032f58c3c3 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/add_filter_to_global_search_bar/index.test.tsx @@ -0,0 +1,175 @@ +/* + * 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 { mount, shallow } from 'enzyme'; +import React from 'react'; + +import { + apolloClientObservable, + mockGlobalState, + TestProviders, + SUB_PLUGINS_REDUCER, +} from '../../mock'; +import { createStore, State } from '../../store'; +import { AddFilterToGlobalSearchBar } from '.'; + +const mockAddFilters = jest.fn(); +jest.mock('../../lib/kibana', () => ({ + useKibana: () => ({ + services: { + data: { + query: { + filterManager: { + addFilters: mockAddFilters, + }, + }, + }, + }, + }), +})); + +describe('AddFilterToGlobalSearchBar Component', () => { + const state: State = mockGlobalState; + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + + beforeEach(() => { + store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + mockAddFilters.mockClear(); + }); + + test('Rendering', async () => { + const wrapper = shallow( + + <>{'siem-kibana'} + + ); + + expect(wrapper).toMatchSnapshot(); + }); + + test('Rendering tooltip', async () => { + const wrapper = shallow( + + + <>{'siem-kibana'} + + + ); + + wrapper.simulate('mouseenter'); + wrapper.update(); + expect(wrapper.find('[data-test-subj="hover-actions-container"] svg').first()).toBeTruthy(); + }); + + test('Functionality with inputs state', async () => { + const onFilterAdded = jest.fn(); + + const wrapper = mount( + + + <>{'siem-kibana'} + + + ); + + wrapper + .simulate('mouseenter') + .find('[data-test-subj="hover-actions-container"] [data-euiicon-type]') + .first() + .simulate('click'); + wrapper.update(); + + expect(mockAddFilters.mock.calls[0][0]).toEqual({ + meta: { + alias: null, + disabled: false, + key: 'host.name', + negate: false, + params: { + query: 'siem-kibana', + }, + type: 'phrase', + value: 'siem-kibana', + }, + query: { + match: { + 'host.name': { + query: 'siem-kibana', + type: 'phrase', + }, + }, + }, + }); + expect(onFilterAdded).toHaveBeenCalledTimes(1); + }); +}); diff --git a/x-pack/plugins/siem/public/common/components/add_filter_to_global_search_bar/index.tsx b/x-pack/plugins/siem/public/common/components/add_filter_to_global_search_bar/index.tsx new file mode 100644 index 00000000000000..8a294ec1b71fdd --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/add_filter_to_global_search_bar/index.tsx @@ -0,0 +1,81 @@ +/* + * 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 { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import React, { useCallback } from 'react'; + +import { Filter } from '../../../../../../../src/plugins/data/public'; +import { WithHoverActions } from '../with_hover_actions'; +import { useKibana } from '../../lib/kibana'; + +import * as i18n from './translations'; + +export * from './helpers'; + +interface OwnProps { + children: JSX.Element; + filter: Filter; + onFilterAdded?: () => void; +} + +export const AddFilterToGlobalSearchBar = React.memo( + ({ children, filter, onFilterAdded }) => { + const { filterManager } = useKibana().services.data.query; + + const filterForValue = useCallback(() => { + filterManager.addFilters(filter); + + if (onFilterAdded != null) { + onFilterAdded(); + } + }, [filterManager, filter, onFilterAdded]); + + const filterOutValue = useCallback(() => { + filterManager.addFilters({ + ...filter, + meta: { + ...filter.meta, + negate: true, + }, + }); + + if (onFilterAdded != null) { + onFilterAdded(); + } + }, [filterManager, filter, onFilterAdded]); + + return ( + + + + + + + + + + } + render={() => children} + /> + ); + } +); + +AddFilterToGlobalSearchBar.displayName = 'AddFilterToGlobalSearchBar'; diff --git a/x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/translations.ts b/x-pack/plugins/siem/public/common/components/add_filter_to_global_search_bar/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/translations.ts rename to x-pack/plugins/siem/public/common/components/add_filter_to_global_search_bar/translations.ts diff --git a/x-pack/plugins/siem/public/components/alerts_viewer/alerts_table.tsx b/x-pack/plugins/siem/public/common/components/alerts_viewer/alerts_table.tsx similarity index 96% rename from x-pack/plugins/siem/public/components/alerts_viewer/alerts_table.tsx rename to x-pack/plugins/siem/public/common/components/alerts_viewer/alerts_table.tsx index d545a071c3ea68..dd608babef48fc 100644 --- a/x-pack/plugins/siem/public/components/alerts_viewer/alerts_table.tsx +++ b/x-pack/plugins/siem/public/common/components/alerts_viewer/alerts_table.tsx @@ -6,7 +6,7 @@ import React, { useMemo } from 'react'; -import { Filter } from '../../../../../../src/plugins/data/public'; +import { Filter } from '../../../../../../../src/plugins/data/public'; import { StatefulEventsViewer } from '../events_viewer'; import * as i18n from './translations'; import { alertsDefaultModel } from './default_headers'; diff --git a/x-pack/plugins/siem/public/components/alerts_viewer/default_headers.ts b/x-pack/plugins/siem/public/common/components/alerts_viewer/default_headers.ts similarity index 85% rename from x-pack/plugins/siem/public/components/alerts_viewer/default_headers.ts rename to x-pack/plugins/siem/public/common/components/alerts_viewer/default_headers.ts index b12bd1b6c2a516..cf5b565b99f673 100644 --- a/x-pack/plugins/siem/public/components/alerts_viewer/default_headers.ts +++ b/x-pack/plugins/siem/public/common/components/alerts_viewer/default_headers.ts @@ -4,13 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -import { defaultColumnHeaderType } from '../timeline/body/column_headers/default_headers'; +import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers'; import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH, -} from '../timeline/body/constants'; -import { ColumnHeaderOptions, SubsetTimelineModel } from '../../store/timeline/model'; -import { timelineDefaults } from '../../store/timeline/defaults'; +} from '../../../timelines/components/timeline/body/constants'; +import { ColumnHeaderOptions, SubsetTimelineModel } from '../../../timelines/store/timeline/model'; +import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; export const alertsHeaders: ColumnHeaderOptions[] = [ { diff --git a/x-pack/plugins/siem/public/common/components/alerts_viewer/histogram_configs.ts b/x-pack/plugins/siem/public/common/components/alerts_viewer/histogram_configs.ts new file mode 100644 index 00000000000000..5a00079bb056b1 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/alerts_viewer/histogram_configs.ts @@ -0,0 +1,32 @@ +/* + * 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 i18n from './translations'; +import { MatrixHistogramOption, MatrixHisrogramConfigs } from '../matrix_histogram/types'; +import { HistogramType } from '../../../graphql/types'; + +export const alertsStackByOptions: MatrixHistogramOption[] = [ + { + text: 'event.category', + value: 'event.category', + }, + { + text: 'event.module', + value: 'event.module', + }, +]; + +const DEFAULT_STACK_BY = 'event.module'; + +export const histogramConfigs: MatrixHisrogramConfigs = { + defaultStackByOption: + alertsStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? alertsStackByOptions[1], + errorMessage: i18n.ERROR_FETCHING_ALERTS_DATA, + histogramType: HistogramType.alerts, + stackByOptions: alertsStackByOptions, + subtitle: undefined, + title: i18n.ALERTS_GRAPH_TITLE, +}; diff --git a/x-pack/plugins/siem/public/common/components/alerts_viewer/index.tsx b/x-pack/plugins/siem/public/common/components/alerts_viewer/index.tsx new file mode 100644 index 00000000000000..29f4bdff92ad60 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/alerts_viewer/index.tsx @@ -0,0 +1,67 @@ +/* + * 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, useCallback, useMemo } from 'react'; +import numeral from '@elastic/numeral'; + +import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; +import { AlertsComponentsQueryProps } from './types'; +import { AlertsTable } from './alerts_table'; +import * as i18n from './translations'; +import { useUiSetting$ } from '../../lib/kibana'; +import { MatrixHistogramContainer } from '../matrix_histogram'; +import { histogramConfigs } from './histogram_configs'; +import { MatrixHisrogramConfigs } from '../matrix_histogram/types'; +const ID = 'alertsOverTimeQuery'; + +export const AlertsView = ({ + deleteQuery, + endDate, + filterQuery, + pageFilters, + setQuery, + startDate, + type, +}: AlertsComponentsQueryProps) => { + const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); + const getSubtitle = useCallback( + (totalCount: number) => + `${i18n.SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${i18n.UNIT( + totalCount + )}`, + [] + ); + const alertsHistogramConfigs: MatrixHisrogramConfigs = useMemo( + () => ({ + ...histogramConfigs, + subtitle: getSubtitle, + }), + [getSubtitle] + ); + useEffect(() => { + return () => { + if (deleteQuery) { + deleteQuery({ id: ID }); + } + }; + }, [deleteQuery]); + + return ( + <> + + + + ); +}; +AlertsView.displayName = 'AlertsView'; diff --git a/x-pack/plugins/siem/public/components/alerts_viewer/translations.ts b/x-pack/plugins/siem/public/common/components/alerts_viewer/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/alerts_viewer/translations.ts rename to x-pack/plugins/siem/public/common/components/alerts_viewer/translations.ts diff --git a/x-pack/plugins/siem/public/common/components/alerts_viewer/types.ts b/x-pack/plugins/siem/public/common/components/alerts_viewer/types.ts new file mode 100644 index 00000000000000..2bc33aaf1bae77 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/alerts_viewer/types.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 { Filter } from '../../../../../../../src/plugins/data/public'; +import { HostsComponentsQueryProps } from '../../../hosts/pages/navigation/types'; +import { NetworkComponentQueryProps } from '../../../network/pages/navigation/types'; +import { MatrixHistogramOption } from '../matrix_histogram/types'; + +type CommonQueryProps = HostsComponentsQueryProps | NetworkComponentQueryProps; +export interface AlertsComponentsQueryProps + extends Pick< + CommonQueryProps, + 'deleteQuery' | 'endDate' | 'filterQuery' | 'skip' | 'setQuery' | 'startDate' | 'type' + > { + pageFilters: Filter[]; + stackByOptions?: MatrixHistogramOption[]; + defaultFilters?: Filter[]; + defaultStackByOption?: MatrixHistogramOption; +} diff --git a/x-pack/plugins/siem/public/components/autocomplete_field/__examples__/index.stories.tsx b/x-pack/plugins/siem/public/common/components/autocomplete_field/__examples__/index.stories.tsx similarity index 94% rename from x-pack/plugins/siem/public/components/autocomplete_field/__examples__/index.stories.tsx rename to x-pack/plugins/siem/public/common/components/autocomplete_field/__examples__/index.stories.tsx index dccc156ff6e447..8f261da629f949 100644 --- a/x-pack/plugins/siem/public/components/autocomplete_field/__examples__/index.stories.tsx +++ b/x-pack/plugins/siem/public/common/components/autocomplete_field/__examples__/index.stories.tsx @@ -11,7 +11,7 @@ import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; import { QuerySuggestion, QuerySuggestionTypes, -} from '../../../../../../../src/plugins/data/public'; +} from '../../../../../../../../src/plugins/data/public'; import { SuggestionItem } from '../suggestion_item'; const suggestion: QuerySuggestion = { diff --git a/x-pack/plugins/siem/public/components/autocomplete_field/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/common/components/autocomplete_field/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/autocomplete_field/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/autocomplete_field/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/common/components/autocomplete_field/index.test.tsx b/x-pack/plugins/siem/public/common/components/autocomplete_field/index.test.tsx new file mode 100644 index 00000000000000..55e114818ffea1 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/autocomplete_field/index.test.tsx @@ -0,0 +1,388 @@ +/* + * 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 { EuiFieldSearch } from '@elastic/eui'; +import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { mount, shallow } from 'enzyme'; +import { noop } from 'lodash/fp'; +import React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { + QuerySuggestion, + QuerySuggestionTypes, +} from '../../../../../../../src/plugins/data/public'; + +import { TestProviders } from '../../mock'; + +import { AutocompleteField } from '.'; + +const mockAutoCompleteData: QuerySuggestion[] = [ + { + type: QuerySuggestionTypes.Field, + text: 'agent.ephemeral_id ', + description: + '

Filter results that contain agent.ephemeral_id

', + start: 0, + end: 1, + }, + { + type: QuerySuggestionTypes.Field, + text: 'agent.hostname ', + description: + '

Filter results that contain agent.hostname

', + start: 0, + end: 1, + }, + { + type: QuerySuggestionTypes.Field, + text: 'agent.id ', + description: + '

Filter results that contain agent.id

', + start: 0, + end: 1, + }, + { + type: QuerySuggestionTypes.Field, + text: 'agent.name ', + description: + '

Filter results that contain agent.name

', + start: 0, + end: 1, + }, + { + type: QuerySuggestionTypes.Field, + text: 'agent.type ', + description: + '

Filter results that contain agent.type

', + start: 0, + end: 1, + }, + { + type: QuerySuggestionTypes.Field, + text: 'agent.version ', + description: + '

Filter results that contain agent.version

', + start: 0, + end: 1, + }, + { + type: QuerySuggestionTypes.Field, + text: 'agent.test1 ', + description: + '

Filter results that contain agent.test1

', + start: 0, + end: 1, + }, + { + type: QuerySuggestionTypes.Field, + text: 'agent.test2 ', + description: + '

Filter results that contain agent.test2

', + start: 0, + end: 1, + }, + { + type: QuerySuggestionTypes.Field, + text: 'agent.test3 ', + description: + '

Filter results that contain agent.test3

', + start: 0, + end: 1, + }, + { + type: QuerySuggestionTypes.Field, + text: 'agent.test4 ', + description: + '

Filter results that contain agent.test4

', + start: 0, + end: 1, + }, +]; + +describe('Autocomplete', () => { + describe('rendering', () => { + test('it renders against snapshot', () => { + const placeholder = 'myPlaceholder'; + + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); + + test('it is rendering with placeholder', () => { + const placeholder = 'myPlaceholder'; + + const wrapper = mount( + + ); + const input = wrapper.find('input[type="search"]'); + expect(input.find('[placeholder]').props().placeholder).toEqual(placeholder); + }); + + test('Rendering suggested items', () => { + const wrapper = mount( + ({ eui: euiDarkVars, darkMode: true })}> + + + ); + const wrapperAutocompleteField = wrapper.find(AutocompleteField); + wrapperAutocompleteField.setState({ areSuggestionsVisible: true }); + wrapper.update(); + + expect(wrapper.find('.euiPanel div[data-test-subj="suggestion-item"]').length).toEqual(10); + }); + + test('Should Not render suggested items if loading new suggestions', () => { + const wrapper = mount( + ({ eui: euiDarkVars, darkMode: true })}> + + + ); + const wrapperAutocompleteField = wrapper.find(AutocompleteField); + wrapperAutocompleteField.setState({ areSuggestionsVisible: true }); + wrapper.update(); + + expect(wrapper.find('.euiPanel div[data-test-subj="suggestion-item"]').length).toEqual(0); + }); + }); + + describe('events', () => { + test('OnChange should have been called', () => { + const onChange = jest.fn((value: string) => value); + + const wrapper = mount( + + ); + const wrapperFixedEuiFieldSearch = wrapper.find('input'); + wrapperFixedEuiFieldSearch.simulate('change', { target: { value: 'test' } }); + expect(onChange).toHaveBeenCalled(); + }); + }); + + test('OnSubmit should have been called by keying enter on the search input', () => { + const onSubmit = jest.fn((value: string) => value); + + const wrapper = mount( + + ); + const wrapperAutocompleteField = wrapper.find(AutocompleteField); + wrapperAutocompleteField.setState({ selectedIndex: null }); + const wrapperFixedEuiFieldSearch = wrapper.find('input'); + wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Enter', preventDefault: noop }); + expect(onSubmit).toHaveBeenCalled(); + }); + + test('OnSubmit should have been called by onSearch event on the input', () => { + const onSubmit = jest.fn((value: string) => value); + + const wrapper = mount( + + ); + const wrapperAutocompleteField = wrapper.find(AutocompleteField); + wrapperAutocompleteField.setState({ selectedIndex: null }); + const wrapperFixedEuiFieldSearch = wrapper.find(EuiFieldSearch); + // TODO: FixedEuiFieldSearch fails to import + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (wrapperFixedEuiFieldSearch as any).props().onSearch(); + expect(onSubmit).toHaveBeenCalled(); + }); + + test('OnChange should have been called if keying enter on a suggested item selected', () => { + const onChange = jest.fn((value: string) => value); + + const wrapper = mount( + + ); + const wrapperAutocompleteField = wrapper.find(AutocompleteField); + wrapperAutocompleteField.setState({ selectedIndex: 1 }); + const wrapperFixedEuiFieldSearch = wrapper.find('input'); + wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Enter', preventDefault: noop }); + expect(onChange).toHaveBeenCalled(); + }); + + test('OnChange should be called if tab is pressed when a suggested item is selected', () => { + const onChange = jest.fn((value: string) => value); + + const wrapper = mount( + + ); + const wrapperAutocompleteField = wrapper.find(AutocompleteField); + wrapperAutocompleteField.setState({ selectedIndex: 1 }); + const wrapperFixedEuiFieldSearch = wrapper.find('input'); + wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Tab', preventDefault: noop }); + expect(onChange).toHaveBeenCalled(); + }); + + test('OnChange should NOT be called if tab is pressed when more than one item is suggested, and no selection has been made', () => { + const onChange = jest.fn((value: string) => value); + + const wrapper = mount( + + ); + + const wrapperFixedEuiFieldSearch = wrapper.find('input'); + wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Tab', preventDefault: noop }); + expect(onChange).not.toHaveBeenCalled(); + }); + + test('OnChange should be called if tab is pressed when only one item is suggested, even though that item is NOT selected', () => { + const onChange = jest.fn((value: string) => value); + const onlyOneSuggestion = [mockAutoCompleteData[0]]; + + const wrapper = mount( + + + + ); + + const wrapperAutocompleteField = wrapper.find(AutocompleteField); + wrapperAutocompleteField.setState({ areSuggestionsVisible: true }); + const wrapperFixedEuiFieldSearch = wrapper.find('input'); + wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Tab', preventDefault: noop }); + expect(onChange).toHaveBeenCalled(); + }); + + test('OnChange should NOT be called if tab is pressed when 0 items are suggested', () => { + const onChange = jest.fn((value: string) => value); + + const wrapper = mount( + + ); + + const wrapperFixedEuiFieldSearch = wrapper.find('input'); + wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Tab', preventDefault: noop }); + expect(onChange).not.toHaveBeenCalled(); + }); + + test('Load more suggestions when arrowdown on the search bar', () => { + const loadSuggestions = jest.fn(noop); + + const wrapper = mount( + + ); + const wrapperFixedEuiFieldSearch = wrapper.find('input'); + wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'ArrowDown', preventDefault: noop }); + expect(loadSuggestions).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/siem/public/common/components/autocomplete_field/index.tsx b/x-pack/plugins/siem/public/common/components/autocomplete_field/index.tsx new file mode 100644 index 00000000000000..0140a652ba1837 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/autocomplete_field/index.tsx @@ -0,0 +1,333 @@ +/* + * 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 { + EuiFieldSearch, + EuiFieldSearchProps, + EuiOutsideClickDetector, + EuiPanel, +} from '@elastic/eui'; +import React from 'react'; +import { QuerySuggestion } from '../../../../../../../src/plugins/data/public'; + +import euiStyled from '../../../../../../legacy/common/eui_styled_components'; + +import { SuggestionItem } from './suggestion_item'; + +interface AutocompleteFieldProps { + 'data-test-subj'?: string; + isLoadingSuggestions: boolean; + isValid: boolean; + loadSuggestions: (value: string, cursorPosition: number, maxCount?: number) => void; + onSubmit?: (value: string) => void; + onChange?: (value: string) => void; + placeholder?: string; + suggestions: QuerySuggestion[]; + value: string; +} + +interface AutocompleteFieldState { + areSuggestionsVisible: boolean; + isFocused: boolean; + selectedIndex: number | null; +} + +export class AutocompleteField extends React.PureComponent< + AutocompleteFieldProps, + AutocompleteFieldState +> { + public readonly state: AutocompleteFieldState = { + areSuggestionsVisible: false, + isFocused: false, + selectedIndex: null, + }; + + private inputElement: HTMLInputElement | null = null; + + public render() { + const { + 'data-test-subj': dataTestSubj, + suggestions, + isLoadingSuggestions, + isValid, + placeholder, + value, + } = this.props; + const { areSuggestionsVisible, selectedIndex } = this.state; + return ( + + + + {areSuggestionsVisible && !isLoadingSuggestions && suggestions.length > 0 ? ( + + {suggestions.map((suggestion, suggestionIndex) => ( + + ))} + + ) : null} + + + ); + } + + public componentDidUpdate(prevProps: AutocompleteFieldProps, prevState: AutocompleteFieldState) { + const hasNewValue = prevProps.value !== this.props.value; + const hasNewSuggestions = prevProps.suggestions !== this.props.suggestions; + + if (hasNewValue) { + this.updateSuggestions(); + } + + if (hasNewSuggestions && this.state.isFocused) { + this.showSuggestions(); + } + } + + private handleChangeInputRef = (element: HTMLInputElement | null) => { + this.inputElement = element; + }; + + private handleChange = (evt: React.ChangeEvent) => { + this.changeValue(evt.currentTarget.value); + }; + + private handleKeyDown = (evt: React.KeyboardEvent) => { + const { suggestions } = this.props; + switch (evt.key) { + case 'ArrowUp': + evt.preventDefault(); + if (suggestions.length > 0) { + this.setState( + composeStateUpdaters(withSuggestionsVisible, withPreviousSuggestionSelected) + ); + } + break; + case 'ArrowDown': + evt.preventDefault(); + if (suggestions.length > 0) { + this.setState(composeStateUpdaters(withSuggestionsVisible, withNextSuggestionSelected)); + } else { + this.updateSuggestions(); + } + break; + case 'Enter': + evt.preventDefault(); + if (this.state.selectedIndex !== null) { + this.applySelectedSuggestion(); + } else { + this.submit(); + } + break; + case 'Tab': + evt.preventDefault(); + if (this.state.areSuggestionsVisible && this.props.suggestions.length === 1) { + this.applySuggestionAt(0)(); + } else if (this.state.selectedIndex !== null) { + this.applySelectedSuggestion(); + } + break; + case 'Escape': + evt.preventDefault(); + evt.stopPropagation(); + this.setState(withSuggestionsHidden); + break; + } + }; + + private handleKeyUp = (evt: React.KeyboardEvent) => { + switch (evt.key) { + case 'ArrowLeft': + case 'ArrowRight': + case 'Home': + case 'End': + this.updateSuggestions(); + break; + } + }; + + private handleFocus = () => { + this.setState(composeStateUpdaters(withSuggestionsVisible, withFocused)); + }; + + private handleBlur = () => { + this.setState(composeStateUpdaters(withSuggestionsHidden, withUnfocused)); + }; + + private selectSuggestionAt = (index: number) => () => { + this.setState(withSuggestionAtIndexSelected(index)); + }; + + private applySelectedSuggestion = () => { + if (this.state.selectedIndex !== null) { + this.applySuggestionAt(this.state.selectedIndex)(); + } + }; + + private applySuggestionAt = (index: number) => () => { + const { value, suggestions } = this.props; + const selectedSuggestion = suggestions[index]; + + if (!selectedSuggestion) { + return; + } + + const newValue = + value.substr(0, selectedSuggestion.start) + + selectedSuggestion.text + + value.substr(selectedSuggestion.end); + + this.setState(withSuggestionsHidden); + this.changeValue(newValue); + this.focusInputElement(); + }; + + private changeValue = (value: string) => { + const { onChange } = this.props; + + if (onChange) { + onChange(value); + } + }; + + private focusInputElement = () => { + if (this.inputElement) { + this.inputElement.focus(); + } + }; + + private showSuggestions = () => { + this.setState(withSuggestionsVisible); + }; + + private submit = () => { + const { isValid, onSubmit, value } = this.props; + + if (isValid && onSubmit) { + onSubmit(value); + } + + this.setState(withSuggestionsHidden); + }; + + private updateSuggestions = () => { + const inputCursorPosition = this.inputElement ? this.inputElement.selectionStart || 0 : 0; + this.props.loadSuggestions(this.props.value, inputCursorPosition, 10); + }; +} + +type StateUpdater = ( + prevState: Readonly, + prevProps: Readonly +) => State | null; + +function composeStateUpdaters(...updaters: Array>) { + return (state: State, props: Props) => + updaters.reduce((currentState, updater) => updater(currentState, props) || currentState, state); +} + +const withPreviousSuggestionSelected = ( + state: AutocompleteFieldState, + props: AutocompleteFieldProps +): AutocompleteFieldState => ({ + ...state, + selectedIndex: + props.suggestions.length === 0 + ? null + : state.selectedIndex !== null + ? (state.selectedIndex + props.suggestions.length - 1) % props.suggestions.length + : Math.max(props.suggestions.length - 1, 0), +}); + +const withNextSuggestionSelected = ( + state: AutocompleteFieldState, + props: AutocompleteFieldProps +): AutocompleteFieldState => ({ + ...state, + selectedIndex: + props.suggestions.length === 0 + ? null + : state.selectedIndex !== null + ? (state.selectedIndex + 1) % props.suggestions.length + : 0, +}); + +const withSuggestionAtIndexSelected = (suggestionIndex: number) => ( + state: AutocompleteFieldState, + props: AutocompleteFieldProps +): AutocompleteFieldState => ({ + ...state, + selectedIndex: + props.suggestions.length === 0 + ? null + : suggestionIndex >= 0 && suggestionIndex < props.suggestions.length + ? suggestionIndex + : 0, +}); + +const withSuggestionsVisible = (state: AutocompleteFieldState) => ({ + ...state, + areSuggestionsVisible: true, +}); + +const withSuggestionsHidden = (state: AutocompleteFieldState) => ({ + ...state, + areSuggestionsVisible: false, + selectedIndex: null, +}); + +const withFocused = (state: AutocompleteFieldState) => ({ + ...state, + isFocused: true, +}); + +const withUnfocused = (state: AutocompleteFieldState) => ({ + ...state, + isFocused: false, +}); + +export const FixedEuiFieldSearch: React.FC & + EuiFieldSearchProps & { + inputRef?: (element: HTMLInputElement | null) => void; + onSearch: (value: string) => void; + }> = EuiFieldSearch as any; // eslint-disable-line @typescript-eslint/no-explicit-any + +const AutocompleteContainer = euiStyled.div` + position: relative; +`; + +AutocompleteContainer.displayName = 'AutocompleteContainer'; + +const SuggestionsPanel = euiStyled(EuiPanel).attrs(() => ({ + paddingSize: 'none', + hasShadow: true, +}))` + position: absolute; + width: 100%; + margin-top: 2px; + overflow: hidden; + z-index: ${props => props.theme.eui.euiZLevel1}; +`; + +SuggestionsPanel.displayName = 'SuggestionsPanel'; diff --git a/x-pack/plugins/siem/public/components/autocomplete_field/suggestion_item.tsx b/x-pack/plugins/siem/public/common/components/autocomplete_field/suggestion_item.tsx similarity index 96% rename from x-pack/plugins/siem/public/components/autocomplete_field/suggestion_item.tsx rename to x-pack/plugins/siem/public/common/components/autocomplete_field/suggestion_item.tsx index be9a9817265b0a..b305663dd48be9 100644 --- a/x-pack/plugins/siem/public/components/autocomplete_field/suggestion_item.tsx +++ b/x-pack/plugins/siem/public/common/components/autocomplete_field/suggestion_item.tsx @@ -8,8 +8,8 @@ import { EuiIcon } from '@elastic/eui'; import { transparentize } from 'polished'; import React from 'react'; import styled from 'styled-components'; -import euiStyled from '../../../../../legacy/common/eui_styled_components'; -import { QuerySuggestion } from '../../../../../../src/plugins/data/public'; +import euiStyled from '../../../../../../legacy/common/eui_styled_components'; +import { QuerySuggestion } from '../../../../../../../src/plugins/data/public'; interface SuggestionItemProps { isSelected?: boolean; diff --git a/x-pack/plugins/siem/public/components/charts/__snapshots__/areachart.test.tsx.snap b/x-pack/plugins/siem/public/common/components/charts/__snapshots__/areachart.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/charts/__snapshots__/areachart.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/charts/__snapshots__/areachart.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/charts/__snapshots__/barchart.test.tsx.snap b/x-pack/plugins/siem/public/common/components/charts/__snapshots__/barchart.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/charts/__snapshots__/barchart.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/charts/__snapshots__/barchart.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/charts/areachart.test.tsx b/x-pack/plugins/siem/public/common/components/charts/areachart.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/charts/areachart.test.tsx rename to x-pack/plugins/siem/public/common/components/charts/areachart.test.tsx diff --git a/x-pack/plugins/siem/public/components/charts/areachart.tsx b/x-pack/plugins/siem/public/common/components/charts/areachart.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/charts/areachart.tsx rename to x-pack/plugins/siem/public/common/components/charts/areachart.tsx diff --git a/x-pack/plugins/siem/public/components/charts/barchart.test.tsx b/x-pack/plugins/siem/public/common/components/charts/barchart.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/charts/barchart.test.tsx rename to x-pack/plugins/siem/public/common/components/charts/barchart.test.tsx diff --git a/x-pack/plugins/siem/public/components/charts/barchart.tsx b/x-pack/plugins/siem/public/common/components/charts/barchart.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/charts/barchart.tsx rename to x-pack/plugins/siem/public/common/components/charts/barchart.tsx diff --git a/x-pack/plugins/siem/public/components/charts/chart_place_holder.test.tsx b/x-pack/plugins/siem/public/common/components/charts/chart_place_holder.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/charts/chart_place_holder.test.tsx rename to x-pack/plugins/siem/public/common/components/charts/chart_place_holder.test.tsx diff --git a/x-pack/plugins/siem/public/components/charts/chart_place_holder.tsx b/x-pack/plugins/siem/public/common/components/charts/chart_place_holder.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/charts/chart_place_holder.tsx rename to x-pack/plugins/siem/public/common/components/charts/chart_place_holder.tsx diff --git a/x-pack/plugins/siem/public/components/charts/common.test.tsx b/x-pack/plugins/siem/public/common/components/charts/common.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/charts/common.test.tsx rename to x-pack/plugins/siem/public/common/components/charts/common.test.tsx diff --git a/x-pack/plugins/siem/public/components/charts/common.tsx b/x-pack/plugins/siem/public/common/components/charts/common.tsx similarity index 98% rename from x-pack/plugins/siem/public/components/charts/common.tsx rename to x-pack/plugins/siem/public/common/components/charts/common.tsx index 7e4b3079160421..1078040e9efd02 100644 --- a/x-pack/plugins/siem/public/components/charts/common.tsx +++ b/x-pack/plugins/siem/public/common/components/charts/common.tsx @@ -20,7 +20,7 @@ import { import React, { useMemo } from 'react'; import styled from 'styled-components'; -import { DEFAULT_DARK_MODE } from '../../../common/constants'; +import { DEFAULT_DARK_MODE } from '../../../../common/constants'; import { useUiSetting } from '../../lib/kibana'; export const defaultChartHeight = '100%'; diff --git a/x-pack/plugins/siem/public/components/charts/draggable_legend.test.tsx b/x-pack/plugins/siem/public/common/components/charts/draggable_legend.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/charts/draggable_legend.test.tsx rename to x-pack/plugins/siem/public/common/components/charts/draggable_legend.test.tsx diff --git a/x-pack/plugins/siem/public/components/charts/draggable_legend.tsx b/x-pack/plugins/siem/public/common/components/charts/draggable_legend.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/charts/draggable_legend.tsx rename to x-pack/plugins/siem/public/common/components/charts/draggable_legend.tsx diff --git a/x-pack/plugins/siem/public/components/charts/draggable_legend_item.test.tsx b/x-pack/plugins/siem/public/common/components/charts/draggable_legend_item.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/charts/draggable_legend_item.test.tsx rename to x-pack/plugins/siem/public/common/components/charts/draggable_legend_item.test.tsx diff --git a/x-pack/plugins/siem/public/components/charts/draggable_legend_item.tsx b/x-pack/plugins/siem/public/common/components/charts/draggable_legend_item.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/charts/draggable_legend_item.tsx rename to x-pack/plugins/siem/public/common/components/charts/draggable_legend_item.tsx diff --git a/x-pack/plugins/siem/public/components/charts/translation.ts b/x-pack/plugins/siem/public/common/components/charts/translation.ts similarity index 100% rename from x-pack/plugins/siem/public/components/charts/translation.ts rename to x-pack/plugins/siem/public/common/components/charts/translation.ts diff --git a/x-pack/plugins/siem/public/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap b/x-pack/plugins/siem/public/common/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/drag_and_drop/__snapshots__/drag_drop_context_wrapper.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap b/x-pack/plugins/siem/public/common/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/drag_and_drop/__snapshots__/draggable_wrapper.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/drag_and_drop/__snapshots__/droppable_wrapper.test.tsx.snap b/x-pack/plugins/siem/public/common/components/drag_and_drop/__snapshots__/droppable_wrapper.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/drag_and_drop/__snapshots__/droppable_wrapper.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/drag_and_drop/__snapshots__/droppable_wrapper.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/drag_and_drop/drag_drop_context.tsx b/x-pack/plugins/siem/public/common/components/drag_and_drop/drag_drop_context.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/drag_and_drop/drag_drop_context.tsx rename to x-pack/plugins/siem/public/common/components/drag_and_drop/drag_drop_context.tsx diff --git a/x-pack/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.test.tsx b/x-pack/plugins/siem/public/common/components/drag_and_drop/drag_drop_context_wrapper.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.test.tsx rename to x-pack/plugins/siem/public/common/components/drag_and_drop/drag_drop_context_wrapper.test.tsx diff --git a/x-pack/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx b/x-pack/plugins/siem/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx similarity index 94% rename from x-pack/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx rename to x-pack/plugins/siem/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx index 8e6743ad8f92e6..3bd2a3da1c88b3 100644 --- a/x-pack/plugins/siem/public/components/drag_and_drop/drag_drop_context_wrapper.tsx +++ b/x-pack/plugins/siem/public/common/components/drag_and_drop/drag_drop_context_wrapper.tsx @@ -12,11 +12,12 @@ import { Dispatch } from 'redux'; import { BeforeCapture } from './drag_drop_context'; import { BrowserFields } from '../../containers/source'; -import { dragAndDropModel, dragAndDropSelectors, timelineSelectors } from '../../store'; +import { dragAndDropModel, dragAndDropSelectors } from '../../store'; +import { timelineSelectors } from '../../../timelines/store/timeline'; import { IdToDataProvider } from '../../store/drag_and_drop/model'; import { State } from '../../store/reducer'; -import { DataProvider } from '../timeline/data_providers/data_provider'; -import { reArrangeProviders } from '../timeline/data_providers/helpers'; +import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider'; +import { reArrangeProviders } from '../../../timelines/components/timeline/data_providers/helpers'; import { ACTIVE_TIMELINE_REDUX_ID } from '../top_n'; import { ADDED_TO_TIMELINE_MESSAGE } from '../../hooks/translations'; import { useAddToTimelineSensor } from '../../hooks/use_add_to_timeline'; diff --git a/x-pack/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx b/x-pack/plugins/siem/public/common/components/drag_and_drop/draggable_wrapper.test.tsx similarity index 98% rename from x-pack/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx rename to x-pack/plugins/siem/public/common/components/drag_and_drop/draggable_wrapper.test.tsx index cd9e1dc95ff01a..d1b3b671307d1c 100644 --- a/x-pack/plugins/siem/public/components/drag_and_drop/draggable_wrapper.test.tsx +++ b/x-pack/plugins/siem/public/common/components/drag_and_drop/draggable_wrapper.test.tsx @@ -11,7 +11,7 @@ import { DraggableStateSnapshot, DraggingStyle } from 'react-beautiful-dnd'; import { mockBrowserFields, mocksSource } from '../../containers/source/mock'; import { TestProviders } from '../../mock'; -import { mockDataProviders } from '../timeline/data_providers/mock/mock_data_providers'; +import { mockDataProviders } from '../../../timelines/components/timeline/data_providers/mock/mock_data_providers'; import { DragDropContextWrapper } from './drag_drop_context_wrapper'; import { ConditionalPortal, DraggableWrapper, getStyle } from './draggable_wrapper'; import { useMountAppended } from '../../utils/use_mount_appended'; diff --git a/x-pack/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx b/x-pack/plugins/siem/public/common/components/drag_and_drop/draggable_wrapper.tsx similarity index 98% rename from x-pack/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx rename to x-pack/plugins/siem/public/common/components/drag_and_drop/draggable_wrapper.tsx index 5676c8fe5c30bd..f90a5c1410c344 100644 --- a/x-pack/plugins/siem/public/components/drag_and_drop/draggable_wrapper.tsx +++ b/x-pack/plugins/siem/public/common/components/drag_and_drop/draggable_wrapper.tsx @@ -18,7 +18,7 @@ import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; import { dragAndDropActions } from '../../store/drag_and_drop'; -import { DataProvider } from '../timeline/data_providers/data_provider'; +import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider'; import { TruncatableText } from '../truncatable_text'; import { WithHoverActions } from '../with_hover_actions'; diff --git a/x-pack/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx b/x-pack/plugins/siem/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx similarity index 99% rename from x-pack/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx rename to x-pack/plugins/siem/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx index 1d9508fc28f3d9..a5fcdd9a943d89 100644 --- a/x-pack/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx +++ b/x-pack/plugins/siem/public/common/components/drag_and_drop/draggable_wrapper_hover_content.test.tsx @@ -13,8 +13,8 @@ import { wait } from '../../lib/helpers'; import { useKibana } from '../../lib/kibana'; import { TestProviders } from '../../mock'; import { createKibanaCoreStartMock } from '../../mock/kibana_core'; -import { FilterManager } from '../../../../../../src/plugins/data/public'; -import { TimelineContext } from '../timeline/timeline_context'; +import { FilterManager } from '../../../../../../../src/plugins/data/public'; +import { TimelineContext } from '../../../timelines/components/timeline/timeline_context'; import { DraggableWrapperHoverContent } from './draggable_wrapper_hover_content'; diff --git a/x-pack/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.tsx b/x-pack/plugins/siem/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx similarity index 96% rename from x-pack/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.tsx rename to x-pack/plugins/siem/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx index 6976714cbe324e..a0546dc64113cd 100644 --- a/x-pack/plugins/siem/public/components/drag_and_drop/draggable_wrapper_hover_content.tsx +++ b/x-pack/plugins/siem/public/common/components/drag_and_drop/draggable_wrapper_hover_content.tsx @@ -12,8 +12,8 @@ import { getAllFieldsByName, WithSource } from '../../containers/source'; import { useAddToTimeline } from '../../hooks/use_add_to_timeline'; import { WithCopyToClipboard } from '../../lib/clipboard/with_copy_to_clipboard'; import { useKibana } from '../../lib/kibana'; -import { createFilter } from '../page/add_filter_to_global_search_bar'; -import { useTimelineContext } from '../timeline/timeline_context'; +import { createFilter } from '../add_filter_to_global_search_bar'; +import { useTimelineContext } from '../../../timelines/components/timeline/timeline_context'; import { StatefulTopN } from '../top_n'; import { allowTopN } from './helpers'; diff --git a/x-pack/plugins/siem/public/components/drag_and_drop/droppable_wrapper.test.tsx b/x-pack/plugins/siem/public/common/components/drag_and_drop/droppable_wrapper.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/drag_and_drop/droppable_wrapper.test.tsx rename to x-pack/plugins/siem/public/common/components/drag_and_drop/droppable_wrapper.test.tsx diff --git a/x-pack/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx b/x-pack/plugins/siem/public/common/components/drag_and_drop/droppable_wrapper.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/drag_and_drop/droppable_wrapper.tsx rename to x-pack/plugins/siem/public/common/components/drag_and_drop/droppable_wrapper.tsx diff --git a/x-pack/plugins/siem/public/components/drag_and_drop/helpers.test.ts b/x-pack/plugins/siem/public/common/components/drag_and_drop/helpers.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/drag_and_drop/helpers.test.ts rename to x-pack/plugins/siem/public/common/components/drag_and_drop/helpers.test.ts diff --git a/x-pack/plugins/siem/public/common/components/drag_and_drop/helpers.ts b/x-pack/plugins/siem/public/common/components/drag_and_drop/helpers.ts new file mode 100644 index 00000000000000..ad370f647738f3 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/drag_and_drop/helpers.ts @@ -0,0 +1,334 @@ +/* + * 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 { isString } from 'lodash/fp'; +import { DropResult } from 'react-beautiful-dnd'; +import { Dispatch } from 'redux'; +import { ActionCreator } from 'typescript-fsa'; + +import { BrowserField, BrowserFields, getAllFieldsByName } from '../../containers/source'; +import { dragAndDropActions } from '../../store/actions'; +import { IdToDataProvider } from '../../store/drag_and_drop/model'; +import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; +import { timelineActions } from '../../../timelines/store/timeline'; +import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants'; +import { addContentToTimeline } from '../../../timelines/components/timeline/data_providers/helpers'; +import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider'; + +export const draggableIdPrefix = 'draggableId'; + +export const droppableIdPrefix = 'droppableId'; + +export const draggableContentPrefix = `${draggableIdPrefix}.content.`; + +export const draggableTimelineProvidersPrefix = `${draggableIdPrefix}.timelineProviders.`; + +export const draggableFieldPrefix = `${draggableIdPrefix}.field.`; + +export const droppableContentPrefix = `${droppableIdPrefix}.content.`; + +export const droppableFieldPrefix = `${droppableIdPrefix}.field.`; + +export const droppableTimelineProvidersPrefix = `${droppableIdPrefix}.timelineProviders.`; + +export const droppableTimelineColumnsPrefix = `${droppableIdPrefix}.timelineColumns.`; + +export const droppableTimelineFlyoutButtonPrefix = `${droppableIdPrefix}.flyoutButton.`; + +export const getDraggableId = (dataProviderId: string): string => + `${draggableContentPrefix}${dataProviderId}`; + +export const getDraggableFieldId = ({ + contextId, + fieldId, +}: { + contextId: string; + fieldId: string; +}): string => `${draggableFieldPrefix}${escapeContextId(contextId)}.${escapeFieldId(fieldId)}`; + +export const getTimelineProviderDroppableId = ({ + groupIndex, + timelineId, +}: { + groupIndex: number; + timelineId: string; +}): string => `${droppableTimelineProvidersPrefix}${timelineId}.group.${groupIndex}`; + +export const getTimelineProviderDraggableId = ({ + dataProviderId, + groupIndex, + timelineId, +}: { + dataProviderId: string; + groupIndex: number; + timelineId: string; +}): string => + `${draggableTimelineProvidersPrefix}${timelineId}.group.${groupIndex}.${dataProviderId}`; + +export const getDroppableId = (visualizationPlaceholderId: string): string => + `${droppableContentPrefix}${visualizationPlaceholderId}`; + +export const sourceIsContent = (result: DropResult): boolean => + result.source.droppableId.startsWith(droppableContentPrefix); + +export const sourceAndDestinationAreSameTimelineProviders = (result: DropResult): boolean => { + const regex = /^droppableId\.timelineProviders\.(\S+)\./; + const sourceMatches = result.source.droppableId.match(regex) ?? []; + const destinationMatches = result.destination?.droppableId.match(regex) ?? []; + + return ( + sourceMatches.length >= 2 && + destinationMatches.length >= 2 && + sourceMatches[1] === destinationMatches[1] + ); +}; + +export const draggableIsContent = (result: DropResult | { draggableId: string }): boolean => + result.draggableId.startsWith(draggableContentPrefix); + +export const draggableIsField = (result: DropResult | { draggableId: string }): boolean => + result.draggableId.startsWith(draggableFieldPrefix); + +export const reasonIsDrop = (result: DropResult): boolean => result.reason === 'DROP'; + +export const destinationIsTimelineProviders = (result: DropResult): boolean => + result.destination != null && + result.destination.droppableId.startsWith(droppableTimelineProvidersPrefix); + +export const destinationIsTimelineColumns = (result: DropResult): boolean => + result.destination != null && + result.destination.droppableId.startsWith(droppableTimelineColumnsPrefix); + +export const destinationIsTimelineButton = (result: DropResult): boolean => + result.destination != null && + result.destination.droppableId.startsWith(droppableTimelineFlyoutButtonPrefix); + +export const getProviderIdFromDraggable = (result: DropResult): string => + result.draggableId.substring(result.draggableId.lastIndexOf('.') + 1); + +export const getFieldIdFromDraggable = (result: DropResult): string => + unEscapeFieldId(result.draggableId.substring(result.draggableId.lastIndexOf('.') + 1)); + +export const escapeDataProviderId = (path: string) => path.replace(/\./g, '_'); + +export const escapeContextId = (path: string) => path.replace(/\./g, '_'); + +export const escapeFieldId = (path: string) => path.replace(/\./g, '!!!DOT!!!'); + +export const unEscapeFieldId = (path: string) => path.replace(/!!!DOT!!!/g, '.'); + +export const providerWasDroppedOnTimeline = (result: DropResult): boolean => + reasonIsDrop(result) && + draggableIsContent(result) && + sourceIsContent(result) && + destinationIsTimelineProviders(result); + +export const userIsReArrangingProviders = (result: DropResult): boolean => + reasonIsDrop(result) && sourceAndDestinationAreSameTimelineProviders(result); + +export const fieldWasDroppedOnTimelineColumns = (result: DropResult): boolean => + reasonIsDrop(result) && draggableIsField(result) && destinationIsTimelineColumns(result); + +interface AddProviderToTimelineParams { + activeTimelineDataProviders: DataProvider[]; + dataProviders: IdToDataProvider; + dispatch: Dispatch; + noProviderFound?: ActionCreator<{ + id: string; + }>; + onAddedToTimeline: (fieldOrValue: string) => void; + result: DropResult; + timelineId: string; +} + +interface AddFieldToTimelineColumnsParams { + upsertColumn?: ActionCreator<{ + column: ColumnHeaderOptions; + id: string; + index: number; + }>; + browserFields: BrowserFields; + dispatch: Dispatch; + result: DropResult; + timelineId: string; +} + +export const addProviderToTimeline = ({ + activeTimelineDataProviders, + dataProviders, + dispatch, + result, + timelineId, + noProviderFound = dragAndDropActions.noProviderFound, + onAddedToTimeline, +}: AddProviderToTimelineParams): void => { + const providerId = getProviderIdFromDraggable(result); + const providerToAdd = dataProviders[providerId]; + + if (providerToAdd) { + addContentToTimeline({ + dataProviders: activeTimelineDataProviders, + destination: result.destination, + dispatch, + onAddedToTimeline, + providerToAdd, + timelineId, + }); + } else { + dispatch(noProviderFound({ id: providerId })); + } +}; + +export const addFieldToTimelineColumns = ({ + upsertColumn = timelineActions.upsertColumn, + browserFields, + dispatch, + result, + timelineId, +}: AddFieldToTimelineColumnsParams): void => { + const fieldId = getFieldIdFromDraggable(result); + const allColumns = getAllFieldsByName(browserFields); + const column = allColumns[fieldId]; + + if (column != null) { + dispatch( + upsertColumn({ + column: { + category: column.category, + columnHeaderType: 'not-filtered', + description: isString(column.description) ? column.description : undefined, + example: isString(column.example) ? column.example : undefined, + id: fieldId, + type: column.type, + aggregatable: column.aggregatable, + width: DEFAULT_COLUMN_MIN_WIDTH, + }, + id: timelineId, + index: result.destination != null ? result.destination.index : 0, + }) + ); + } else { + // create a column definition, because it doesn't exist in the browserFields: + dispatch( + upsertColumn({ + column: { + columnHeaderType: 'not-filtered', + id: fieldId, + width: DEFAULT_COLUMN_MIN_WIDTH, + }, + id: timelineId, + index: result.destination != null ? result.destination.index : 0, + }) + ); + } +}; + +/** + * Prevents fields from being dragged or dropped to any area other than column + * header drop zone in the timeline + */ +export const DRAG_TYPE_FIELD = 'drag-type-field'; + +/** This class is added to the document body while dragging */ +export const IS_DRAGGING_CLASS_NAME = 'is-dragging'; + +/** This class is added to the document body while timeline field dragging */ +export const IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME = 'is-timeline-field-dragging'; + +export const allowTopN = ({ + browserField, + fieldName, +}: { + browserField: Partial | undefined; + fieldName: string; +}): boolean => { + const isAggregatable = browserField?.aggregatable ?? false; + const fieldType = browserField?.type ?? ''; + const isAllowedType = [ + 'boolean', + 'geo-point', + 'geo-shape', + 'ip', + 'keyword', + 'number', + 'numeric', + 'string', + ].includes(fieldType); + + // TODO: remove this explicit whitelist when the ECS documentation includes signals + const isWhitelistedNonBrowserField = [ + 'signal.ancestors.depth', + 'signal.ancestors.id', + 'signal.ancestors.rule', + 'signal.ancestors.type', + 'signal.original_event.action', + 'signal.original_event.category', + 'signal.original_event.code', + 'signal.original_event.created', + 'signal.original_event.dataset', + 'signal.original_event.duration', + 'signal.original_event.end', + 'signal.original_event.hash', + 'signal.original_event.id', + 'signal.original_event.kind', + 'signal.original_event.module', + 'signal.original_event.original', + 'signal.original_event.outcome', + 'signal.original_event.provider', + 'signal.original_event.risk_score', + 'signal.original_event.risk_score_norm', + 'signal.original_event.sequence', + 'signal.original_event.severity', + 'signal.original_event.start', + 'signal.original_event.timezone', + 'signal.original_event.type', + 'signal.original_time', + 'signal.parent.depth', + 'signal.parent.id', + 'signal.parent.index', + 'signal.parent.rule', + 'signal.parent.type', + 'signal.rule.created_by', + 'signal.rule.description', + 'signal.rule.enabled', + 'signal.rule.false_positives', + 'signal.rule.filters', + 'signal.rule.from', + 'signal.rule.id', + 'signal.rule.immutable', + 'signal.rule.index', + 'signal.rule.interval', + 'signal.rule.language', + 'signal.rule.max_signals', + 'signal.rule.name', + 'signal.rule.note', + 'signal.rule.output_index', + 'signal.rule.query', + 'signal.rule.references', + 'signal.rule.risk_score', + 'signal.rule.rule_id', + 'signal.rule.saved_id', + 'signal.rule.severity', + 'signal.rule.size', + 'signal.rule.tags', + 'signal.rule.threat', + 'signal.rule.threat.tactic.id', + 'signal.rule.threat.tactic.name', + 'signal.rule.threat.tactic.reference', + 'signal.rule.threat.technique.id', + 'signal.rule.threat.technique.name', + 'signal.rule.threat.technique.reference', + 'signal.rule.timeline_id', + 'signal.rule.timeline_title', + 'signal.rule.to', + 'signal.rule.type', + 'signal.rule.updated_by', + 'signal.rule.version', + 'signal.status', + ].includes(fieldName); + + return isWhitelistedNonBrowserField || (isAggregatable && isAllowedType); +}; diff --git a/x-pack/plugins/siem/public/components/drag_and_drop/provider_container.tsx b/x-pack/plugins/siem/public/common/components/drag_and_drop/provider_container.tsx similarity index 97% rename from x-pack/plugins/siem/public/components/drag_and_drop/provider_container.tsx rename to x-pack/plugins/siem/public/common/components/drag_and_drop/provider_container.tsx index c1f029086aa350..06cb8ee2e1a467 100644 --- a/x-pack/plugins/siem/public/components/drag_and_drop/provider_container.tsx +++ b/x-pack/plugins/siem/public/common/components/drag_and_drop/provider_container.tsx @@ -6,7 +6,7 @@ import React from 'react'; import styled, { css } from 'styled-components'; -import { STATEFUL_EVENT_CSS_CLASS_NAME } from '../timeline/helpers'; +import { STATEFUL_EVENT_CSS_CLASS_NAME } from '../../../timelines/components/timeline/helpers'; interface ProviderContainerProps { isDragging: boolean; diff --git a/x-pack/plugins/siem/public/components/drag_and_drop/translations.ts b/x-pack/plugins/siem/public/common/components/drag_and_drop/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/drag_and_drop/translations.ts rename to x-pack/plugins/siem/public/common/components/drag_and_drop/translations.ts diff --git a/x-pack/plugins/siem/public/components/draggables/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/common/components/draggables/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/draggables/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/draggables/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/draggables/field_badge/index.tsx b/x-pack/plugins/siem/public/common/components/draggables/field_badge/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/draggables/field_badge/index.tsx rename to x-pack/plugins/siem/public/common/components/draggables/field_badge/index.tsx diff --git a/x-pack/plugins/siem/public/components/draggables/field_badge/translations.ts b/x-pack/plugins/siem/public/common/components/draggables/field_badge/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/draggables/field_badge/translations.ts rename to x-pack/plugins/siem/public/common/components/draggables/field_badge/translations.ts diff --git a/x-pack/plugins/siem/public/components/draggables/index.test.tsx b/x-pack/plugins/siem/public/common/components/draggables/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/draggables/index.test.tsx rename to x-pack/plugins/siem/public/common/components/draggables/index.test.tsx diff --git a/x-pack/plugins/siem/public/common/components/draggables/index.tsx b/x-pack/plugins/siem/public/common/components/draggables/index.tsx new file mode 100644 index 00000000000000..fcf007a4cf1bab --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/draggables/index.tsx @@ -0,0 +1,176 @@ +/* + * 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 { EuiBadge, EuiToolTip, IconType } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +import { DragEffects, DraggableWrapper } from '../drag_and_drop/draggable_wrapper'; +import { escapeDataProviderId } from '../drag_and_drop/helpers'; +import { getEmptyStringTag } from '../empty_value'; +import { IS_OPERATOR } from '../../../timelines/components/timeline/data_providers/data_provider'; +import { Provider } from '../../../timelines/components/timeline/data_providers/provider'; + +export interface DefaultDraggableType { + id: string; + field: string; + value?: string | null; + name?: string | null; + queryValue?: string | null; + children?: React.ReactNode; + tooltipContent?: React.ReactNode; +} + +/** + * Only returns true if the specified tooltipContent is exactly `null`. + * Example input / output: + * `bob -> false` + * `undefined -> false` + * `thing -> false` + * `null -> true` + */ +export const tooltipContentIsExplicitlyNull = (tooltipContent?: React.ReactNode): boolean => + tooltipContent === null; // an explicit / exact null check + +/** + * Derives the tooltip content from the field name if no tooltip was specified + */ +export const getDefaultWhenTooltipIsUnspecified = ({ + field, + tooltipContent, +}: { + field: string; + tooltipContent?: React.ReactNode; +}): React.ReactNode => (tooltipContent != null ? tooltipContent : field); + +/** + * Renders the content of the draggable, wrapped in a tooltip + */ +const Content = React.memo<{ + children?: React.ReactNode; + field: string; + tooltipContent?: React.ReactNode; + value?: string | null; +}>(({ children, field, tooltipContent, value }) => + !tooltipContentIsExplicitlyNull(tooltipContent) ? ( + + <>{children ? children : value} + + ) : ( + <>{children ? children : value} + ) +); + +Content.displayName = 'Content'; + +/** + * Draggable text (or an arbitrary visualization specified by `children`) + * that's only displayed when the specified value is non-`null`. + * + * @param id - a unique draggable id, which typically follows the format `${contextId}-${eventId}-${field}-${value}` + * @param field - the name of the field, e.g. `network.transport` + * @param value - value of the field e.g. `tcp` + * @param name - defaulting to `field`, this optional human readable name is used by the `DataProvider` that represents the data + * @param children - defaults to displaying `value`, this allows an arbitrary visualization to be displayed in lieu of the default behavior + * @param tooltipContent - defaults to displaying `field`, pass `null` to + * prevent a tooltip from being displayed, or pass arbitrary content + * @param queryValue - defaults to `value`, this query overrides the `queryMatch.value` used by the `DataProvider` that represents the data + */ +export const DefaultDraggable = React.memo( + ({ id, field, value, name, children, tooltipContent, queryValue }) => + value != null ? ( + + snapshot.isDragging ? ( + + + + ) : ( + + {children} + + ) + } + /> + ) : null +); + +DefaultDraggable.displayName = 'DefaultDraggable'; + +export const Badge = styled(EuiBadge)` + vertical-align: top; +` as any; // eslint-disable-line @typescript-eslint/no-explicit-any + +Badge.displayName = 'Badge'; + +export type BadgeDraggableType = Omit & { + contextId: string; + eventId: string; + iconType?: IconType; + color?: string; +}; + +/** + * A draggable badge that's only displayed when the specified value is non-`null`. + * + * @param contextId - used as part of the formula to derive a unique draggable id, this describes the context e.g. `event-fields-browser` in which the badge is displayed + * @param eventId - uniquely identifies an event, as specified in the `_id` field of the document + * @param field - the name of the field, e.g. `network.transport` + * @param value - value of the field e.g. `tcp` + * @param iconType -the (optional) type of icon e.g. `snowflake` to display on the badge + * @param name - defaulting to `field`, this optional human readable name is used by the `DataProvider` that represents the data + * @param color - defaults to `hollow`, optionally overwrite the color of the badge icon + * @param children - defaults to displaying `value`, this allows an arbitrary visualization to be displayed in lieu of the default behavior + * @param tooltipContent - defaults to displaying `field`, pass `null` to + * prevent a tooltip from being displayed, or pass arbitrary content + * @param queryValue - defaults to `value`, this query overrides the `queryMatch.value` used by the `DataProvider` that represents the data + */ +export const DraggableBadge = React.memo( + ({ + contextId, + eventId, + field, + value, + iconType, + name, + color = 'hollow', + children, + tooltipContent, + queryValue, + }) => + value != null ? ( + + + {children ? children : value !== '' ? value : getEmptyStringTag()} + + + ) : null +); + +DraggableBadge.displayName = 'DraggableBadge'; diff --git a/x-pack/plugins/siem/public/components/empty_page/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/common/components/empty_page/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/empty_page/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/empty_page/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/empty_page/index.test.tsx b/x-pack/plugins/siem/public/common/components/empty_page/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/empty_page/index.test.tsx rename to x-pack/plugins/siem/public/common/components/empty_page/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/empty_page/index.tsx b/x-pack/plugins/siem/public/common/components/empty_page/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/empty_page/index.tsx rename to x-pack/plugins/siem/public/common/components/empty_page/index.tsx diff --git a/x-pack/plugins/siem/public/components/empty_value/__snapshots__/empty_value.test.tsx.snap b/x-pack/plugins/siem/public/common/components/empty_value/__snapshots__/empty_value.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/empty_value/__snapshots__/empty_value.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/empty_value/__snapshots__/empty_value.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/empty_value/empty_value.test.tsx b/x-pack/plugins/siem/public/common/components/empty_value/empty_value.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/empty_value/empty_value.test.tsx rename to x-pack/plugins/siem/public/common/components/empty_value/empty_value.test.tsx diff --git a/x-pack/plugins/siem/public/components/empty_value/index.tsx b/x-pack/plugins/siem/public/common/components/empty_value/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/empty_value/index.tsx rename to x-pack/plugins/siem/public/common/components/empty_value/index.tsx diff --git a/x-pack/plugins/siem/public/components/empty_value/translations.ts b/x-pack/plugins/siem/public/common/components/empty_value/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/empty_value/translations.ts rename to x-pack/plugins/siem/public/common/components/empty_value/translations.ts diff --git a/x-pack/plugins/siem/public/components/error_toast_dispatcher/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/common/components/error_toast_dispatcher/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/error_toast_dispatcher/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/error_toast_dispatcher/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/common/components/error_toast_dispatcher/index.test.tsx b/x-pack/plugins/siem/public/common/components/error_toast_dispatcher/index.test.tsx new file mode 100644 index 00000000000000..50b20099b17d07 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/error_toast_dispatcher/index.test.tsx @@ -0,0 +1,35 @@ +/* + * 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 { Provider } from 'react-redux'; + +import { apolloClientObservable, mockGlobalState, SUB_PLUGINS_REDUCER } from '../../mock'; +import { createStore } from '../../store/store'; + +import { ErrorToastDispatcher } from '.'; +import { State } from '../../store/reducer'; + +describe('Error Toast Dispatcher', () => { + const state: State = mockGlobalState; + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + + beforeEach(() => { + store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + }); + + describe('rendering', () => { + test('it renders', () => { + const wrapper = shallow( + + + + ); + expect(wrapper.find('Connect(ErrorToastDispatcherComponent)')).toMatchSnapshot(); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/components/error_toast_dispatcher/index.tsx b/x-pack/plugins/siem/public/common/components/error_toast_dispatcher/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/error_toast_dispatcher/index.tsx rename to x-pack/plugins/siem/public/common/components/error_toast_dispatcher/index.tsx diff --git a/x-pack/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap b/x-pack/plugins/siem/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/event_details/__snapshots__/event_details.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/event_details/__snapshots__/event_details.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/event_details/__snapshots__/json_view.test.tsx.snap b/x-pack/plugins/siem/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/event_details/__snapshots__/json_view.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/event_details/__snapshots__/json_view.test.tsx.snap diff --git a/x-pack/plugins/siem/public/common/components/event_details/columns.tsx b/x-pack/plugins/siem/public/common/components/event_details/columns.tsx new file mode 100644 index 00000000000000..4b5ce3b98e5e11 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/event_details/columns.tsx @@ -0,0 +1,210 @@ +/* + * 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. + */ + +/* eslint-disable react/display-name */ + +import { + EuiCheckbox, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiPanel, + EuiText, + EuiToolTip, +} from '@elastic/eui'; +import React from 'react'; +import { Draggable } from 'react-beautiful-dnd'; +import styled from 'styled-components'; + +import { BrowserFields } from '../../containers/source'; +import { ToStringArray } from '../../../graphql/types'; +import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; +import { DragEffects } from '../drag_and_drop/draggable_wrapper'; +import { DroppableWrapper } from '../drag_and_drop/droppable_wrapper'; +import { getDroppableId, getDraggableFieldId, DRAG_TYPE_FIELD } from '../drag_and_drop/helpers'; +import { DraggableFieldBadge } from '../draggables/field_badge'; +import { FieldName } from '../../../timelines/components/fields_browser/field_name'; +import { SelectableText } from '../selectable_text'; +import { OverflowField } from '../tables/helpers'; +import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers'; +import { DEFAULT_COLUMN_MIN_WIDTH } from '../../../timelines/components/timeline/body/constants'; +import { MESSAGE_FIELD_NAME } from '../../../timelines/components/timeline/body/renderers/constants'; +import { FormattedFieldValue } from '../../../timelines/components/timeline/body/renderers/formatted_field'; +import { OnUpdateColumns } from '../../../timelines/components/timeline/events'; +import { getIconFromType, getExampleText, getColumnsWithTimestamp } from './helpers'; +import * as i18n from './translations'; +import { EventFieldsData } from './types'; + +const HoverActionsContainer = styled(EuiPanel)` + align-items: center; + display: flex; + flex-direction: row; + height: 25px; + justify-content: center; + left: 5px; + position: absolute; + top: -10px; + width: 30px; +`; + +HoverActionsContainer.displayName = 'HoverActionsContainer'; + +export const getColumns = ({ + browserFields, + columnHeaders, + eventId, + onUpdateColumns, + contextId, + toggleColumn, +}: { + browserFields: BrowserFields; + columnHeaders: ColumnHeaderOptions[]; + eventId: string; + onUpdateColumns: OnUpdateColumns; + contextId: string; + toggleColumn: (column: ColumnHeaderOptions) => void; +}) => [ + { + field: 'field', + name: '', + sortable: false, + truncateText: false, + width: '30px', + render: (field: string) => ( + + c.id === field) !== -1} + data-test-subj={`toggle-field-${field}`} + id={field} + onChange={() => + toggleColumn({ + columnHeaderType: defaultColumnHeaderType, + id: field, + width: DEFAULT_COLUMN_MIN_WIDTH, + }) + } + /> + + ), + }, + { + field: 'field', + name: i18n.FIELD, + sortable: true, + truncateText: false, + render: (field: string, data: EventFieldsData) => ( + + + + + + + + + ( +
+ + + +
+ )} + > + + {provided => ( +
+ +
+ )} +
+
+
+
+ ), + }, + { + field: 'values', + name: i18n.VALUE, + sortable: true, + truncateText: false, + render: (values: ToStringArray | null | undefined, data: EventFieldsData) => ( + + {values != null && + values.map((value, i) => ( + + {data.field === MESSAGE_FIELD_NAME ? ( + + ) : ( + + )} + + ))} + + ), + }, + { + field: 'description', + name: i18n.DESCRIPTION, + render: (description: string | null | undefined, data: EventFieldsData) => ( + + {`${description || ''} ${getExampleText(data.example)}`} + + ), + sortable: true, + truncateText: true, + width: '50%', + }, + { + field: 'valuesConcatenated', + name: i18n.BLANK, + render: () => null, + sortable: false, + truncateText: true, + width: '1px', + }, +]; diff --git a/x-pack/plugins/siem/public/components/event_details/event_details.test.tsx b/x-pack/plugins/siem/public/common/components/event_details/event_details.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/event_details/event_details.test.tsx rename to x-pack/plugins/siem/public/common/components/event_details/event_details.test.tsx diff --git a/x-pack/plugins/siem/public/components/event_details/event_details.tsx b/x-pack/plugins/siem/public/common/components/event_details/event_details.tsx similarity index 90% rename from x-pack/plugins/siem/public/components/event_details/event_details.tsx rename to x-pack/plugins/siem/public/common/components/event_details/event_details.tsx index 9234fe44320f06..c6a7a05bb26989 100644 --- a/x-pack/plugins/siem/public/components/event_details/event_details.tsx +++ b/x-pack/plugins/siem/public/common/components/event_details/event_details.tsx @@ -9,9 +9,9 @@ import React from 'react'; import styled from 'styled-components'; import { BrowserFields } from '../../containers/source'; -import { DetailItem } from '../../graphql/types'; -import { ColumnHeaderOptions } from '../../store/timeline/model'; -import { OnUpdateColumns } from '../timeline/events'; +import { DetailItem } from '../../../graphql/types'; +import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; +import { OnUpdateColumns } from '../../../timelines/components/timeline/events'; import { EventFieldsBrowser } from './event_fields_browser'; import { JsonView } from './json_view'; import * as i18n from './translations'; diff --git a/x-pack/plugins/siem/public/components/event_details/event_fields_browser.test.tsx b/x-pack/plugins/siem/public/common/components/event_details/event_fields_browser.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/event_details/event_fields_browser.test.tsx rename to x-pack/plugins/siem/public/common/components/event_details/event_fields_browser.test.tsx diff --git a/x-pack/plugins/siem/public/components/event_details/event_fields_browser.tsx b/x-pack/plugins/siem/public/common/components/event_details/event_fields_browser.tsx similarity index 90% rename from x-pack/plugins/siem/public/components/event_details/event_fields_browser.tsx rename to x-pack/plugins/siem/public/common/components/event_details/event_fields_browser.tsx index 9a842339cb62ef..0428f3ec8a197b 100644 --- a/x-pack/plugins/siem/public/components/event_details/event_fields_browser.tsx +++ b/x-pack/plugins/siem/public/common/components/event_details/event_fields_browser.tsx @@ -8,10 +8,10 @@ import { sortBy } from 'lodash'; import { EuiInMemoryTable } from '@elastic/eui'; import React, { useMemo } from 'react'; -import { ColumnHeaderOptions } from '../../store/timeline/model'; +import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; import { BrowserFields, getAllFieldsByName } from '../../containers/source'; -import { DetailItem } from '../../graphql/types'; -import { OnUpdateColumns } from '../timeline/events'; +import { DetailItem } from '../../../graphql/types'; +import { OnUpdateColumns } from '../../../timelines/components/timeline/events'; import { getColumns } from './columns'; import { search } from './helpers'; diff --git a/x-pack/plugins/siem/public/components/event_details/event_id.ts b/x-pack/plugins/siem/public/common/components/event_details/event_id.ts similarity index 100% rename from x-pack/plugins/siem/public/components/event_details/event_id.ts rename to x-pack/plugins/siem/public/common/components/event_details/event_id.ts diff --git a/x-pack/plugins/siem/public/components/event_details/helpers.test.tsx b/x-pack/plugins/siem/public/common/components/event_details/helpers.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/event_details/helpers.test.tsx rename to x-pack/plugins/siem/public/common/components/event_details/helpers.test.tsx diff --git a/x-pack/plugins/siem/public/common/components/event_details/helpers.tsx b/x-pack/plugins/siem/public/common/components/event_details/helpers.tsx new file mode 100644 index 00000000000000..aae7ca901c3d21 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/event_details/helpers.tsx @@ -0,0 +1,115 @@ +/* + * 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 { get, getOr, isEmpty, uniqBy } from 'lodash/fp'; + +import { BrowserField, BrowserFields } from '../../containers/source'; +import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; +import { + DEFAULT_DATE_COLUMN_MIN_WIDTH, + DEFAULT_COLUMN_MIN_WIDTH, +} from '../../../timelines/components/timeline/body/constants'; +import { ToStringArray } from '../../../graphql/types'; + +import * as i18n from './translations'; + +/** + * Defines the behavior of the search input that appears above the table of data + */ +export const search = { + box: { + incremental: true, + placeholder: i18n.PLACEHOLDER, + schema: true, + }, +}; + +export interface ItemValues { + value: JSX.Element; + valueAsString: string; +} + +/** + * An item rendered in the table + */ +export interface Item { + description: string; + field: JSX.Element; + fieldId: string; + type: string; + values: ToStringArray; +} + +export const getColumnHeaderFromBrowserField = ({ + browserField, + width = DEFAULT_COLUMN_MIN_WIDTH, +}: { + browserField: Partial; + width?: number; +}): ColumnHeaderOptions => ({ + category: browserField.category, + columnHeaderType: 'not-filtered', + description: browserField.description != null ? browserField.description : undefined, + example: browserField.example != null ? `${browserField.example}` : undefined, + id: browserField.name || '', + type: browserField.type, + aggregatable: browserField.aggregatable, + width, +}); + +/** + * Returns a collection of columns, where the first column in the collection + * is a timestamp, and the remaining columns are all the columns in the + * specified category + */ +export const getColumnsWithTimestamp = ({ + browserFields, + category, +}: { + browserFields: BrowserFields; + category: string; +}): ColumnHeaderOptions[] => { + const emptyFields: Record> = {}; + const timestamp = get('base.fields.@timestamp', browserFields); + const categoryFields: Array> = [ + ...Object.values(getOr(emptyFields, `${category}.fields`, browserFields)), + ]; + + return timestamp != null && categoryFields.length + ? uniqBy('id', [ + getColumnHeaderFromBrowserField({ + browserField: timestamp, + width: DEFAULT_DATE_COLUMN_MIN_WIDTH, + }), + ...categoryFields.map(f => getColumnHeaderFromBrowserField({ browserField: f })), + ]) + : []; +}; + +/** Returns example text, or an empty string if the field does not have an example */ +export const getExampleText = (example: string | number | null | undefined): string => + !isEmpty(example) ? `Example: ${example}` : ''; + +export const getIconFromType = (type: string | null) => { + switch (type) { + case 'string': // fall through + case 'keyword': + return 'string'; + case 'number': // fall through + case 'long': + return 'number'; + case 'date': + return 'clock'; + case 'ip': + return 'globe'; + case 'object': + return 'questionInCircle'; + case 'float': + return 'number'; + default: + return 'questionInCircle'; + } +}; diff --git a/x-pack/plugins/siem/public/components/event_details/json_view.test.tsx b/x-pack/plugins/siem/public/common/components/event_details/json_view.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/event_details/json_view.test.tsx rename to x-pack/plugins/siem/public/common/components/event_details/json_view.test.tsx diff --git a/x-pack/plugins/siem/public/components/event_details/json_view.tsx b/x-pack/plugins/siem/public/common/components/event_details/json_view.tsx similarity index 88% rename from x-pack/plugins/siem/public/components/event_details/json_view.tsx rename to x-pack/plugins/siem/public/common/components/event_details/json_view.tsx index 9897e319e0487b..788ca95e2022ea 100644 --- a/x-pack/plugins/siem/public/components/event_details/json_view.tsx +++ b/x-pack/plugins/siem/public/common/components/event_details/json_view.tsx @@ -9,8 +9,8 @@ import { set } from 'lodash/fp'; import React from 'react'; import styled from 'styled-components'; -import { DetailItem } from '../../graphql/types'; -import { omitTypenameAndEmpty } from '../timeline/body/helpers'; +import { DetailItem } from '../../../graphql/types'; +import { omitTypenameAndEmpty } from '../../../timelines/components/timeline/body/helpers'; interface Props { data: DetailItem[]; diff --git a/x-pack/plugins/siem/public/components/event_details/stateful_event_details.tsx b/x-pack/plugins/siem/public/common/components/event_details/stateful_event_details.tsx similarity index 86% rename from x-pack/plugins/siem/public/components/event_details/stateful_event_details.tsx rename to x-pack/plugins/siem/public/common/components/event_details/stateful_event_details.tsx index c79f02740253a9..ec0e82c218a075 100644 --- a/x-pack/plugins/siem/public/components/event_details/stateful_event_details.tsx +++ b/x-pack/plugins/siem/public/common/components/event_details/stateful_event_details.tsx @@ -7,9 +7,9 @@ import React, { useCallback, useState } from 'react'; import { BrowserFields } from '../../containers/source'; -import { DetailItem } from '../../graphql/types'; -import { ColumnHeaderOptions } from '../../store/timeline/model'; -import { OnUpdateColumns } from '../timeline/events'; +import { DetailItem } from '../../../graphql/types'; +import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; +import { OnUpdateColumns } from '../../../timelines/components/timeline/events'; import { EventDetails, View } from './event_details'; diff --git a/x-pack/plugins/siem/public/components/event_details/translations.ts b/x-pack/plugins/siem/public/common/components/event_details/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/event_details/translations.ts rename to x-pack/plugins/siem/public/common/components/event_details/translations.ts diff --git a/x-pack/plugins/siem/public/common/components/event_details/types.ts b/x-pack/plugins/siem/public/common/components/event_details/types.ts new file mode 100644 index 00000000000000..db53f411fa5183 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/event_details/types.ts @@ -0,0 +1,10 @@ +/* + * 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 { BrowserField } from '../../containers/source'; +import { DetailItem } from '../../../graphql/types'; + +export type EventFieldsData = BrowserField & DetailItem; diff --git a/x-pack/plugins/siem/public/components/events_viewer/default_headers.tsx b/x-pack/plugins/siem/public/common/components/events_viewer/default_headers.tsx similarity index 84% rename from x-pack/plugins/siem/public/components/events_viewer/default_headers.tsx rename to x-pack/plugins/siem/public/common/components/events_viewer/default_headers.tsx index b97e0da5df078b..4660351e0d8f9a 100644 --- a/x-pack/plugins/siem/public/components/events_viewer/default_headers.tsx +++ b/x-pack/plugins/siem/public/common/components/events_viewer/default_headers.tsx @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ColumnHeaderOptions } from '../../store/timeline/model'; -import { defaultColumnHeaderType } from '../timeline/body/column_headers/default_headers'; +import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; +import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers'; import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH, -} from '../timeline/body/constants'; +} from '../../../timelines/components/timeline/body/constants'; export const defaultHeaders: ColumnHeaderOptions[] = [ { diff --git a/x-pack/plugins/siem/public/common/components/events_viewer/default_model.tsx b/x-pack/plugins/siem/public/common/components/events_viewer/default_model.tsx new file mode 100644 index 00000000000000..ecb76eb7ff93f2 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/events_viewer/default_model.tsx @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { defaultHeaders } from './default_headers'; +import { SubsetTimelineModel } from '../../../timelines/store/timeline/model'; +import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; + +export const eventsDefaultModel: SubsetTimelineModel = { + ...timelineDefaults, + columns: defaultHeaders, +}; diff --git a/x-pack/plugins/siem/public/components/events_viewer/event_details_width_context.tsx b/x-pack/plugins/siem/public/common/components/events_viewer/event_details_width_context.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/events_viewer/event_details_width_context.tsx rename to x-pack/plugins/siem/public/common/components/events_viewer/event_details_width_context.tsx diff --git a/x-pack/plugins/siem/public/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/siem/public/common/components/events_viewer/events_viewer.test.tsx similarity index 95% rename from x-pack/plugins/siem/public/components/events_viewer/events_viewer.test.tsx rename to x-pack/plugins/siem/public/common/components/events_viewer/events_viewer.test.tsx index d3cdf9886e4693..d2f0d47380dd27 100644 --- a/x-pack/plugins/siem/public/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/plugins/siem/public/common/components/events_viewer/events_viewer.test.tsx @@ -14,13 +14,13 @@ import { wait } from '../../lib/helpers'; import { mockEventViewerResponse } from './mock'; import { StatefulEventsViewer } from '.'; import { defaultHeaders } from './default_headers'; -import { useFetchIndexPatterns } from '../../containers/detection_engine/rules/fetch_index_patterns'; +import { useFetchIndexPatterns } from '../../../alerts/containers/detection_engine/rules/fetch_index_patterns'; import { mockBrowserFields } from '../../containers/source/mock'; import { eventsDefaultModel } from './default_model'; import { useMountAppended } from '../../utils/use_mount_appended'; const mockUseFetchIndexPatterns: jest.Mock = useFetchIndexPatterns as jest.Mock; -jest.mock('../../containers/detection_engine/rules/fetch_index_patterns'); +jest.mock('../../../alerts/containers/detection_engine/rules/fetch_index_patterns'); mockUseFetchIndexPatterns.mockImplementation(() => [ { browserFields: mockBrowserFields, diff --git a/x-pack/plugins/siem/public/components/events_viewer/events_viewer.tsx b/x-pack/plugins/siem/public/common/components/events_viewer/events_viewer.tsx similarity index 86% rename from x-pack/plugins/siem/public/components/events_viewer/events_viewer.tsx rename to x-pack/plugins/siem/public/common/components/events_viewer/events_viewer.tsx index aff66396af39d3..bec8c30ecdd384 100644 --- a/x-pack/plugins/siem/public/components/events_viewer/events_viewer.tsx +++ b/x-pack/plugins/siem/public/common/components/events_viewer/events_viewer.tsx @@ -11,23 +11,31 @@ import styled from 'styled-components'; import deepEqual from 'fast-deep-equal'; import { BrowserFields } from '../../containers/source'; -import { TimelineQuery } from '../../containers/timeline'; -import { Direction } from '../../graphql/types'; +import { TimelineQuery } from '../../../timelines/containers'; +import { Direction } from '../../../graphql/types'; import { useKibana } from '../../lib/kibana'; -import { ColumnHeaderOptions, KqlMode } from '../../store/timeline/model'; +import { ColumnHeaderOptions, KqlMode } from '../../../timelines/store/timeline/model'; import { HeaderSection } from '../header_section'; -import { defaultHeaders } from '../timeline/body/column_headers/default_headers'; -import { Sort } from '../timeline/body/sort'; -import { StatefulBody } from '../timeline/body/stateful_body'; -import { DataProvider } from '../timeline/data_providers/data_provider'; -import { OnChangeItemsPerPage } from '../timeline/events'; -import { Footer, footerHeight } from '../timeline/footer'; -import { combineQueries } from '../timeline/helpers'; -import { TimelineRefetch } from '../timeline/refetch_timeline'; -import { ManageTimelineContext, TimelineTypeContextProps } from '../timeline/timeline_context'; +import { defaultHeaders } from '../../../timelines/components/timeline/body/column_headers/default_headers'; +import { Sort } from '../../../timelines/components/timeline/body/sort'; +import { StatefulBody } from '../../../timelines/components/timeline/body/stateful_body'; +import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider'; +import { OnChangeItemsPerPage } from '../../../timelines/components/timeline/events'; +import { Footer, footerHeight } from '../../../timelines/components/timeline/footer'; +import { combineQueries } from '../../../timelines/components/timeline/helpers'; +import { TimelineRefetch } from '../../../timelines/components/timeline/refetch_timeline'; +import { + ManageTimelineContext, + TimelineTypeContextProps, +} from '../../../timelines/components/timeline/timeline_context'; import { EventDetailsWidthProvider } from './event_details_width_context'; import * as i18n from './translations'; -import { Filter, esQuery, IIndexPattern, Query } from '../../../../../../src/plugins/data/public'; +import { + Filter, + esQuery, + IIndexPattern, + Query, +} from '../../../../../../../src/plugins/data/public'; import { inputsModel } from '../../store'; const DEFAULT_EVENTS_VIEWER_HEIGHT = 500; diff --git a/x-pack/plugins/siem/public/common/components/events_viewer/index.test.tsx b/x-pack/plugins/siem/public/common/components/events_viewer/index.test.tsx new file mode 100644 index 00000000000000..bdc0338450507a --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/events_viewer/index.test.tsx @@ -0,0 +1,85 @@ +/* + * 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 { MockedProvider } from 'react-apollo/test-utils'; +import useResizeObserver from 'use-resize-observer/polyfilled'; + +import { wait } from '../../lib/helpers'; +import { mockIndexPattern, TestProviders } from '../../mock'; +import { useMountAppended } from '../../utils/use_mount_appended'; + +import { mockEventViewerResponse } from './mock'; +import { StatefulEventsViewer } from '.'; +import { useFetchIndexPatterns } from '../../../alerts/containers/detection_engine/rules/fetch_index_patterns'; +import { mockBrowserFields } from '../../containers/source/mock'; +import { eventsDefaultModel } from './default_model'; + +const mockUseFetchIndexPatterns: jest.Mock = useFetchIndexPatterns as jest.Mock; +jest.mock('../../../alerts/containers/detection_engine/rules/fetch_index_patterns'); +mockUseFetchIndexPatterns.mockImplementation(() => [ + { + browserFields: mockBrowserFields, + indexPatterns: mockIndexPattern, + }, +]); + +const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; +jest.mock('use-resize-observer/polyfilled'); +mockUseResizeObserver.mockImplementation(() => ({})); + +const from = 1566943856794; +const to = 1566857456791; + +describe('StatefulEventsViewer', () => { + const mount = useMountAppended(); + + test('it renders the events viewer', async () => { + const wrapper = mount( + + + + + + ); + + await wait(); + wrapper.update(); + + expect( + wrapper + .find('[data-test-subj="events-viewer-panel"]') + .first() + .exists() + ).toBe(true); + }); + + // InspectButtonContainer controls displaying InspectButton components + test('it renders InspectButtonContainer', async () => { + const wrapper = mount( + + + + + + ); + + await wait(); + wrapper.update(); + + expect(wrapper.find(`InspectButtonContainer`).exists()).toBe(true); + }); +}); diff --git a/x-pack/plugins/siem/public/common/components/events_viewer/index.tsx b/x-pack/plugins/siem/public/common/components/events_viewer/index.tsx new file mode 100644 index 00000000000000..e7af69096179af --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/events_viewer/index.tsx @@ -0,0 +1,219 @@ +/* + * 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, { useCallback, useMemo, useEffect } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; + +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { inputsModel, inputsSelectors, State } from '../../store'; +import { inputsActions } from '../../store/actions'; +import { timelineSelectors, timelineActions } from '../../../timelines/store/timeline'; +import { + ColumnHeaderOptions, + SubsetTimelineModel, + TimelineModel, +} from '../../../timelines/store/timeline/model'; +import { OnChangeItemsPerPage } from '../../../timelines/components/timeline/events'; +import { Filter } from '../../../../../../../src/plugins/data/public'; +import { useUiSetting } from '../../lib/kibana'; +import { EventsViewer } from './events_viewer'; +import { useFetchIndexPatterns } from '../../../alerts/containers/detection_engine/rules/fetch_index_patterns'; +import { TimelineTypeContextProps } from '../../../timelines/components/timeline/timeline_context'; +import { InspectButtonContainer } from '../inspect'; +import * as i18n from './translations'; + +export interface OwnProps { + defaultIndices?: string[]; + defaultModel: SubsetTimelineModel; + end: number; + id: string; + start: number; + headerFilterGroup?: React.ReactNode; + pageFilters?: Filter[]; + timelineTypeContext?: TimelineTypeContextProps; + utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode; +} + +type Props = OwnProps & PropsFromRedux; + +const defaultTimelineTypeContext = { + loadingText: i18n.LOADING_EVENTS, +}; + +const StatefulEventsViewerComponent: React.FC = ({ + createTimeline, + columns, + dataProviders, + deletedEventIds, + defaultIndices, + deleteEventQuery, + end, + filters, + headerFilterGroup, + id, + isLive, + itemsPerPage, + itemsPerPageOptions, + kqlMode, + pageFilters, + query, + removeColumn, + start, + showCheckboxes, + showRowRenderers, + sort, + timelineTypeContext = defaultTimelineTypeContext, + updateItemsPerPage, + upsertColumn, + utilityBar, +}) => { + const [{ browserFields, indexPatterns }] = useFetchIndexPatterns( + defaultIndices ?? useUiSetting(DEFAULT_INDEX_KEY) + ); + + useEffect(() => { + if (createTimeline != null) { + createTimeline({ id, columns, sort, itemsPerPage, showCheckboxes, showRowRenderers }); + } + return () => { + deleteEventQuery({ id, inputId: 'global' }); + }; + }, []); + + const onChangeItemsPerPage: OnChangeItemsPerPage = useCallback( + itemsChangedPerPage => updateItemsPerPage({ id, itemsPerPage: itemsChangedPerPage }), + [id, updateItemsPerPage] + ); + + const toggleColumn = useCallback( + (column: ColumnHeaderOptions) => { + const exists = columns.findIndex(c => c.id === column.id) !== -1; + + if (!exists && upsertColumn != null) { + upsertColumn({ + column, + id, + index: 1, + }); + } + + if (exists && removeColumn != null) { + removeColumn({ + columnId: column.id, + id, + }); + } + }, + [columns, id, upsertColumn, removeColumn] + ); + + const globalFilters = useMemo(() => [...filters, ...(pageFilters ?? [])], [filters, pageFilters]); + + return ( + + + + ); +}; + +const makeMapStateToProps = () => { + const getInputsTimeline = inputsSelectors.getTimelineSelector(); + const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); + const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); + const getEvents = timelineSelectors.getEventsByIdSelector(); + const mapStateToProps = (state: State, { id, defaultModel }: OwnProps) => { + const input: inputsModel.InputsRange = getInputsTimeline(state); + const events: TimelineModel = getEvents(state, id) ?? defaultModel; + const { + columns, + dataProviders, + deletedEventIds, + itemsPerPage, + itemsPerPageOptions, + kqlMode, + sort, + showCheckboxes, + showRowRenderers, + } = events; + + return { + columns, + dataProviders, + deletedEventIds, + filters: getGlobalFiltersQuerySelector(state), + id, + isLive: input.policy.kind === 'interval', + itemsPerPage, + itemsPerPageOptions, + kqlMode, + query: getGlobalQuerySelector(state), + sort, + showCheckboxes, + showRowRenderers, + }; + }; + return mapStateToProps; +}; + +const mapDispatchToProps = { + createTimeline: timelineActions.createTimeline, + deleteEventQuery: inputsActions.deleteOneQuery, + updateItemsPerPage: timelineActions.updateItemsPerPage, + removeColumn: timelineActions.removeColumn, + upsertColumn: timelineActions.upsertColumn, +}; + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const StatefulEventsViewer = connector( + React.memo( + StatefulEventsViewerComponent, + (prevProps, nextProps) => + prevProps.id === nextProps.id && + deepEqual(prevProps.columns, nextProps.columns) && + deepEqual(prevProps.dataProviders, nextProps.dataProviders) && + prevProps.deletedEventIds === nextProps.deletedEventIds && + prevProps.end === nextProps.end && + deepEqual(prevProps.filters, nextProps.filters) && + prevProps.isLive === nextProps.isLive && + prevProps.itemsPerPage === nextProps.itemsPerPage && + deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) && + prevProps.kqlMode === nextProps.kqlMode && + deepEqual(prevProps.query, nextProps.query) && + deepEqual(prevProps.sort, nextProps.sort) && + prevProps.start === nextProps.start && + deepEqual(prevProps.pageFilters, nextProps.pageFilters) && + prevProps.showCheckboxes === nextProps.showCheckboxes && + prevProps.showRowRenderers === nextProps.showRowRenderers && + prevProps.start === nextProps.start && + deepEqual(prevProps.timelineTypeContext, nextProps.timelineTypeContext) && + prevProps.utilityBar === nextProps.utilityBar + ) +); diff --git a/x-pack/plugins/siem/public/common/components/events_viewer/mock.ts b/x-pack/plugins/siem/public/common/components/events_viewer/mock.ts new file mode 100644 index 00000000000000..bf95a58aec9818 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/events_viewer/mock.ts @@ -0,0 +1,59 @@ +/* + * 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 { noop } from 'lodash/fp'; +import { timelineQuery } from '../../../timelines/containers/index.gql_query'; + +export const mockEventViewerResponse = [ + { + request: { + query: timelineQuery, + fetchPolicy: 'network-only', + notifyOnNetworkStatusChange: true, + variables: { + fieldRequested: [ + '@timestamp', + 'message', + 'host.name', + 'event.module', + 'event.dataset', + 'event.action', + 'user.name', + 'source.ip', + 'destination.ip', + ], + filterQuery: + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1566943856794}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1566857456791}}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}', + sourceId: 'default', + pagination: { limit: 25, cursor: null, tiebreaker: null }, + sortField: { sortFieldId: '@timestamp', direction: 'desc' }, + defaultIndex: ['filebeat-*', 'auditbeat-*', 'packetbeat-*'], + inspect: false, + }, + }, + result: { + loading: false, + fetchMore: noop, + refetch: noop, + data: { + source: { + id: 'default', + Timeline: { + totalCount: 12, + pageInfo: { + endCursor: null, + hasNextPage: true, + __typename: 'PageInfo', + }, + edges: [], + __typename: 'TimelineData', + }, + __typename: 'Source', + }, + }, + }, + }, +]; diff --git a/x-pack/plugins/siem/public/components/events_viewer/translations.ts b/x-pack/plugins/siem/public/common/components/events_viewer/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/events_viewer/translations.ts rename to x-pack/plugins/siem/public/common/components/events_viewer/translations.ts diff --git a/x-pack/plugins/siem/public/components/external_link_icon/index.test.tsx b/x-pack/plugins/siem/public/common/components/external_link_icon/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/external_link_icon/index.test.tsx rename to x-pack/plugins/siem/public/common/components/external_link_icon/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/external_link_icon/index.tsx b/x-pack/plugins/siem/public/common/components/external_link_icon/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/external_link_icon/index.tsx rename to x-pack/plugins/siem/public/common/components/external_link_icon/index.tsx diff --git a/x-pack/plugins/siem/public/components/filters_global/__snapshots__/filters_global.test.tsx.snap b/x-pack/plugins/siem/public/common/components/filters_global/__snapshots__/filters_global.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/filters_global/__snapshots__/filters_global.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/filters_global/__snapshots__/filters_global.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/filters_global/filters_global.test.tsx b/x-pack/plugins/siem/public/common/components/filters_global/filters_global.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/filters_global/filters_global.test.tsx rename to x-pack/plugins/siem/public/common/components/filters_global/filters_global.test.tsx diff --git a/x-pack/plugins/siem/public/components/filters_global/filters_global.tsx b/x-pack/plugins/siem/public/common/components/filters_global/filters_global.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/filters_global/filters_global.tsx rename to x-pack/plugins/siem/public/common/components/filters_global/filters_global.tsx diff --git a/x-pack/plugins/siem/public/components/filters_global/index.tsx b/x-pack/plugins/siem/public/common/components/filters_global/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/filters_global/index.tsx rename to x-pack/plugins/siem/public/common/components/filters_global/index.tsx diff --git a/x-pack/plugins/siem/public/components/formatted_bytes/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/common/components/formatted_bytes/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/formatted_bytes/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/formatted_bytes/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/formatted_bytes/index.test.tsx b/x-pack/plugins/siem/public/common/components/formatted_bytes/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/formatted_bytes/index.test.tsx rename to x-pack/plugins/siem/public/common/components/formatted_bytes/index.test.tsx diff --git a/x-pack/plugins/siem/public/common/components/formatted_bytes/index.tsx b/x-pack/plugins/siem/public/common/components/formatted_bytes/index.tsx new file mode 100644 index 00000000000000..5664af2aa3f5b4 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/formatted_bytes/index.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import numeral from '@elastic/numeral'; + +import { DEFAULT_BYTES_FORMAT } from '../../../../common/constants'; +import { useUiSetting$ } from '../../lib/kibana'; + +type Bytes = string | number; + +export const formatBytes = (value: Bytes, format: string) => { + return numeral(value).format(format); +}; + +export const useFormatBytes = () => { + const [bytesFormat] = useUiSetting$(DEFAULT_BYTES_FORMAT); + + return (value: Bytes) => formatBytes(value, bytesFormat); +}; + +export const PreferenceFormattedBytesComponent = ({ value }: { value: Bytes }) => ( + <>{useFormatBytes()(value)} +); + +PreferenceFormattedBytesComponent.displayName = 'PreferenceFormattedBytesComponent'; + +export const PreferenceFormattedBytes = React.memo(PreferenceFormattedBytesComponent); + +PreferenceFormattedBytes.displayName = 'PreferenceFormattedBytes'; diff --git a/x-pack/plugins/siem/public/components/formatted_date/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/common/components/formatted_date/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/formatted_date/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/formatted_date/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/formatted_date/index.test.tsx b/x-pack/plugins/siem/public/common/components/formatted_date/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/formatted_date/index.test.tsx rename to x-pack/plugins/siem/public/common/components/formatted_date/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/formatted_date/index.tsx b/x-pack/plugins/siem/public/common/components/formatted_date/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/formatted_date/index.tsx rename to x-pack/plugins/siem/public/common/components/formatted_date/index.tsx diff --git a/x-pack/plugins/siem/public/components/formatted_date/maybe_date.test.ts b/x-pack/plugins/siem/public/common/components/formatted_date/maybe_date.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/formatted_date/maybe_date.test.ts rename to x-pack/plugins/siem/public/common/components/formatted_date/maybe_date.test.ts diff --git a/x-pack/plugins/siem/public/components/formatted_date/maybe_date.ts b/x-pack/plugins/siem/public/common/components/formatted_date/maybe_date.ts similarity index 100% rename from x-pack/plugins/siem/public/components/formatted_date/maybe_date.ts rename to x-pack/plugins/siem/public/common/components/formatted_date/maybe_date.ts diff --git a/x-pack/plugins/siem/public/components/generic_downloader/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/common/components/generic_downloader/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/generic_downloader/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/generic_downloader/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/generic_downloader/index.test.tsx b/x-pack/plugins/siem/public/common/components/generic_downloader/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/generic_downloader/index.test.tsx rename to x-pack/plugins/siem/public/common/components/generic_downloader/index.test.tsx diff --git a/x-pack/plugins/siem/public/common/components/generic_downloader/index.tsx b/x-pack/plugins/siem/public/common/components/generic_downloader/index.tsx new file mode 100644 index 00000000000000..2f68da0c18727b --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/generic_downloader/index.tsx @@ -0,0 +1,107 @@ +/* + * 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, useRef } from 'react'; +import styled from 'styled-components'; +import { isFunction } from 'lodash/fp'; +import * as i18n from './translations'; + +import { ExportDocumentsProps } from '../../../alerts/containers/detection_engine/rules'; +import { useStateToaster, errorToToaster } from '../toasters'; + +const InvisibleAnchor = styled.a` + display: none; +`; + +export type ExportSelectedData = ({ + excludeExportDetails, + filename, + ids, + signal, +}: ExportDocumentsProps) => Promise; + +export interface GenericDownloaderProps { + filename: string; + ids?: string[]; + exportSelectedData: ExportSelectedData; + onExportSuccess?: (exportCount: number) => void; + onExportFailure?: () => void; +} + +/** + * Component for downloading Rules as an exported .ndjson file. Download will occur on each update to `rules` param + * + * @param filename of file to be downloaded + * @param payload Rule[] + * + */ + +export const GenericDownloaderComponent = ({ + exportSelectedData, + filename, + ids, + onExportSuccess, + onExportFailure, +}: GenericDownloaderProps) => { + const anchorRef = useRef(null); + const [, dispatchToaster] = useStateToaster(); + + useEffect(() => { + let isSubscribed = true; + const abortCtrl = new AbortController(); + + const exportData = async () => { + if (anchorRef && anchorRef.current && ids != null && ids.length > 0) { + try { + const exportResponse = await exportSelectedData({ + ids, + signal: abortCtrl.signal, + }); + + if (isSubscribed) { + // this is for supporting IE + if (isFunction(window.navigator.msSaveOrOpenBlob)) { + window.navigator.msSaveBlob(exportResponse); + } else { + const objectURL = window.URL.createObjectURL(exportResponse); + // These are safe-assignments as writes to anchorRef are isolated to exportData + anchorRef.current.href = objectURL; // eslint-disable-line require-atomic-updates + anchorRef.current.download = filename; // eslint-disable-line require-atomic-updates + anchorRef.current.click(); + window.URL.revokeObjectURL(objectURL); + } + + if (onExportSuccess != null) { + onExportSuccess(ids.length); + } + } + } catch (error) { + if (isSubscribed) { + if (onExportFailure != null) { + onExportFailure(); + } + errorToToaster({ title: i18n.EXPORT_FAILURE, error, dispatchToaster }); + } + } + } + }; + + exportData(); + + return () => { + isSubscribed = false; + abortCtrl.abort(); + }; + }, [ids]); + + return ; +}; + +GenericDownloaderComponent.displayName = 'GenericDownloaderComponent'; + +export const GenericDownloader = React.memo(GenericDownloaderComponent); + +GenericDownloader.displayName = 'GenericDownloader'; diff --git a/x-pack/plugins/siem/public/components/generic_downloader/translations.ts b/x-pack/plugins/siem/public/common/components/generic_downloader/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/generic_downloader/translations.ts rename to x-pack/plugins/siem/public/common/components/generic_downloader/translations.ts diff --git a/x-pack/plugins/siem/public/components/header_global/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/common/components/header_global/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/header_global/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/header_global/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/header_global/index.test.tsx b/x-pack/plugins/siem/public/common/components/header_global/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/header_global/index.test.tsx rename to x-pack/plugins/siem/public/common/components/header_global/index.test.tsx diff --git a/x-pack/plugins/siem/public/common/components/header_global/index.tsx b/x-pack/plugins/siem/public/common/components/header_global/index.tsx new file mode 100644 index 00000000000000..bc4bb80d8874de --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/header_global/index.tsx @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink } from '@elastic/eui'; +import { pickBy } from 'lodash/fp'; +import React from 'react'; +import styled, { css } from 'styled-components'; + +import { useLocation } from 'react-router-dom'; +import { gutterTimeline } from '../../lib/helpers'; +import { navTabs } from '../../../app/home/home_navigations'; +import { SiemPageName } from '../../../app/types'; +import { getOverviewUrl } from '../link_to'; +import { MlPopover } from '../ml_popover/ml_popover'; +import { SiemNavigation } from '../navigation'; +import * as i18n from './translations'; +import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source'; + +const Wrapper = styled.header` + ${({ theme }) => css` + background: ${theme.eui.euiColorEmptyShade}; + border-bottom: ${theme.eui.euiBorderThin}; + padding: ${theme.eui.paddingSizes.m} ${gutterTimeline} ${theme.eui.paddingSizes.m} + ${theme.eui.paddingSizes.l}; + `} +`; +Wrapper.displayName = 'Wrapper'; + +const FlexItem = styled(EuiFlexItem)` + min-width: 0; +`; +FlexItem.displayName = 'FlexItem'; + +interface HeaderGlobalProps { + hideDetectionEngine?: boolean; +} +export const HeaderGlobal = React.memo(({ hideDetectionEngine = false }) => { + const currentLocation = useLocation(); + + return ( + + + + {({ indicesExist }) => ( + <> + + + + + + + + + + {indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( + key !== SiemPageName.detections, navTabs) + : navTabs + } + /> + ) : ( + key === SiemPageName.overview, navTabs)} + /> + )} + + + + + + + {indicesExistOrDataTemporarilyUnavailable(indicesExist) && + currentLocation.pathname.includes(`/${SiemPageName.detections}/`) && ( + + + + )} + + + + {i18n.BUTTON_ADD_DATA} + + + + + + )} + + + + ); +}); +HeaderGlobal.displayName = 'HeaderGlobal'; diff --git a/x-pack/plugins/siem/public/components/header_global/translations.ts b/x-pack/plugins/siem/public/common/components/header_global/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/header_global/translations.ts rename to x-pack/plugins/siem/public/common/components/header_global/translations.ts diff --git a/x-pack/plugins/siem/public/components/header_page/__snapshots__/editable_title.test.tsx.snap b/x-pack/plugins/siem/public/common/components/header_page/__snapshots__/editable_title.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/header_page/__snapshots__/editable_title.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/header_page/__snapshots__/editable_title.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/header_page/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/common/components/header_page/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/header_page/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/header_page/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/header_page/__snapshots__/title.test.tsx.snap b/x-pack/plugins/siem/public/common/components/header_page/__snapshots__/title.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/header_page/__snapshots__/title.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/header_page/__snapshots__/title.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/header_page/editable_title.test.tsx b/x-pack/plugins/siem/public/common/components/header_page/editable_title.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/header_page/editable_title.test.tsx rename to x-pack/plugins/siem/public/common/components/header_page/editable_title.test.tsx diff --git a/x-pack/plugins/siem/public/components/header_page/editable_title.tsx b/x-pack/plugins/siem/public/common/components/header_page/editable_title.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/header_page/editable_title.tsx rename to x-pack/plugins/siem/public/common/components/header_page/editable_title.tsx diff --git a/x-pack/plugins/siem/public/components/header_page/index.test.tsx b/x-pack/plugins/siem/public/common/components/header_page/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/header_page/index.test.tsx rename to x-pack/plugins/siem/public/common/components/header_page/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/header_page/index.tsx b/x-pack/plugins/siem/public/common/components/header_page/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/header_page/index.tsx rename to x-pack/plugins/siem/public/common/components/header_page/index.tsx diff --git a/x-pack/plugins/siem/public/components/header_page/title.test.tsx b/x-pack/plugins/siem/public/common/components/header_page/title.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/header_page/title.test.tsx rename to x-pack/plugins/siem/public/common/components/header_page/title.test.tsx diff --git a/x-pack/plugins/siem/public/components/header_page/title.tsx b/x-pack/plugins/siem/public/common/components/header_page/title.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/header_page/title.tsx rename to x-pack/plugins/siem/public/common/components/header_page/title.tsx diff --git a/x-pack/plugins/siem/public/components/header_page/translations.ts b/x-pack/plugins/siem/public/common/components/header_page/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/header_page/translations.ts rename to x-pack/plugins/siem/public/common/components/header_page/translations.ts diff --git a/x-pack/plugins/siem/public/components/header_page/types.ts b/x-pack/plugins/siem/public/common/components/header_page/types.ts similarity index 100% rename from x-pack/plugins/siem/public/components/header_page/types.ts rename to x-pack/plugins/siem/public/common/components/header_page/types.ts diff --git a/x-pack/plugins/siem/public/components/header_section/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/common/components/header_section/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/header_section/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/header_section/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/header_section/index.test.tsx b/x-pack/plugins/siem/public/common/components/header_section/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/header_section/index.test.tsx rename to x-pack/plugins/siem/public/common/components/header_section/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/header_section/index.tsx b/x-pack/plugins/siem/public/common/components/header_section/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/header_section/index.tsx rename to x-pack/plugins/siem/public/common/components/header_section/index.tsx diff --git a/x-pack/plugins/siem/public/components/help_menu/index.tsx b/x-pack/plugins/siem/public/common/components/help_menu/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/help_menu/index.tsx rename to x-pack/plugins/siem/public/common/components/help_menu/index.tsx diff --git a/x-pack/plugins/siem/public/components/import_data_modal/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/common/components/import_data_modal/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/import_data_modal/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/import_data_modal/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/import_data_modal/index.test.tsx b/x-pack/plugins/siem/public/common/components/import_data_modal/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/import_data_modal/index.test.tsx rename to x-pack/plugins/siem/public/common/components/import_data_modal/index.test.tsx diff --git a/x-pack/plugins/siem/public/common/components/import_data_modal/index.tsx b/x-pack/plugins/siem/public/common/components/import_data_modal/index.tsx new file mode 100644 index 00000000000000..45368d1fefc539 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/import_data_modal/index.tsx @@ -0,0 +1,176 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiButton, + EuiButtonEmpty, + EuiCheckbox, + // @ts-ignore no-exported-member + EuiFilePicker, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiOverlayMask, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import React, { useCallback, useState } from 'react'; + +import { + ImportDataResponse, + ImportDataProps, +} from '../../../alerts/containers/detection_engine/rules'; +import { + displayErrorToast, + displaySuccessToast, + useStateToaster, + errorToToaster, +} from '../toasters'; +import * as i18n from './translations'; + +interface ImportDataModalProps { + checkBoxLabel: string; + closeModal: () => void; + description: string; + errorMessage: string; + failedDetailed: (id: string, statusCode: number, message: string) => string; + importComplete: () => void; + importData: (arg: ImportDataProps) => Promise; + showCheckBox: boolean; + showModal: boolean; + submitBtnText: string; + subtitle: string; + successMessage: (totalCount: number) => string; + title: string; +} + +/** + * Modal component for importing Rules from a json file + */ +export const ImportDataModalComponent = ({ + checkBoxLabel, + closeModal, + description, + errorMessage, + failedDetailed, + importComplete, + importData, + showCheckBox = true, + showModal, + submitBtnText, + subtitle, + successMessage, + title, +}: ImportDataModalProps) => { + const [selectedFiles, setSelectedFiles] = useState(null); + const [isImporting, setIsImporting] = useState(false); + const [overwrite, setOverwrite] = useState(false); + const [, dispatchToaster] = useStateToaster(); + + const cleanupAndCloseModal = useCallback(() => { + setIsImporting(false); + setSelectedFiles(null); + closeModal(); + }, [setIsImporting, setSelectedFiles, closeModal]); + + const importDataCallback = useCallback(async () => { + if (selectedFiles != null) { + setIsImporting(true); + const abortCtrl = new AbortController(); + + try { + const importResponse = await importData({ + fileToImport: selectedFiles[0], + overwrite, + signal: abortCtrl.signal, + }); + + // TODO: Improve error toast details for better debugging failed imports + // e.g. When success == true && success_count === 0 that means no rules were overwritten, etc + if (importResponse.success) { + displaySuccessToast(successMessage(importResponse.success_count), dispatchToaster); + } + if (importResponse.errors.length > 0) { + const formattedErrors = importResponse.errors.map(e => + failedDetailed(e.rule_id, e.error.status_code, e.error.message) + ); + displayErrorToast(errorMessage, formattedErrors, dispatchToaster); + } + + importComplete(); + cleanupAndCloseModal(); + } catch (error) { + cleanupAndCloseModal(); + errorToToaster({ title: errorMessage, error, dispatchToaster }); + } + } + }, [selectedFiles, overwrite]); + + const handleCloseModal = useCallback(() => { + setSelectedFiles(null); + closeModal(); + }, [closeModal]); + + return ( + <> + {showModal && ( + + + + {title} + + + + +

{description}

+
+ + + { + setSelectedFiles(files && files.length > 0 ? files : null); + }} + display={'large'} + fullWidth={true} + isLoading={isImporting} + /> + + {showCheckBox && ( + setOverwrite(!overwrite)} + /> + )} +
+ + + {i18n.CANCEL_BUTTON} + + {submitBtnText} + + +
+
+ )} + + ); +}; + +ImportDataModalComponent.displayName = 'ImportDataModalComponent'; + +export const ImportDataModal = React.memo(ImportDataModalComponent); + +ImportDataModal.displayName = 'ImportDataModal'; diff --git a/x-pack/plugins/siem/public/components/import_data_modal/translations.ts b/x-pack/plugins/siem/public/common/components/import_data_modal/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/import_data_modal/translations.ts rename to x-pack/plugins/siem/public/common/components/import_data_modal/translations.ts diff --git a/x-pack/plugins/siem/public/common/components/inspect/index.test.tsx b/x-pack/plugins/siem/public/common/components/inspect/index.test.tsx new file mode 100644 index 00000000000000..a4ef6f8c795709 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/inspect/index.test.tsx @@ -0,0 +1,239 @@ +/* + * 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 euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { mount } from 'enzyme'; +import React from 'react'; +import { ThemeProvider } from 'styled-components'; + +import { + TestProviderWithoutDragAndDrop, + mockGlobalState, + apolloClientObservable, + SUB_PLUGINS_REDUCER, +} from '../../mock'; +import { createStore, State } from '../../store'; +import { UpdateQueryParams, upsertQuery } from '../../store/inputs/helpers'; + +import { InspectButton, InspectButtonContainer, BUTTON_CLASS } from '.'; +import { cloneDeep } from 'lodash/fp'; + +describe('Inspect Button', () => { + const theme = () => ({ eui: euiDarkVars, darkMode: true }); + const refetch = jest.fn(); + const state: State = mockGlobalState; + const newQuery: UpdateQueryParams = { + inputId: 'global', + id: 'myQuery', + inspect: null, + loading: false, + refetch, + state: state.inputs, + }; + + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + + describe('Render', () => { + beforeEach(() => { + const myState = cloneDeep(state); + myState.inputs = upsertQuery(newQuery); + store = createStore(myState, SUB_PLUGINS_REDUCER, apolloClientObservable); + }); + test('Eui Empty Button', () => { + const wrapper = mount( + + + + ); + expect( + wrapper + .find('button[data-test-subj="inspect-empty-button"]') + .first() + .exists() + ).toBe(true); + }); + + test('it does NOT render the Eui Empty Button when timeline is timeline and compact is true', () => { + const wrapper = mount( + + + + ); + expect( + wrapper + .find('button[data-test-subj="inspect-empty-button"]') + .first() + .exists() + ).toBe(false); + }); + + test('Eui Icon Button', () => { + const wrapper = mount( + + + + ); + expect( + wrapper + .find('button[data-test-subj="inspect-icon-button"]') + .first() + .exists() + ).toBe(true); + }); + + test('renders the Icon Button when inputId does NOT equal global, but compact is true', () => { + const wrapper = mount( + + + + ); + expect( + wrapper + .find('button[data-test-subj="inspect-icon-button"]') + .first() + .exists() + ).toBe(true); + }); + + test('Eui Empty Button disabled', () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('.euiButtonIcon').get(0).props.disabled).toBe(true); + }); + + test('Eui Icon Button disabled', () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('.euiButtonIcon').get(0).props.disabled).toBe(true); + }); + + describe('InspectButtonContainer', () => { + test('it renders a transparent inspect button by default', async () => { + const wrapper = mount( + + + + + + ); + + expect(wrapper.find(`InspectButtonContainer`)).toHaveStyleRule('opacity', '0', { + modifier: `.${BUTTON_CLASS}`, + }); + }); + + test('it renders an opaque inspect button when it has mouse focus', async () => { + const wrapper = mount( + + + + + + ); + + expect(wrapper.find(`InspectButtonContainer`)).toHaveStyleRule('opacity', '1', { + modifier: `:hover .${BUTTON_CLASS}`, + }); + }); + }); + }); + + describe('Modal Inspect - happy path', () => { + beforeEach(() => { + const myState = cloneDeep(state); + const myQuery = cloneDeep(newQuery); + myQuery.inspect = { + dsl: ['my dsl'], + response: ['my response'], + }; + myState.inputs = upsertQuery(myQuery); + store = createStore(myState, SUB_PLUGINS_REDUCER, apolloClientObservable); + }); + test('Open Inspect Modal', () => { + const wrapper = mount( + + + + + + ); + wrapper + .find('button[data-test-subj="inspect-icon-button"]') + .first() + .simulate('click'); + + wrapper.update(); + + expect(store.getState().inputs.global.queries[0].isInspected).toBe(true); + expect( + wrapper + .find('button[data-test-subj="modal-inspect-close"]') + .first() + .exists() + ).toBe(true); + }); + + test('Close Inspect Modal', () => { + const wrapper = mount( + + + + + + ); + wrapper + .find('button[data-test-subj="inspect-icon-button"]') + .first() + .simulate('click'); + + wrapper.update(); + + wrapper + .find('button[data-test-subj="modal-inspect-close"]') + .first() + .simulate('click'); + + wrapper.update(); + + expect(store.getState().inputs.global.queries[0].isInspected).toBe(false); + expect( + wrapper + .find('button[data-test-subj="modal-inspect-close"]') + .first() + .exists() + ).toBe(false); + }); + + test('Do not Open Inspect Modal if it is loading', () => { + const wrapper = mount( + + + + ); + store.getState().inputs.global.queries[0].loading = true; + wrapper + .find('button[data-test-subj="inspect-icon-button"]') + .first() + .simulate('click'); + + wrapper.update(); + + expect(store.getState().inputs.global.queries[0].isInspected).toBe(true); + expect( + wrapper + .find('button[data-test-subj="modal-inspect-close"]') + .first() + .exists() + ).toBe(false); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/components/inspect/index.tsx b/x-pack/plugins/siem/public/common/components/inspect/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/inspect/index.tsx rename to x-pack/plugins/siem/public/common/components/inspect/index.tsx diff --git a/x-pack/plugins/siem/public/components/inspect/modal.test.tsx b/x-pack/plugins/siem/public/common/components/inspect/modal.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/inspect/modal.test.tsx rename to x-pack/plugins/siem/public/common/components/inspect/modal.test.tsx diff --git a/x-pack/plugins/siem/public/components/inspect/modal.tsx b/x-pack/plugins/siem/public/common/components/inspect/modal.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/inspect/modal.tsx rename to x-pack/plugins/siem/public/common/components/inspect/modal.tsx diff --git a/x-pack/plugins/siem/public/components/inspect/translations.ts b/x-pack/plugins/siem/public/common/components/inspect/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/inspect/translations.ts rename to x-pack/plugins/siem/public/common/components/inspect/translations.ts diff --git a/x-pack/plugins/siem/public/common/components/last_event_time/index.test.tsx b/x-pack/plugins/siem/public/common/components/last_event_time/index.test.tsx new file mode 100644 index 00000000000000..2f0060c91668bb --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/last_event_time/index.test.tsx @@ -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 React from 'react'; + +import { getEmptyValue } from '../empty_value'; +import { LastEventIndexKey } from '../../../graphql/types'; +import { mockLastEventTimeQuery } from '../../containers/events/last_event_time/mock'; + +import { useMountAppended } from '../../utils/use_mount_appended'; +import { useLastEventTimeQuery } from '../../containers/events/last_event_time'; +import { TestProviders } from '../../mock'; + +import { LastEventTime } from '.'; + +const mockUseLastEventTimeQuery: jest.Mock = useLastEventTimeQuery as jest.Mock; +jest.mock('../../containers/events/last_event_time', () => ({ + useLastEventTimeQuery: jest.fn(), +})); + +describe('Last Event Time Stat', () => { + const mount = useMountAppended(); + + beforeEach(() => { + mockUseLastEventTimeQuery.mockReset(); + }); + + test('Loading', async () => { + mockUseLastEventTimeQuery.mockImplementation(() => ({ + loading: true, + lastSeen: null, + errorMessage: null, + })); + const wrapper = mount( + + + + ); + expect(wrapper.html()).toBe( + '' + ); + }); + test('Last seen', async () => { + mockUseLastEventTimeQuery.mockImplementation(() => ({ + loading: false, + lastSeen: mockLastEventTimeQuery[0].result.data!.source.LastEventTime.lastSeen, + errorMessage: mockLastEventTimeQuery[0].result.data!.source.LastEventTime.errorMessage, + })); + const wrapper = mount( + + + + ); + expect(wrapper.html()).toBe('Last event: 12 minutes ago'); + }); + test('Bad date time string', async () => { + mockUseLastEventTimeQuery.mockImplementation(() => ({ + loading: false, + lastSeen: 'something-invalid', + errorMessage: mockLastEventTimeQuery[0].result.data!.source.LastEventTime.errorMessage, + })); + const wrapper = mount( + + + + ); + + expect(wrapper.html()).toBe('something-invalid'); + }); + test('Null time string', async () => { + mockUseLastEventTimeQuery.mockImplementation(() => ({ + loading: false, + lastSeen: null, + errorMessage: mockLastEventTimeQuery[0].result.data!.source.LastEventTime.errorMessage, + })); + const wrapper = mount( + + + + ); + expect(wrapper.html()).toContain(getEmptyValue()); + }); +}); diff --git a/x-pack/plugins/siem/public/common/components/last_event_time/index.tsx b/x-pack/plugins/siem/public/common/components/last_event_time/index.tsx new file mode 100644 index 00000000000000..1c988ed989e86a --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/last_event_time/index.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiIcon, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { memo } from 'react'; + +import { LastEventIndexKey } from '../../../graphql/types'; +import { useLastEventTimeQuery } from '../../containers/events/last_event_time'; +import { getEmptyTagValue } from '../empty_value'; +import { FormattedRelativePreferenceDate } from '../formatted_date'; + +export interface LastEventTimeProps { + hostName?: string; + indexKey: LastEventIndexKey; + ip?: string; +} + +export const LastEventTime = memo(({ hostName, indexKey, ip }) => { + const { loading, lastSeen, errorMessage } = useLastEventTimeQuery( + indexKey, + { hostName, ip }, + 'default' + ); + + if (errorMessage != null) { + return ( + + + + ); + } + + return ( + <> + {loading && } + {!loading && lastSeen != null && new Date(lastSeen).toString() === 'Invalid Date' + ? lastSeen + : !loading && + lastSeen != null && ( + , + }} + /> + )} + {!loading && lastSeen == null && getEmptyTagValue()} + + ); +}); + +LastEventTime.displayName = 'LastEventTime'; diff --git a/x-pack/plugins/siem/public/components/link_icon/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/common/components/link_icon/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/link_icon/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/link_icon/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/link_icon/index.test.tsx b/x-pack/plugins/siem/public/common/components/link_icon/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/link_icon/index.test.tsx rename to x-pack/plugins/siem/public/common/components/link_icon/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/link_icon/index.tsx b/x-pack/plugins/siem/public/common/components/link_icon/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/link_icon/index.tsx rename to x-pack/plugins/siem/public/common/components/link_icon/index.tsx diff --git a/x-pack/plugins/siem/public/components/link_to/helpers.test.ts b/x-pack/plugins/siem/public/common/components/link_to/helpers.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/link_to/helpers.test.ts rename to x-pack/plugins/siem/public/common/components/link_to/helpers.test.ts diff --git a/x-pack/plugins/siem/public/components/link_to/helpers.ts b/x-pack/plugins/siem/public/common/components/link_to/helpers.ts similarity index 100% rename from x-pack/plugins/siem/public/components/link_to/helpers.ts rename to x-pack/plugins/siem/public/common/components/link_to/helpers.ts diff --git a/x-pack/plugins/siem/public/components/link_to/index.ts b/x-pack/plugins/siem/public/common/components/link_to/index.ts similarity index 100% rename from x-pack/plugins/siem/public/components/link_to/index.ts rename to x-pack/plugins/siem/public/common/components/link_to/index.ts diff --git a/x-pack/plugins/siem/public/components/link_to/link_to.tsx b/x-pack/plugins/siem/public/common/components/link_to/link_to.tsx similarity index 94% rename from x-pack/plugins/siem/public/components/link_to/link_to.tsx rename to x-pack/plugins/siem/public/common/components/link_to/link_to.tsx index d3bf2e34b435b7..77636af8bc4a49 100644 --- a/x-pack/plugins/siem/public/components/link_to/link_to.tsx +++ b/x-pack/plugins/siem/public/common/components/link_to/link_to.tsx @@ -7,8 +7,8 @@ import React from 'react'; import { match as RouteMatch, Redirect, Route, Switch } from 'react-router-dom'; -import { SiemPageName } from '../../pages/home/types'; -import { HostsTableType } from '../../store/hosts/model'; +import { SiemPageName } from '../../../app/types'; +import { HostsTableType } from '../../../hosts/store/model'; import { RedirectToCreateRulePage, RedirectToDetectionEnginePage, @@ -25,8 +25,8 @@ import { RedirectToCreatePage, RedirectToConfigureCasesPage, } from './redirect_to_case'; -import { DetectionEngineTab } from '../../pages/detection_engine/types'; -import { TimelineType } from '../../../common/types/timeline'; +import { DetectionEngineTab } from '../../../alerts/pages/detection_engine/types'; +import { TimelineType } from '../../../../common/types/timeline'; interface LinkToPageProps { match: RouteMatch<{}>; diff --git a/x-pack/plugins/siem/public/components/link_to/redirect_to_case.tsx b/x-pack/plugins/siem/public/common/components/link_to/redirect_to_case.tsx similarity index 96% rename from x-pack/plugins/siem/public/components/link_to/redirect_to_case.tsx rename to x-pack/plugins/siem/public/common/components/link_to/redirect_to_case.tsx index 6ec15b55ba83dc..e0c03519c6cbea 100644 --- a/x-pack/plugins/siem/public/components/link_to/redirect_to_case.tsx +++ b/x-pack/plugins/siem/public/common/components/link_to/redirect_to_case.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { appendSearch } from './helpers'; import { RedirectWrapper } from './redirect_wrapper'; -import { SiemPageName } from '../../pages/home/types'; +import { SiemPageName } from '../../../app/types'; export type CaseComponentProps = RouteComponentProps<{ detailName: string; diff --git a/x-pack/plugins/siem/public/components/link_to/redirect_to_detection_engine.tsx b/x-pack/plugins/siem/public/common/components/link_to/redirect_to_detection_engine.tsx similarity index 97% rename from x-pack/plugins/siem/public/components/link_to/redirect_to_detection_engine.tsx rename to x-pack/plugins/siem/public/common/components/link_to/redirect_to_detection_engine.tsx index 18111aa93a27a8..fc5aef966f228c 100644 --- a/x-pack/plugins/siem/public/components/link_to/redirect_to_detection_engine.tsx +++ b/x-pack/plugins/siem/public/common/components/link_to/redirect_to_detection_engine.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { DetectionEngineTab } from '../../pages/detection_engine/types'; +import { DetectionEngineTab } from '../../../alerts/pages/detection_engine/types'; import { appendSearch } from './helpers'; import { RedirectWrapper } from './redirect_wrapper'; diff --git a/x-pack/plugins/siem/public/components/link_to/redirect_to_hosts.tsx b/x-pack/plugins/siem/public/common/components/link_to/redirect_to_hosts.tsx similarity index 94% rename from x-pack/plugins/siem/public/components/link_to/redirect_to_hosts.tsx rename to x-pack/plugins/siem/public/common/components/link_to/redirect_to_hosts.tsx index 746a959cc996a1..0cfe8e655e255d 100644 --- a/x-pack/plugins/siem/public/components/link_to/redirect_to_hosts.tsx +++ b/x-pack/plugins/siem/public/common/components/link_to/redirect_to_hosts.tsx @@ -7,8 +7,8 @@ import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { HostsTableType } from '../../store/hosts/model'; -import { SiemPageName } from '../../pages/home/types'; +import { HostsTableType } from '../../../hosts/store/model'; +import { SiemPageName } from '../../../app/types'; import { appendSearch } from './helpers'; import { RedirectWrapper } from './redirect_wrapper'; diff --git a/x-pack/plugins/siem/public/components/link_to/redirect_to_network.tsx b/x-pack/plugins/siem/public/common/components/link_to/redirect_to_network.tsx similarity index 90% rename from x-pack/plugins/siem/public/components/link_to/redirect_to_network.tsx rename to x-pack/plugins/siem/public/common/components/link_to/redirect_to_network.tsx index 71925edd5c0864..d72bacf511faa9 100644 --- a/x-pack/plugins/siem/public/components/link_to/redirect_to_network.tsx +++ b/x-pack/plugins/siem/public/common/components/link_to/redirect_to_network.tsx @@ -7,8 +7,8 @@ import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { SiemPageName } from '../../pages/home/types'; -import { FlowTarget, FlowTargetSourceDest } from '../../graphql/types'; +import { SiemPageName } from '../../../app/types'; +import { FlowTarget, FlowTargetSourceDest } from '../../../graphql/types'; import { appendSearch } from './helpers'; import { RedirectWrapper } from './redirect_wrapper'; diff --git a/x-pack/plugins/siem/public/components/link_to/redirect_to_overview.tsx b/x-pack/plugins/siem/public/common/components/link_to/redirect_to_overview.tsx similarity index 92% rename from x-pack/plugins/siem/public/components/link_to/redirect_to_overview.tsx rename to x-pack/plugins/siem/public/common/components/link_to/redirect_to_overview.tsx index e0789ac9e2558b..2043b820e6966c 100644 --- a/x-pack/plugins/siem/public/components/link_to/redirect_to_overview.tsx +++ b/x-pack/plugins/siem/public/common/components/link_to/redirect_to_overview.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; import { RedirectWrapper } from './redirect_wrapper'; -import { SiemPageName } from '../../pages/home/types'; +import { SiemPageName } from '../../../app/types'; export type OverviewComponentProps = RouteComponentProps<{ search: string; diff --git a/x-pack/plugins/siem/public/components/link_to/redirect_to_timelines.tsx b/x-pack/plugins/siem/public/common/components/link_to/redirect_to_timelines.tsx similarity index 89% rename from x-pack/plugins/siem/public/components/link_to/redirect_to_timelines.tsx rename to x-pack/plugins/siem/public/common/components/link_to/redirect_to_timelines.tsx index 9c704a7f70d293..3562153bea646e 100644 --- a/x-pack/plugins/siem/public/components/link_to/redirect_to_timelines.tsx +++ b/x-pack/plugins/siem/public/common/components/link_to/redirect_to_timelines.tsx @@ -7,11 +7,11 @@ import React from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { SiemPageName } from '../../pages/home/types'; +import { SiemPageName } from '../../../app/types'; import { appendSearch } from './helpers'; import { RedirectWrapper } from './redirect_wrapper'; -import { TimelineTypeLiteral, TimelineType } from '../../../common/types/timeline'; +import { TimelineTypeLiteral, TimelineType } from '../../../../common/types/timeline'; export type TimelineComponentProps = RouteComponentProps<{ tabName: TimelineTypeLiteral; diff --git a/x-pack/plugins/siem/public/components/link_to/redirect_wrapper.tsx b/x-pack/plugins/siem/public/common/components/link_to/redirect_wrapper.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/link_to/redirect_wrapper.tsx rename to x-pack/plugins/siem/public/common/components/link_to/redirect_wrapper.tsx diff --git a/x-pack/plugins/siem/public/common/components/links/index.test.tsx b/x-pack/plugins/siem/public/common/components/links/index.test.tsx new file mode 100644 index 00000000000000..9eff86bffb369a --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/links/index.test.tsx @@ -0,0 +1,563 @@ +/* + * 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 { mount, shallow, ShallowWrapper } from 'enzyme'; +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; + +import { encodeIpv6 } from '../../lib/helpers'; +import { useUiSetting$ } from '../../lib/kibana'; + +import { + GoogleLink, + HostDetailsLink, + IPDetailsLink, + ReputationLink, + WhoIsLink, + CertificateFingerprintLink, + Ja3FingerprintLink, + PortOrServiceNameLink, + DEFAULT_NUMBER_OF_LINK, + ExternalLink, +} from '.'; + +jest.mock('../../../overview/components/events_by_dataset'); + +jest.mock('../../lib/kibana', () => { + return { + useUiSetting$: jest.fn(), + }; +}); + +describe('Custom Links', () => { + const hostName = 'Host Name'; + const ipv4 = '192.0.2.255'; + const ipv6 = '2001:db8:ffff:ffff:ffff:ffff:ffff:ffff'; + const ipv6Encoded = encodeIpv6(ipv6); + + describe('HostDetailsLink', () => { + test('should render valid link to Host Details with hostName as the display text', () => { + const wrapper = mount(); + expect(wrapper.find('EuiLink').prop('href')).toEqual( + `#/link-to/hosts/${encodeURIComponent(hostName)}` + ); + expect(wrapper.text()).toEqual(hostName); + }); + + test('should render valid link to Host Details with child text as the display text', () => { + const wrapper = mount({hostName}); + expect(wrapper.find('EuiLink').prop('href')).toEqual( + `#/link-to/hosts/${encodeURIComponent(hostName)}` + ); + expect(wrapper.text()).toEqual(hostName); + }); + }); + + describe('IPDetailsLink', () => { + test('should render valid link to IP Details with ipv4 as the display text', () => { + const wrapper = mount(); + expect(wrapper.find('EuiLink').prop('href')).toEqual( + `#/link-to/network/ip/${encodeURIComponent(ipv4)}/source` + ); + expect(wrapper.text()).toEqual(ipv4); + }); + + test('should render valid link to IP Details with child text as the display text', () => { + const wrapper = mount({hostName}); + expect(wrapper.find('EuiLink').prop('href')).toEqual( + `#/link-to/network/ip/${encodeURIComponent(ipv4)}/source` + ); + expect(wrapper.text()).toEqual(hostName); + }); + + test('should render valid link to IP Details with ipv6 as the display text', () => { + const wrapper = mount(); + expect(wrapper.find('EuiLink').prop('href')).toEqual( + `#/link-to/network/ip/${encodeURIComponent(ipv6Encoded)}/source` + ); + expect(wrapper.text()).toEqual(ipv6); + }); + }); + + describe('GoogleLink', () => { + test('it renders text passed in as value', () => { + const wrapper = mountWithIntl( + {'Example Link'} + ); + expect(wrapper.text()).toEqual('Example Link'); + }); + + test('it renders props passed in as link', () => { + const wrapper = mountWithIntl( + {'Example Link'} + ); + expect(wrapper.find('a').prop('href')).toEqual( + 'https://www.google.com/search?q=http%3A%2F%2Fexample.com%2F' + ); + }); + + test("it encodes ", () => { + const wrapper = mountWithIntl( + alert('XSS')"}> + {'Example Link'} + + ); + expect(wrapper.find('a').prop('href')).toEqual( + "https://www.google.com/search?q=http%3A%2F%2Fexample.com%3Fq%3D%3Cscript%3Ealert('XSS')%3C%2Fscript%3E" + ); + }); + }); + + describe('External Link', () => { + const mockLink = 'https://www.virustotal.com/gui/search/'; + const mockLinkName = 'Link'; + let wrapper: ShallowWrapper; + + describe('render', () => { + beforeAll(() => { + wrapper = shallow( + + {mockLinkName} + + ); + }); + + test('it renders tooltip', () => { + expect(wrapper.find('[data-test-subj="externalLinkTooltip"]').exists()).toBeTruthy(); + }); + + test('it renders ExternalLinkIcon', () => { + expect(wrapper.find('[data-test-subj="externalLinkIcon"]').exists()).toBeTruthy(); + }); + + test('it renders correct url', () => { + expect(wrapper.find('[data-test-subj="externalLink"]').prop('href')).toEqual(mockLink); + }); + + test('it renders comma if id is given', () => { + expect(wrapper.find('[data-test-subj="externalLinkComma"]').exists()).toBeTruthy(); + }); + }); + + describe('not render', () => { + test('it should not render if childen prop is not given', () => { + wrapper = shallow( + + ); + expect(wrapper.find('[data-test-subj="externalLinkTooltip"]').exists()).toBeFalsy(); + }); + + test('it should not render if url prop is not given', () => { + wrapper = shallow( + + ); + expect(wrapper.find('[data-test-subj="externalLinkTooltip"]').exists()).toBeFalsy(); + }); + + test('it should not render if url prop is invalid', () => { + wrapper = shallow( + + ); + expect(wrapper.find('[data-test-subj="externalLinkTooltip"]').exists()).toBeFalsy(); + }); + + test('it should not render comma if id is not given', () => { + wrapper = shallow( + + {mockLinkName} + + ); + expect(wrapper.find('[data-test-subj="externalLinkComma"]').exists()).toBeFalsy(); + }); + + test('it should not render comma for the last item', () => { + wrapper = shallow( + + {mockLinkName} + + ); + expect(wrapper.find('[data-test-subj="externalLinkComma"]').exists()).toBeFalsy(); + }); + }); + + describe.each<[number, number, number, boolean]>([ + [0, 2, 5, true], + [1, 2, 5, false], + [2, 2, 5, false], + [3, 2, 5, false], + [4, 2, 5, false], + [5, 2, 5, false], + ])( + 'renders Comma when overflowIndex is smaller than allItems limit', + (idx, overflowIndexStart, allItemsLimit, showComma) => { + beforeAll(() => { + wrapper = shallow( + + {mockLinkName} + + ); + }); + + test(`should render Comma if current id (${idx}) is smaller than the index of last visible item`, () => { + expect(wrapper.find('[data-test-subj="externalLinkComma"]').exists()).toEqual(showComma); + }); + } + ); + + describe.each<[number, number, number, boolean]>([ + [0, 5, 4, true], + [1, 5, 4, true], + [2, 5, 4, true], + [3, 5, 4, false], + [4, 5, 4, false], + [5, 5, 4, false], + ])( + 'When overflowIndex is grater than allItems limit', + (idx, overflowIndexStart, allItemsLimit, showComma) => { + beforeAll(() => { + wrapper = shallow( + + {mockLinkName} + + ); + }); + + test(`Current item (${idx}) should render Comma execpt the last item`, () => { + expect(wrapper.find('[data-test-subj="externalLinkComma"]').exists()).toEqual(showComma); + }); + } + ); + + describe.each<[number, number, number, boolean]>([ + [0, 5, 5, true], + [1, 5, 5, true], + [2, 5, 5, true], + [3, 5, 5, true], + [4, 5, 5, false], + [5, 5, 5, false], + ])( + 'when overflowIndex equals to allItems limit', + (idx, overflowIndexStart, allItemsLimit, showComma) => { + beforeAll(() => { + wrapper = shallow( + + {mockLinkName} + + ); + }); + + test(`Current item (${idx}) should render Comma correctly`, () => { + expect(wrapper.find('[data-test-subj="externalLinkComma"]').exists()).toEqual(showComma); + }); + } + ); + }); + + describe('ReputationLink', () => { + const mockCustomizedReputationLinks = [ + { name: 'Link 1', url_template: 'https://www.virustotal.com/gui/search/{{ip}}' }, + { + name: 'Link 2', + url_template: 'https://talosintelligence.com/reputation_center/lookup?search={{ip}}', + }, + { name: 'Link 3', url_template: 'https://www.virustotal.com/gui/search/{{ip}}' }, + { + name: 'Link 4', + url_template: 'https://talosintelligence.com/reputation_center/lookup?search={{ip}}', + }, + { name: 'Link 5', url_template: 'https://www.virustotal.com/gui/search/{{ip}}' }, + { + name: 'Link 6', + url_template: 'https://talosintelligence.com/reputation_center/lookup?search={{ip}}', + }, + ]; + const mockDefaultReputationLinks = mockCustomizedReputationLinks.slice(0, 2); + + describe('links property', () => { + beforeEach(() => { + (useUiSetting$ as jest.Mock).mockReset(); + (useUiSetting$ as jest.Mock).mockReturnValue([mockDefaultReputationLinks]); + }); + + test('it renders default link text', () => { + const wrapper = shallow(); + wrapper.find('[data-test-subj="externalLink"]').forEach((node, idx) => { + expect(node.at(idx).text()).toEqual(mockDefaultReputationLinks[idx].name); + }); + }); + + test('it renders customized link text', () => { + (useUiSetting$ as jest.Mock).mockReset(); + (useUiSetting$ as jest.Mock).mockReturnValue([mockCustomizedReputationLinks]); + const wrapper = shallow(); + wrapper.find('[data-test-subj="externalLink"]').forEach((node, idx) => { + expect(node.at(idx).text()).toEqual(mockCustomizedReputationLinks[idx].name); + }); + }); + + test('it renders correct href', () => { + const wrapper = shallow(); + wrapper.find('[data-test-subj="externalLink"]').forEach((node, idx) => { + expect(node.prop('href')).toEqual( + mockDefaultReputationLinks[idx].url_template.replace('{{ip}}', '192.0.2.0') + ); + }); + }); + }); + + describe('number of links', () => { + beforeAll(() => { + (useUiSetting$ as jest.Mock).mockReset(); + (useUiSetting$ as jest.Mock).mockReturnValue([mockCustomizedReputationLinks]); + }); + + afterEach(() => { + (useUiSetting$ as jest.Mock).mockClear(); + }); + + test('it renders correct number of links by default', () => { + const wrapper = mountWithIntl(); + expect(wrapper.find('[data-test-subj="externalLinkComponent"]')).toHaveLength( + DEFAULT_NUMBER_OF_LINK + ); + }); + + test('it renders correct number of tooltips by default', () => { + const wrapper = mountWithIntl(); + expect(wrapper.find('[data-test-subj="externalLinkTooltip"]')).toHaveLength( + DEFAULT_NUMBER_OF_LINK + ); + }); + + test('it renders correct number of visible link', () => { + (useUiSetting$ as jest.Mock).mockReset(); + (useUiSetting$ as jest.Mock).mockReturnValue([mockCustomizedReputationLinks]); + + const wrapper = mountWithIntl( + + ); + expect(wrapper.find('[data-test-subj="externalLinkComponent"]')).toHaveLength(1); + }); + + test('it renders correct number of tooltips for visible links', () => { + (useUiSetting$ as jest.Mock).mockReset(); + (useUiSetting$ as jest.Mock).mockReturnValue([mockCustomizedReputationLinks]); + + const wrapper = mountWithIntl( + + ); + expect(wrapper.find('[data-test-subj="externalLinkTooltip"]')).toHaveLength(1); + }); + }); + + describe('invalid customized links', () => { + const mockInvalidLinksEmptyObj = [{}]; + const mockInvalidLinksNoName = [ + { url_template: 'https://talosintelligence.com/reputation_center/lookup?search={{ip}}' }, + ]; + const mockInvalidLinksNoUrl = [{ name: 'Link 1' }]; + const mockInvalidUrl = [{ name: 'Link 1', url_template: "" }]; + afterEach(() => { + (useUiSetting$ as jest.Mock).mockReset(); + }); + + test('it filters empty object', () => { + (useUiSetting$ as jest.Mock).mockReturnValue([mockInvalidLinksEmptyObj]); + + const wrapper = mountWithIntl( + + ); + expect(wrapper.find('[data-test-subj="externalLink"]')).toHaveLength(0); + }); + + test('it filters object without name property', () => { + (useUiSetting$ as jest.Mock).mockReturnValue([mockInvalidLinksNoName]); + + const wrapper = mountWithIntl( + + ); + expect(wrapper.find('[data-test-subj="externalLink"]')).toHaveLength(0); + }); + + test('it filters object without url_template property', () => { + (useUiSetting$ as jest.Mock).mockReturnValue([mockInvalidLinksNoUrl]); + + const wrapper = mountWithIntl( + + ); + expect(wrapper.find('[data-test-subj="externalLink"]')).toHaveLength(0); + }); + + test('it filters object with invalid url', () => { + (useUiSetting$ as jest.Mock).mockReturnValue([mockInvalidUrl]); + + const wrapper = mountWithIntl( + + ); + expect(wrapper.find('[data-test-subj="externalLink"]')).toHaveLength(0); + }); + }); + + describe('external icon', () => { + beforeAll(() => { + (useUiSetting$ as jest.Mock).mockReset(); + (useUiSetting$ as jest.Mock).mockReturnValue([mockCustomizedReputationLinks]); + }); + + afterEach(() => { + (useUiSetting$ as jest.Mock).mockClear(); + }); + + test('it renders correct number of external icons by default', () => { + const wrapper = mountWithIntl(); + expect(wrapper.find('[data-test-subj="externalLinkIcon"]')).toHaveLength(5); + }); + + test('it renders correct number of external icons', () => { + const wrapper = mountWithIntl( + + ); + expect(wrapper.find('[data-test-subj="externalLinkIcon"]')).toHaveLength(1); + }); + }); + }); + + describe('WhoisLink', () => { + test('it renders ip passed in as domain', () => { + const wrapper = mountWithIntl({'Example Link'}); + expect(wrapper.text()).toEqual('Example Link'); + }); + + test('it renders correct href', () => { + const wrapper = mountWithIntl({'Example Link'} ); + expect(wrapper.find('a').prop('href')).toEqual('https://www.iana.org/whois?q=192.0.2.0'); + }); + + test("it encodes ", () => { + const wrapper = mountWithIntl( + alert('XSS')"}>{'Example Link'} + ); + expect(wrapper.find('a').prop('href')).toEqual( + "https://www.iana.org/whois?q=%3Cscript%3Ealert('XSS')%3C%2Fscript%3E" + ); + }); + }); + + describe('CertificateFingerprintLink', () => { + test('it renders link text', () => { + const wrapper = mountWithIntl( + + {'Example Link'} + + ); + expect(wrapper.text()).toEqual('Example Link'); + }); + + test('it renders correct href', () => { + const wrapper = mountWithIntl( + + {'Example Link'} + + ); + expect(wrapper.find('a').prop('href')).toEqual( + 'https://sslbl.abuse.ch/ssl-certificates/sha1/abcd' + ); + }); + + test("it encodes ", () => { + const wrapper = mountWithIntl( + alert('XSS')"}> + {'Example Link'} + + ); + expect(wrapper.find('a').prop('href')).toEqual( + "https://sslbl.abuse.ch/ssl-certificates/sha1/%3Cscript%3Ealert('XSS')%3C%2Fscript%3E" + ); + }); + }); + + describe('Ja3FingerprintLink', () => { + test('it renders link text', () => { + const wrapper = mountWithIntl( + {'Example Link'} + ); + expect(wrapper.text()).toEqual('Example Link'); + }); + + test('it renders correct href', () => { + const wrapper = mountWithIntl( + {'Example Link'} + ); + expect(wrapper.find('a').prop('href')).toEqual( + 'https://sslbl.abuse.ch/ja3-fingerprints/abcd' + ); + }); + + test("it encodes ", () => { + const wrapper = mountWithIntl( + alert('XSS')"}> + {'Example Link'} + + ); + expect(wrapper.find('a').prop('href')).toEqual( + "https://sslbl.abuse.ch/ja3-fingerprints/%3Cscript%3Ealert('XSS')%3C%2Fscript%3E" + ); + }); + }); + + describe('PortOrServiceNameLink', () => { + test('it renders link text', () => { + const wrapper = mountWithIntl( + {'Example Link'} + ); + expect(wrapper.text()).toEqual('Example Link'); + }); + + test('it renders correct href when port is a number', () => { + const wrapper = mountWithIntl( + {'Example Link'} + ); + expect(wrapper.find('a').prop('href')).toEqual( + 'https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=443' + ); + }); + + test('it renders correct href when port is a string', () => { + const wrapper = mountWithIntl( + {'Example Link'} + ); + expect(wrapper.find('a').prop('href')).toEqual( + 'https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=80' + ); + }); + + test("it encodes ", () => { + const wrapper = mountWithIntl( + alert('XSS')"}> + {'Example Link'} + + ); + expect(wrapper.find('a').prop('href')).toEqual( + "https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=%3Cscript%3Ealert('XSS')%3C%2Fscript%3E" + ); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/common/components/links/index.tsx b/x-pack/plugins/siem/public/common/components/links/index.tsx new file mode 100644 index 00000000000000..4d639ce2781b12 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/links/index.tsx @@ -0,0 +1,312 @@ +/* + * 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 { EuiLink, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { useMemo } from 'react'; +import { isNil } from 'lodash/fp'; +import styled from 'styled-components'; + +import { IP_REPUTATION_LINKS_SETTING } from '../../../../common/constants'; +import { + DefaultFieldRendererOverflow, + DEFAULT_MORE_MAX_HEIGHT, +} from '../../../timelines/components/field_renderers/field_renderers'; +import { encodeIpv6 } from '../../lib/helpers'; +import { + getCaseDetailsUrl, + getHostDetailsUrl, + getIPDetailsUrl, + getCreateCaseUrl, +} from '../link_to'; +import { FlowTarget, FlowTargetSourceDest } from '../../../graphql/types'; +import { useUiSetting$ } from '../../lib/kibana'; +import { isUrlInvalid } from '../../../alerts/components/rules/step_about_rule/helpers'; +import { ExternalLinkIcon } from '../external_link_icon'; +import { navTabs } from '../../../app/home/home_navigations'; +import { useGetUrlSearch } from '../navigation/use_get_url_search'; + +import * as i18n from './translations'; + +export const DEFAULT_NUMBER_OF_LINK = 5; + +// Internal Links +const HostDetailsLinkComponent: React.FC<{ children?: React.ReactNode; hostName: string }> = ({ + children, + hostName, +}) => ( + + {children ? children : hostName} + +); + +const whitelistUrlSchemes = ['http://', 'https://']; +export const ExternalLink = React.memo<{ + url: string; + children?: React.ReactNode; + idx?: number; + overflowIndexStart?: number; + allItemsLimit?: number; +}>( + ({ + url, + children, + idx, + overflowIndexStart = DEFAULT_NUMBER_OF_LINK, + allItemsLimit = DEFAULT_NUMBER_OF_LINK, + }) => { + const lastVisibleItemIndex = overflowIndexStart - 1; + const lastItemIndex = allItemsLimit - 1; + const lastIndexToShow = Math.max(0, Math.min(lastVisibleItemIndex, lastItemIndex)); + const inWhitelist = whitelistUrlSchemes.some(scheme => url.indexOf(scheme) === 0); + return url && inWhitelist && !isUrlInvalid(url) && children ? ( + + + {children} + + {!isNil(idx) && idx < lastIndexToShow && } + + + ) : null; + } +); + +ExternalLink.displayName = 'ExternalLink'; + +export const HostDetailsLink = React.memo(HostDetailsLinkComponent); + +const IPDetailsLinkComponent: React.FC<{ + children?: React.ReactNode; + ip: string; + flowTarget?: FlowTarget | FlowTargetSourceDest; +}> = ({ children, ip, flowTarget = FlowTarget.source }) => ( + + {children ? children : ip} + +); + +export const IPDetailsLink = React.memo(IPDetailsLinkComponent); + +const CaseDetailsLinkComponent: React.FC<{ + children?: React.ReactNode; + detailName: string; + title?: string; +}> = ({ children, detailName, title }) => { + const search = useGetUrlSearch(navTabs.case); + + return ( + + {children ? children : detailName} + + ); +}; +export const CaseDetailsLink = React.memo(CaseDetailsLinkComponent); +CaseDetailsLink.displayName = 'CaseDetailsLink'; + +export const CreateCaseLink = React.memo<{ children: React.ReactNode }>(({ children }) => { + const search = useGetUrlSearch(navTabs.case); + return {children}; +}); + +CreateCaseLink.displayName = 'CreateCaseLink'; + +// External Links +export const GoogleLink = React.memo<{ children?: React.ReactNode; link: string }>( + ({ children, link }) => ( + + {children ? children : link} + + ) +); + +GoogleLink.displayName = 'GoogleLink'; + +export const PortOrServiceNameLink = React.memo<{ + children?: React.ReactNode; + portOrServiceName: number | string; +}>(({ children, portOrServiceName }) => ( + + {children ? children : portOrServiceName} + +)); + +PortOrServiceNameLink.displayName = 'PortOrServiceNameLink'; + +export const Ja3FingerprintLink = React.memo<{ + children?: React.ReactNode; + ja3Fingerprint: string; +}>(({ children, ja3Fingerprint }) => ( + + {children ? children : ja3Fingerprint} + +)); + +Ja3FingerprintLink.displayName = 'Ja3FingerprintLink'; + +export const CertificateFingerprintLink = React.memo<{ + children?: React.ReactNode; + certificateFingerprint: string; +}>(({ children, certificateFingerprint }) => ( + + {children ? children : certificateFingerprint} + +)); + +CertificateFingerprintLink.displayName = 'CertificateFingerprintLink'; + +enum DefaultReputationLink { + 'virustotal.com' = 'virustotal.com', + 'talosIntelligence.com' = 'talosIntelligence.com', +} + +export interface ReputationLinkSetting { + name: string; + url_template: string; +} + +function isDefaultReputationLink(name: string): name is DefaultReputationLink { + return ( + name === DefaultReputationLink['virustotal.com'] || + name === DefaultReputationLink['talosIntelligence.com'] + ); +} +const isReputationLink = ( + rowItem: string | ReputationLinkSetting +): rowItem is ReputationLinkSetting => + (rowItem as ReputationLinkSetting).url_template !== undefined && + (rowItem as ReputationLinkSetting).name !== undefined; + +export const Comma = styled('span')` + margin-right: 5px; + margin-left: 5px; + &::after { + content: ' ,'; + } +`; + +Comma.displayName = 'Comma'; + +const defaultNameMapping: Record = { + [DefaultReputationLink['virustotal.com']]: i18n.VIEW_VIRUS_TOTAL, + [DefaultReputationLink['talosIntelligence.com']]: i18n.VIEW_TALOS_INTELLIGENCE, +}; + +const ReputationLinkComponent: React.FC<{ + overflowIndexStart?: number; + allItemsLimit?: number; + showDomain?: boolean; + domain: string; + direction?: 'row' | 'column'; +}> = ({ + overflowIndexStart = DEFAULT_NUMBER_OF_LINK, + allItemsLimit = DEFAULT_NUMBER_OF_LINK, + showDomain = false, + domain, + direction = 'row', +}) => { + const [ipReputationLinksSetting] = useUiSetting$( + IP_REPUTATION_LINKS_SETTING + ); + + const ipReputationLinks: ReputationLinkSetting[] = useMemo( + () => + ipReputationLinksSetting + ?.slice(0, allItemsLimit) + .filter( + ({ url_template, name }) => + !isNil(url_template) && !isNil(name) && !isUrlInvalid(url_template) + ) + .map(({ name, url_template }: { name: string; url_template: string }) => ({ + name: isDefaultReputationLink(name) ? defaultNameMapping[name] : name, + url_template: url_template.replace(`{{ip}}`, encodeURIComponent(domain)), + })), + [ipReputationLinksSetting, domain, defaultNameMapping, allItemsLimit] + ); + + return ipReputationLinks?.length > 0 ? ( +
+ + + {ipReputationLinks + ?.slice(0, overflowIndexStart) + .map(({ name, url_template: urlTemplate }: ReputationLinkSetting, id) => ( + + <>{showDomain ? domain : name ?? domain} + + ))} + + + + { + return ( + isReputationLink(rowItem) && ( + + <>{rowItem.name ?? domain} + + ) + ); + }} + moreMaxHeight={DEFAULT_MORE_MAX_HEIGHT} + overflowIndexStart={overflowIndexStart} + /> + + +
+ ) : null; +}; + +ReputationLinkComponent.displayName = 'ReputationLinkComponent'; + +export const ReputationLink = React.memo(ReputationLinkComponent); + +export const WhoIsLink = React.memo<{ children?: React.ReactNode; domain: string }>( + ({ children, domain }) => ( + + {children ? children : domain} + + ) +); + +WhoIsLink.displayName = 'WhoIsLink'; diff --git a/x-pack/plugins/siem/public/common/components/links/translations.ts b/x-pack/plugins/siem/public/common/components/links/translations.ts new file mode 100644 index 00000000000000..fdc50361175776 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/links/translations.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export * from '../../../network/components/ip_overview/translations'; + +export const CASE_DETAILS_LINK_ARIA = (detailName: string) => + i18n.translate('xpack.siem.case.caseTable.caseDetailsLinkAria', { + values: { detailName }, + defaultMessage: 'click to visit case with title {detailName}', + }); diff --git a/x-pack/plugins/siem/public/components/loader/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/common/components/loader/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/loader/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/loader/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/loader/index.test.tsx b/x-pack/plugins/siem/public/common/components/loader/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/loader/index.test.tsx rename to x-pack/plugins/siem/public/common/components/loader/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/loader/index.tsx b/x-pack/plugins/siem/public/common/components/loader/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/loader/index.tsx rename to x-pack/plugins/siem/public/common/components/loader/index.tsx diff --git a/x-pack/plugins/siem/public/components/localized_date_tooltip/index.test.tsx b/x-pack/plugins/siem/public/common/components/localized_date_tooltip/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/localized_date_tooltip/index.test.tsx rename to x-pack/plugins/siem/public/common/components/localized_date_tooltip/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/localized_date_tooltip/index.tsx b/x-pack/plugins/siem/public/common/components/localized_date_tooltip/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/localized_date_tooltip/index.tsx rename to x-pack/plugins/siem/public/common/components/localized_date_tooltip/index.tsx diff --git a/x-pack/plugins/siem/public/components/markdown/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/common/components/markdown/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/markdown/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/markdown/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/markdown/__snapshots__/markdown_hint.test.tsx.snap b/x-pack/plugins/siem/public/common/components/markdown/__snapshots__/markdown_hint.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/markdown/__snapshots__/markdown_hint.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/markdown/__snapshots__/markdown_hint.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/markdown/index.test.tsx b/x-pack/plugins/siem/public/common/components/markdown/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/markdown/index.test.tsx rename to x-pack/plugins/siem/public/common/components/markdown/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/markdown/index.tsx b/x-pack/plugins/siem/public/common/components/markdown/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/markdown/index.tsx rename to x-pack/plugins/siem/public/common/components/markdown/index.tsx diff --git a/x-pack/plugins/siem/public/components/markdown/markdown_hint.test.tsx b/x-pack/plugins/siem/public/common/components/markdown/markdown_hint.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/markdown/markdown_hint.test.tsx rename to x-pack/plugins/siem/public/common/components/markdown/markdown_hint.test.tsx diff --git a/x-pack/plugins/siem/public/components/markdown/markdown_hint.tsx b/x-pack/plugins/siem/public/common/components/markdown/markdown_hint.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/markdown/markdown_hint.tsx rename to x-pack/plugins/siem/public/common/components/markdown/markdown_hint.tsx diff --git a/x-pack/plugins/siem/public/components/markdown/translations.ts b/x-pack/plugins/siem/public/common/components/markdown/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/markdown/translations.ts rename to x-pack/plugins/siem/public/common/components/markdown/translations.ts diff --git a/x-pack/plugins/siem/public/components/markdown_editor/constants.ts b/x-pack/plugins/siem/public/common/components/markdown_editor/constants.ts similarity index 100% rename from x-pack/plugins/siem/public/components/markdown_editor/constants.ts rename to x-pack/plugins/siem/public/common/components/markdown_editor/constants.ts diff --git a/x-pack/plugins/siem/public/common/components/markdown_editor/form.tsx b/x-pack/plugins/siem/public/common/components/markdown_editor/form.tsx new file mode 100644 index 00000000000000..2ed85b04fe3f6e --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/markdown_editor/form.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFormRow } from '@elastic/eui'; +import React, { useCallback } from 'react'; + +import { FieldHook, getFieldValidityAndErrorMessage } from '../../../shared_imports'; +import { CursorPosition, MarkdownEditor } from '.'; + +interface IMarkdownEditorForm { + bottomRightContent?: React.ReactNode; + dataTestSubj: string; + field: FieldHook; + idAria: string; + isDisabled: boolean; + onCursorPositionUpdate?: (cursorPosition: CursorPosition) => void; + placeholder?: string; + topRightContent?: React.ReactNode; +} +export const MarkdownEditorForm = ({ + bottomRightContent, + dataTestSubj, + field, + idAria, + isDisabled = false, + onCursorPositionUpdate, + placeholder, + topRightContent, +}: IMarkdownEditorForm) => { + const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); + + const handleContentChange = useCallback( + (newContent: string) => { + field.setValue(newContent); + }, + [field] + ); + + return ( + + + + ); +}; diff --git a/x-pack/plugins/siem/public/components/markdown_editor/index.tsx b/x-pack/plugins/siem/public/common/components/markdown_editor/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/markdown_editor/index.tsx rename to x-pack/plugins/siem/public/common/components/markdown_editor/index.tsx diff --git a/x-pack/plugins/siem/public/components/markdown_editor/translations.ts b/x-pack/plugins/siem/public/common/components/markdown_editor/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/markdown_editor/translations.ts rename to x-pack/plugins/siem/public/common/components/markdown_editor/translations.ts diff --git a/x-pack/plugins/siem/public/components/matrix_histogram/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/common/components/matrix_histogram/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/matrix_histogram/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/matrix_histogram/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/common/components/matrix_histogram/index.test.tsx b/x-pack/plugins/siem/public/common/components/matrix_histogram/index.test.tsx new file mode 100644 index 00000000000000..b45207ab47c7ad --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/matrix_histogram/index.test.tsx @@ -0,0 +1,134 @@ +/* + * 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. + */ + +/* eslint-disable react/display-name */ + +import { mount, ReactWrapper } from 'enzyme'; +import React from 'react'; + +import { MatrixHistogram } from '.'; +import { useQuery } from '../../containers/matrix_histogram'; +import { HistogramType } from '../../../graphql/types'; +jest.mock('../../lib/kibana'); + +jest.mock('./matrix_loader', () => { + return { + MatrixLoader: () => { + return
; + }, + }; +}); + +jest.mock('../header_section', () => { + return { + HeaderSection: () =>
, + }; +}); + +jest.mock('../charts/barchart', () => { + return { + BarChart: () =>
, + }; +}); + +jest.mock('../../containers/matrix_histogram', () => { + return { + useQuery: jest.fn(), + }; +}); + +jest.mock('../../components/matrix_histogram/utils', () => { + return { + getBarchartConfigs: jest.fn(), + getCustomChartData: jest.fn().mockReturnValue(true), + }; +}); + +describe('Matrix Histogram Component', () => { + let wrapper: ReactWrapper; + + const mockMatrixOverTimeHistogramProps = { + defaultIndex: ['defaultIndex'], + defaultStackByOption: { text: 'text', value: 'value' }, + endDate: new Date('2019-07-18T20:00:00.000Z').valueOf(), + errorMessage: 'error', + histogramType: HistogramType.alerts, + id: 'mockId', + isInspected: false, + isPtrIncluded: false, + setQuery: jest.fn(), + skip: false, + sourceId: 'default', + stackByField: 'mockStackByField', + stackByOptions: [{ text: 'text', value: 'value' }], + startDate: new Date('2019-07-18T19:00: 00.000Z').valueOf(), + subtitle: 'mockSubtitle', + totalCount: -1, + title: 'mockTitle', + dispatchSetAbsoluteRangeDatePicker: jest.fn(), + }; + + beforeAll(() => { + (useQuery as jest.Mock).mockReturnValue({ + data: null, + loading: false, + inspect: false, + totalCount: null, + }); + wrapper = mount(); + }); + describe('on initial load', () => { + test('it renders MatrixLoader', () => { + expect(wrapper.html()).toMatchSnapshot(); + expect(wrapper.find('MatrixLoader').exists()).toBe(true); + }); + }); + + describe('spacer', () => { + test('it renders a spacer by default', () => { + expect(wrapper.find('[data-test-subj="spacer"]').exists()).toBe(true); + }); + + test('it does NOT render a spacer when showSpacer is false', () => { + wrapper = mount(); + expect(wrapper.find('[data-test-subj="spacer"]').exists()).toBe(false); + }); + }); + + describe('not initial load', () => { + beforeAll(() => { + (useQuery as jest.Mock).mockReturnValue({ + data: [ + { x: 1, y: 2, g: 'g1' }, + { x: 2, y: 4, g: 'g1' }, + { x: 3, y: 6, g: 'g1' }, + { x: 1, y: 1, g: 'g2' }, + { x: 2, y: 3, g: 'g2' }, + { x: 3, y: 5, g: 'g2' }, + ], + loading: false, + inspect: false, + totalCount: 1, + }); + wrapper.setProps({ endDate: 100 }); + wrapper.update(); + }); + test('it renders no MatrixLoader', () => { + expect(wrapper.html()).toMatchSnapshot(); + expect(wrapper.find(`MatrixLoader`).exists()).toBe(false); + }); + + test('it shows BarChart if data available', () => { + expect(wrapper.find(`.barchart`).exists()).toBe(true); + }); + }); + + describe('select dropdown', () => { + test('should be hidden if only one option is provided', () => { + expect(wrapper.find('EuiSelect').exists()).toBe(false); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/common/components/matrix_histogram/index.tsx b/x-pack/plugins/siem/public/common/components/matrix_histogram/index.tsx new file mode 100644 index 00000000000000..b2a9f915005f15 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/matrix_histogram/index.tsx @@ -0,0 +1,275 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import { Position } from '@elastic/charts'; +import styled from 'styled-components'; + +import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSelect, EuiSpacer } from '@elastic/eui'; +import { noop } from 'lodash/fp'; +import { compose } from 'redux'; +import { connect } from 'react-redux'; +import * as i18n from './translations'; +import { BarChart } from '../charts/barchart'; +import { HeaderSection } from '../header_section'; +import { MatrixLoader } from './matrix_loader'; +import { Panel } from '../panel'; +import { getBarchartConfigs, getCustomChartData } from './utils'; +import { useQuery } from '../../containers/matrix_histogram'; +import { + MatrixHistogramProps, + MatrixHistogramOption, + HistogramAggregation, + MatrixHistogramQueryProps, +} from './types'; +import { InspectButtonContainer } from '../inspect'; + +import { State, inputsSelectors } from '../../store'; +import { hostsModel } from '../../../hosts/store'; +import { networkModel } from '../../../network/store'; + +import { + MatrixHistogramMappingTypes, + GetTitle, + GetSubTitle, +} from '../../components/matrix_histogram/types'; +import { SetQuery } from '../../../hosts/pages/navigation/types'; +import { QueryTemplateProps } from '../../containers/query_template'; +import { setAbsoluteRangeDatePicker } from '../../store/inputs/actions'; +import { InputsModelId } from '../../store/inputs/constants'; +import { HistogramType } from '../../../graphql/types'; + +export interface OwnProps extends QueryTemplateProps { + defaultStackByOption: MatrixHistogramOption; + errorMessage: string; + headerChildren?: React.ReactNode; + hideHistogramIfEmpty?: boolean; + histogramType: HistogramType; + id: string; + indexToAdd?: string[] | null; + legendPosition?: Position; + mapping?: MatrixHistogramMappingTypes; + showSpacer?: boolean; + setQuery: SetQuery; + setAbsoluteRangeDatePickerTarget?: InputsModelId; + showLegend?: boolean; + stackByOptions: MatrixHistogramOption[]; + subtitle?: string | GetSubTitle; + title: string | GetTitle; + type: hostsModel.HostsType | networkModel.NetworkType; +} + +const DEFAULT_PANEL_HEIGHT = 300; + +const HeaderChildrenFlexItem = styled(EuiFlexItem)` + margin-left: 24px; +`; + +// @ts-ignore - the EUI type definitions for Panel do no play nice with styled-components +const HistogramPanel = styled(Panel)<{ height?: number }>` + display: flex; + flex-direction: column; + ${({ height }) => (height != null ? `height: ${height}px;` : '')} +`; + +export const MatrixHistogramComponent: React.FC = ({ + chartHeight, + defaultStackByOption, + endDate, + errorMessage, + filterQuery, + headerChildren, + histogramType, + hideHistogramIfEmpty = false, + id, + indexToAdd, + isInspected, + legendPosition, + mapping, + panelHeight = DEFAULT_PANEL_HEIGHT, + setAbsoluteRangeDatePickerTarget = 'global', + setQuery, + showLegend, + showSpacer = true, + stackByOptions, + startDate, + subtitle, + title, + titleSize, + dispatchSetAbsoluteRangeDatePicker, + yTickFormatter, +}) => { + const barchartConfigs = useMemo( + () => + getBarchartConfigs({ + chartHeight, + from: startDate, + legendPosition, + to: endDate, + onBrushEnd: ({ x }) => { + if (!x) { + return; + } + const [min, max] = x; + dispatchSetAbsoluteRangeDatePicker({ + id: setAbsoluteRangeDatePickerTarget, + from: min, + to: max, + }); + }, + yTickFormatter, + showLegend, + }), + [ + chartHeight, + startDate, + legendPosition, + endDate, + dispatchSetAbsoluteRangeDatePicker, + yTickFormatter, + showLegend, + ] + ); + const [isInitialLoading, setIsInitialLoading] = useState(true); + const [selectedStackByOption, setSelectedStackByOption] = useState( + defaultStackByOption + ); + const setSelectedChartOptionCallback = useCallback( + (event: React.ChangeEvent) => { + setSelectedStackByOption( + stackByOptions.find(co => co.value === event.target.value) ?? defaultStackByOption + ); + }, + [] + ); + + const { data, loading, inspect, totalCount, refetch = noop } = useQuery<{}, HistogramAggregation>( + { + endDate, + errorMessage, + filterQuery, + histogramType, + indexToAdd, + startDate, + isInspected, + stackByField: selectedStackByOption.value, + } + ); + + const titleWithStackByField = useMemo( + () => (title != null && typeof title === 'function' ? title(selectedStackByOption) : title), + [title, selectedStackByOption] + ); + const subtitleWithCounts = useMemo(() => { + if (isInitialLoading) { + return null; + } + + if (typeof subtitle === 'function') { + return totalCount >= 0 ? subtitle(totalCount) : null; + } + + return subtitle; + }, [isInitialLoading, subtitle, totalCount]); + const hideHistogram = useMemo(() => (totalCount <= 0 && hideHistogramIfEmpty ? true : false), [ + totalCount, + hideHistogramIfEmpty, + ]); + const barChartData = useMemo(() => getCustomChartData(data, mapping), [data, mapping]); + + useEffect(() => { + if (!loading && !isInitialLoading) { + setQuery({ id, inspect, loading, refetch }); + } + + if (isInitialLoading && !!barChartData && data) { + setIsInitialLoading(false); + } + }, [ + setQuery, + id, + inspect, + loading, + refetch, + isInitialLoading, + barChartData, + data, + setIsInitialLoading, + ]); + + if (hideHistogram) { + return null; + } + + return ( + <> + + + {loading && !isInitialLoading && ( + + )} + + + + + {stackByOptions.length > 1 && ( + + )} + + {headerChildren} + + + + {isInitialLoading ? ( + + ) : ( + + )} + + + {showSpacer && } + + ); +}; + +export const MatrixHistogram = React.memo(MatrixHistogramComponent); + +const makeMapStateToProps = () => { + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { type, id }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + isInspected, + }; + }; + return mapStateToProps; +}; + +export const MatrixHistogramContainer = compose>( + connect(makeMapStateToProps, { + dispatchSetAbsoluteRangeDatePicker: setAbsoluteRangeDatePicker, + }) +)(MatrixHistogram); diff --git a/x-pack/plugins/siem/public/components/matrix_histogram/matrix_loader.tsx b/x-pack/plugins/siem/public/common/components/matrix_histogram/matrix_loader.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/matrix_histogram/matrix_loader.tsx rename to x-pack/plugins/siem/public/common/components/matrix_histogram/matrix_loader.tsx diff --git a/x-pack/plugins/siem/public/components/matrix_histogram/translations.ts b/x-pack/plugins/siem/public/common/components/matrix_histogram/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/matrix_histogram/translations.ts rename to x-pack/plugins/siem/public/common/components/matrix_histogram/translations.ts diff --git a/x-pack/plugins/siem/public/common/components/matrix_histogram/types.ts b/x-pack/plugins/siem/public/common/components/matrix_histogram/types.ts new file mode 100644 index 00000000000000..e30f1e9374c266 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/matrix_histogram/types.ts @@ -0,0 +1,143 @@ +/* + * 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 { EuiTitleSize } from '@elastic/eui'; +import { ScaleType, Position, TickFormatter } from '@elastic/charts'; +import { ActionCreator } from 'redux'; +import { ESQuery } from '../../../../common/typed_json'; +import { SetQuery } from '../../../hosts/pages/navigation/types'; +import { InputsModelId } from '../../store/inputs/constants'; +import { HistogramType } from '../../../graphql/types'; +import { UpdateDateRange } from '../charts/common'; + +export type MatrixHistogramMappingTypes = Record< + string, + { key: string; value: null; color?: string | undefined } +>; +export interface MatrixHistogramOption { + text: string; + value: string; +} + +export type GetSubTitle = (count: number) => string; +export type GetTitle = (matrixHistogramOption: MatrixHistogramOption) => string; + +export interface MatrixHisrogramConfigs { + defaultStackByOption: MatrixHistogramOption; + errorMessage: string; + hideHistogramIfEmpty?: boolean; + histogramType: HistogramType; + legendPosition?: Position; + mapping?: MatrixHistogramMappingTypes; + stackByOptions: MatrixHistogramOption[]; + subtitle?: string | GetSubTitle; + title: string | GetTitle; + titleSize?: EuiTitleSize; +} + +interface MatrixHistogramBasicProps { + chartHeight?: number; + defaultIndex: string[]; + defaultStackByOption: MatrixHistogramOption; + dispatchSetAbsoluteRangeDatePicker: ActionCreator<{ + id: InputsModelId; + from: number; + to: number; + }>; + endDate: number; + headerChildren?: React.ReactNode; + hideHistogramIfEmpty?: boolean; + id: string; + legendPosition?: Position; + mapping?: MatrixHistogramMappingTypes; + panelHeight?: number; + setQuery: SetQuery; + startDate: number; + stackByOptions: MatrixHistogramOption[]; + subtitle?: string | GetSubTitle; + title?: string | GetTitle; + titleSize?: EuiTitleSize; +} + +export interface MatrixHistogramQueryProps { + endDate: number; + errorMessage: string; + filterQuery?: ESQuery | string | undefined; + setAbsoluteRangeDatePicker?: ActionCreator<{ + id: InputsModelId; + from: number; + to: number; + }>; + setAbsoluteRangeDatePickerTarget?: InputsModelId; + stackByField: string; + startDate: number; + indexToAdd?: string[] | null; + isInspected: boolean; + histogramType: HistogramType; +} + +export interface MatrixHistogramProps extends MatrixHistogramBasicProps { + scaleType?: ScaleType; + yTickFormatter?: (value: number) => string; + showLegend?: boolean; + showSpacer?: boolean; + legendPosition?: Position; +} + +export interface HistogramBucket { + key_as_string: string; + key: number; + doc_count: number; +} +export interface GroupBucket { + key: string; + signals: { + buckets: HistogramBucket[]; + }; +} + +export interface HistogramAggregation { + histogramAgg: { + buckets: GroupBucket[]; + }; +} + +export interface BarchartConfigs { + series: { + xScaleType: ScaleType; + yScaleType: ScaleType; + stackAccessors: string[]; + }; + axis: { + xTickFormatter: TickFormatter; + yTickFormatter: TickFormatter; + tickSize: number; + }; + settings: { + legendPosition: Position; + onBrushEnd: UpdateDateRange; + showLegend: boolean; + showLegendExtra: boolean; + theme: { + scales: { + barsPadding: number; + }; + chartMargins: { + left: number; + right: number; + top: number; + bottom: number; + }; + chartPaddings: { + left: number; + right: number; + top: number; + bottom: number; + }; + }; + }; + customHeight: number; +} diff --git a/x-pack/plugins/siem/public/components/matrix_histogram/utils.test.ts b/x-pack/plugins/siem/public/common/components/matrix_histogram/utils.test.ts similarity index 98% rename from x-pack/plugins/siem/public/components/matrix_histogram/utils.test.ts rename to x-pack/plugins/siem/public/common/components/matrix_histogram/utils.test.ts index 2c34a307bfdedd..9e3ddcc014c61b 100644 --- a/x-pack/plugins/siem/public/components/matrix_histogram/utils.test.ts +++ b/x-pack/plugins/siem/public/common/components/matrix_histogram/utils.test.ts @@ -13,7 +13,7 @@ import { } from './utils'; import { UpdateDateRange } from '../charts/common'; import { Position } from '@elastic/charts'; -import { MatrixOverTimeHistogramData } from '../../graphql/types'; +import { MatrixOverTimeHistogramData } from '../../../graphql/types'; import { BarchartConfigs } from './types'; describe('utils', () => { diff --git a/x-pack/plugins/siem/public/common/components/matrix_histogram/utils.ts b/x-pack/plugins/siem/public/common/components/matrix_histogram/utils.ts new file mode 100644 index 00000000000000..45e9c54b2eff87 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/matrix_histogram/utils.ts @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { ScaleType, Position } from '@elastic/charts'; +import { get, groupBy, map, toPairs } from 'lodash/fp'; + +import { UpdateDateRange, ChartSeriesData } from '../charts/common'; +import { MatrixHistogramMappingTypes, BarchartConfigs } from './types'; +import { MatrixOverTimeHistogramData } from '../../../graphql/types'; +import { histogramDateTimeFormatter } from '../utils'; + +interface GetBarchartConfigsProps { + chartHeight?: number; + from: number; + legendPosition?: Position; + to: number; + onBrushEnd: UpdateDateRange; + yTickFormatter?: (value: number) => string; + showLegend?: boolean; +} + +export const DEFAULT_CHART_HEIGHT = 174; +export const DEFAULT_Y_TICK_FORMATTER = (value: string | number): string => value.toLocaleString(); + +export const getBarchartConfigs = ({ + chartHeight, + from, + legendPosition, + to, + onBrushEnd, + yTickFormatter, + showLegend, +}: GetBarchartConfigsProps): BarchartConfigs => ({ + series: { + xScaleType: ScaleType.Time, + yScaleType: ScaleType.Linear, + stackAccessors: ['g'], + }, + axis: { + xTickFormatter: histogramDateTimeFormatter([from, to]), + yTickFormatter: yTickFormatter != null ? yTickFormatter : DEFAULT_Y_TICK_FORMATTER, + tickSize: 8, + }, + settings: { + legendPosition: legendPosition ?? Position.Right, + onBrushEnd, + showLegend: showLegend ?? true, + showLegendExtra: true, + theme: { + scales: { + barsPadding: 0.08, + }, + chartMargins: { + left: 0, + right: 0, + top: 0, + bottom: 0, + }, + chartPaddings: { + left: 0, + right: 0, + top: 0, + bottom: 0, + }, + }, + }, + customHeight: chartHeight ?? DEFAULT_CHART_HEIGHT, +}); + +export const defaultLegendColors = [ + '#1EA593', + '#2B70F7', + '#CE0060', + '#38007E', + '#FCA5D3', + '#F37020', + '#E49E29', + '#B0916F', + '#7B000B', + '#34130C', +]; + +export const formatToChartDataItem = ([key, value]: [ + string, + MatrixOverTimeHistogramData[] +]): ChartSeriesData => ({ + key, + value, +}); + +export const getCustomChartData = ( + data: MatrixOverTimeHistogramData[] | null, + mapping?: MatrixHistogramMappingTypes +): ChartSeriesData[] => { + if (!data) return []; + const dataGroupedByEvent = groupBy('g', data); + const dataGroupedEntries = toPairs(dataGroupedByEvent); + const formattedChartData = map(formatToChartDataItem, dataGroupedEntries); + + if (mapping) + return map((item: ChartSeriesData) => { + const mapItem = get(item.key, mapping); + return { ...item, color: mapItem?.color }; + }, formattedChartData); + else return formattedChartData; +}; diff --git a/x-pack/plugins/siem/public/components/ml/__snapshots__/entity_draggable.test.tsx.snap b/x-pack/plugins/siem/public/common/components/ml/__snapshots__/entity_draggable.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/ml/__snapshots__/entity_draggable.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/ml/__snapshots__/entity_draggable.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/ml/anomaly/anomaly_table_provider.tsx b/x-pack/plugins/siem/public/common/components/ml/anomaly/anomaly_table_provider.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/ml/anomaly/anomaly_table_provider.tsx rename to x-pack/plugins/siem/public/common/components/ml/anomaly/anomaly_table_provider.tsx diff --git a/x-pack/plugins/siem/public/components/ml/anomaly/get_interval_from_anomalies.test.ts b/x-pack/plugins/siem/public/common/components/ml/anomaly/get_interval_from_anomalies.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/anomaly/get_interval_from_anomalies.test.ts rename to x-pack/plugins/siem/public/common/components/ml/anomaly/get_interval_from_anomalies.test.ts diff --git a/x-pack/plugins/siem/public/components/ml/anomaly/get_interval_from_anomalies.ts b/x-pack/plugins/siem/public/common/components/ml/anomaly/get_interval_from_anomalies.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/anomaly/get_interval_from_anomalies.ts rename to x-pack/plugins/siem/public/common/components/ml/anomaly/get_interval_from_anomalies.ts diff --git a/x-pack/plugins/siem/public/components/ml/anomaly/translations.ts b/x-pack/plugins/siem/public/common/components/ml/anomaly/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/anomaly/translations.ts rename to x-pack/plugins/siem/public/common/components/ml/anomaly/translations.ts diff --git a/x-pack/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.test.ts b/x-pack/plugins/siem/public/common/components/ml/anomaly/use_anomalies_table_data.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.test.ts rename to x-pack/plugins/siem/public/common/components/ml/anomaly/use_anomalies_table_data.test.ts diff --git a/x-pack/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.ts b/x-pack/plugins/siem/public/common/components/ml/anomaly/use_anomalies_table_data.ts similarity index 95% rename from x-pack/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.ts rename to x-pack/plugins/siem/public/common/components/ml/anomaly/use_anomalies_table_data.ts index d64bd3a64e941f..51300d91450003 100644 --- a/x-pack/plugins/siem/public/components/ml/anomaly/use_anomalies_table_data.ts +++ b/x-pack/plugins/siem/public/common/components/ml/anomaly/use_anomalies_table_data.ts @@ -6,10 +6,10 @@ import { useState, useEffect } from 'react'; -import { DEFAULT_ANOMALY_SCORE } from '../../../../common/constants'; +import { DEFAULT_ANOMALY_SCORE } from '../../../../../common/constants'; import { anomaliesTableData } from '../api/anomalies_table_data'; import { InfluencerInput, Anomalies, CriteriaFields } from '../types'; -import { hasMlUserPermissions } from '../permissions/has_ml_user_permissions'; +import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions'; import { useSiemJobs } from '../../ml_popover/hooks/use_siem_jobs'; import { useMlCapabilities } from '../../ml_popover/hooks/use_ml_capabilities'; import { useStateToaster, errorToToaster } from '../../toasters'; diff --git a/x-pack/plugins/siem/public/components/ml/api/anomalies_table_data.ts b/x-pack/plugins/siem/public/common/components/ml/api/anomalies_table_data.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/api/anomalies_table_data.ts rename to x-pack/plugins/siem/public/common/components/ml/api/anomalies_table_data.ts diff --git a/x-pack/plugins/siem/public/components/ml/api/errors.ts b/x-pack/plugins/siem/public/common/components/ml/api/errors.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/api/errors.ts rename to x-pack/plugins/siem/public/common/components/ml/api/errors.ts diff --git a/x-pack/plugins/siem/public/common/components/ml/api/get_ml_capabilities.ts b/x-pack/plugins/siem/public/common/components/ml/api/get_ml_capabilities.ts new file mode 100644 index 00000000000000..32f6f888ab8d71 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/ml/api/get_ml_capabilities.ts @@ -0,0 +1,30 @@ +/* + * 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 { MlCapabilitiesResponse } from '../../../../../../ml/public'; +import { KibanaServices } from '../../../lib/kibana'; +import { InfluencerInput } from '../types'; + +export interface Body { + jobIds: string[]; + criteriaFields: string[]; + influencers: InfluencerInput[]; + aggregationInterval: string; + threshold: number; + earliestMs: number; + latestMs: number; + dateFormatTz: string; + maxRecords: number; + maxExamples: number; +} + +export const getMlCapabilities = async (signal: AbortSignal): Promise => { + return KibanaServices.get().http.fetch('/api/ml/ml_capabilities', { + method: 'GET', + asSystemRequest: true, + signal, + }); +}; diff --git a/x-pack/plugins/siem/public/components/ml/api/throw_if_not_ok.test.ts b/x-pack/plugins/siem/public/common/components/ml/api/throw_if_not_ok.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/api/throw_if_not_ok.test.ts rename to x-pack/plugins/siem/public/common/components/ml/api/throw_if_not_ok.test.ts diff --git a/x-pack/plugins/siem/public/components/ml/api/throw_if_not_ok.ts b/x-pack/plugins/siem/public/common/components/ml/api/throw_if_not_ok.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/api/throw_if_not_ok.ts rename to x-pack/plugins/siem/public/common/components/ml/api/throw_if_not_ok.ts diff --git a/x-pack/plugins/siem/public/components/ml/api/translations.ts b/x-pack/plugins/siem/public/common/components/ml/api/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/api/translations.ts rename to x-pack/plugins/siem/public/common/components/ml/api/translations.ts diff --git a/x-pack/plugins/siem/public/components/ml/conditional_links/add_entities_to_kql.test.ts b/x-pack/plugins/siem/public/common/components/ml/conditional_links/add_entities_to_kql.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/conditional_links/add_entities_to_kql.test.ts rename to x-pack/plugins/siem/public/common/components/ml/conditional_links/add_entities_to_kql.test.ts diff --git a/x-pack/plugins/siem/public/components/ml/conditional_links/add_entities_to_kql.ts b/x-pack/plugins/siem/public/common/components/ml/conditional_links/add_entities_to_kql.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/conditional_links/add_entities_to_kql.ts rename to x-pack/plugins/siem/public/common/components/ml/conditional_links/add_entities_to_kql.ts diff --git a/x-pack/plugins/siem/public/components/ml/conditional_links/entity_helpers.test.ts b/x-pack/plugins/siem/public/common/components/ml/conditional_links/entity_helpers.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/conditional_links/entity_helpers.test.ts rename to x-pack/plugins/siem/public/common/components/ml/conditional_links/entity_helpers.test.ts diff --git a/x-pack/plugins/siem/public/components/ml/conditional_links/entity_helpers.ts b/x-pack/plugins/siem/public/common/components/ml/conditional_links/entity_helpers.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/conditional_links/entity_helpers.ts rename to x-pack/plugins/siem/public/common/components/ml/conditional_links/entity_helpers.ts diff --git a/x-pack/plugins/siem/public/components/ml/conditional_links/ml_host_conditional_container.tsx b/x-pack/plugins/siem/public/common/components/ml/conditional_links/ml_host_conditional_container.tsx similarity index 94% rename from x-pack/plugins/siem/public/components/ml/conditional_links/ml_host_conditional_container.tsx rename to x-pack/plugins/siem/public/common/components/ml/conditional_links/ml_host_conditional_container.tsx index b7c544273ae927..6ca723c50c6817 100644 --- a/x-pack/plugins/siem/public/components/ml/conditional_links/ml_host_conditional_container.tsx +++ b/x-pack/plugins/siem/public/common/components/ml/conditional_links/ml_host_conditional_container.tsx @@ -11,10 +11,10 @@ import { Redirect, Route, Switch, RouteComponentProps } from 'react-router-dom'; import { addEntitiesToKql } from './add_entities_to_kql'; import { replaceKQLParts } from './replace_kql_parts'; import { emptyEntity, multipleEntities, getMultipleEntities } from './entity_helpers'; -import { SiemPageName } from '../../../pages/home/types'; -import { HostsTableType } from '../../../store/hosts/model'; +import { SiemPageName } from '../../../../app/types'; +import { HostsTableType } from '../../../../hosts/store/model'; -import { url as urlUtils } from '../../../../../../../src/plugins/kibana_utils/public'; +import { url as urlUtils } from '../../../../../../../../src/plugins/kibana_utils/public'; interface QueryStringType { '?_g': string; diff --git a/x-pack/plugins/siem/public/components/ml/conditional_links/ml_network_conditional_container.tsx b/x-pack/plugins/siem/public/common/components/ml/conditional_links/ml_network_conditional_container.tsx similarity index 95% rename from x-pack/plugins/siem/public/components/ml/conditional_links/ml_network_conditional_container.tsx rename to x-pack/plugins/siem/public/common/components/ml/conditional_links/ml_network_conditional_container.tsx index 54773e3ab6dda7..05049cd9b4ea5e 100644 --- a/x-pack/plugins/siem/public/components/ml/conditional_links/ml_network_conditional_container.tsx +++ b/x-pack/plugins/siem/public/common/components/ml/conditional_links/ml_network_conditional_container.tsx @@ -11,9 +11,9 @@ import { Redirect, Route, Switch, RouteComponentProps } from 'react-router-dom'; import { addEntitiesToKql } from './add_entities_to_kql'; import { replaceKQLParts } from './replace_kql_parts'; import { emptyEntity, getMultipleEntities, multipleEntities } from './entity_helpers'; -import { SiemPageName } from '../../../pages/home/types'; +import { SiemPageName } from '../../../../app/types'; -import { url as urlUtils } from '../../../../../../../src/plugins/kibana_utils/public'; +import { url as urlUtils } from '../../../../../../../../src/plugins/kibana_utils/public'; interface QueryStringType { '?_g': string; diff --git a/x-pack/plugins/siem/public/components/ml/conditional_links/remove_kql_variables.test.ts b/x-pack/plugins/siem/public/common/components/ml/conditional_links/remove_kql_variables.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/conditional_links/remove_kql_variables.test.ts rename to x-pack/plugins/siem/public/common/components/ml/conditional_links/remove_kql_variables.test.ts diff --git a/x-pack/plugins/siem/public/components/ml/conditional_links/remove_kql_variables.ts b/x-pack/plugins/siem/public/common/components/ml/conditional_links/remove_kql_variables.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/conditional_links/remove_kql_variables.ts rename to x-pack/plugins/siem/public/common/components/ml/conditional_links/remove_kql_variables.ts diff --git a/x-pack/plugins/siem/public/components/ml/conditional_links/replace_kql_commas_with_or.test.ts b/x-pack/plugins/siem/public/common/components/ml/conditional_links/replace_kql_commas_with_or.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/conditional_links/replace_kql_commas_with_or.test.ts rename to x-pack/plugins/siem/public/common/components/ml/conditional_links/replace_kql_commas_with_or.test.ts diff --git a/x-pack/plugins/siem/public/components/ml/conditional_links/replace_kql_commas_with_or.ts b/x-pack/plugins/siem/public/common/components/ml/conditional_links/replace_kql_commas_with_or.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/conditional_links/replace_kql_commas_with_or.ts rename to x-pack/plugins/siem/public/common/components/ml/conditional_links/replace_kql_commas_with_or.ts diff --git a/x-pack/plugins/siem/public/components/ml/conditional_links/replace_kql_parts.test.ts b/x-pack/plugins/siem/public/common/components/ml/conditional_links/replace_kql_parts.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/conditional_links/replace_kql_parts.test.ts rename to x-pack/plugins/siem/public/common/components/ml/conditional_links/replace_kql_parts.test.ts diff --git a/x-pack/plugins/siem/public/components/ml/conditional_links/replace_kql_parts.ts b/x-pack/plugins/siem/public/common/components/ml/conditional_links/replace_kql_parts.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/conditional_links/replace_kql_parts.ts rename to x-pack/plugins/siem/public/common/components/ml/conditional_links/replace_kql_parts.ts diff --git a/x-pack/plugins/siem/public/components/ml/conditional_links/rison_helpers.test.ts b/x-pack/plugins/siem/public/common/components/ml/conditional_links/rison_helpers.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/conditional_links/rison_helpers.test.ts rename to x-pack/plugins/siem/public/common/components/ml/conditional_links/rison_helpers.test.ts diff --git a/x-pack/plugins/siem/public/components/ml/conditional_links/rison_helpers.ts b/x-pack/plugins/siem/public/common/components/ml/conditional_links/rison_helpers.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/conditional_links/rison_helpers.ts rename to x-pack/plugins/siem/public/common/components/ml/conditional_links/rison_helpers.ts diff --git a/x-pack/plugins/siem/public/components/ml/criteria/get_criteria_from_host_type.test.ts b/x-pack/plugins/siem/public/common/components/ml/criteria/get_criteria_from_host_type.test.ts similarity index 94% rename from x-pack/plugins/siem/public/components/ml/criteria/get_criteria_from_host_type.test.ts rename to x-pack/plugins/siem/public/common/components/ml/criteria/get_criteria_from_host_type.test.ts index d8e951adabbc9e..215df22f4a2558 100644 --- a/x-pack/plugins/siem/public/components/ml/criteria/get_criteria_from_host_type.test.ts +++ b/x-pack/plugins/siem/public/common/components/ml/criteria/get_criteria_from_host_type.test.ts @@ -5,7 +5,7 @@ */ import { getCriteriaFromHostType } from './get_criteria_from_host_type'; -import { HostsType } from '../../../store/hosts/model'; +import { HostsType } from '../../../../hosts/store/model'; describe('get_criteria_from_host_type', () => { test('returns host names from criteria if the host type is details', () => { diff --git a/x-pack/plugins/siem/public/components/ml/criteria/get_criteria_from_host_type.ts b/x-pack/plugins/siem/public/common/components/ml/criteria/get_criteria_from_host_type.ts similarity index 90% rename from x-pack/plugins/siem/public/components/ml/criteria/get_criteria_from_host_type.ts rename to x-pack/plugins/siem/public/common/components/ml/criteria/get_criteria_from_host_type.ts index 2667e3a089f41b..5988f0d1001b28 100644 --- a/x-pack/plugins/siem/public/components/ml/criteria/get_criteria_from_host_type.ts +++ b/x-pack/plugins/siem/public/common/components/ml/criteria/get_criteria_from_host_type.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HostsType } from '../../../store/hosts/model'; +import { HostsType } from '../../../../hosts/store/model'; import { CriteriaFields } from '../types'; export const getCriteriaFromHostType = ( diff --git a/x-pack/plugins/siem/public/components/ml/criteria/get_criteria_from_network_type.test.ts b/x-pack/plugins/siem/public/common/components/ml/criteria/get_criteria_from_network_type.test.ts similarity index 92% rename from x-pack/plugins/siem/public/components/ml/criteria/get_criteria_from_network_type.test.ts rename to x-pack/plugins/siem/public/common/components/ml/criteria/get_criteria_from_network_type.test.ts index fe1cd77a611955..07bdee140a0cdd 100644 --- a/x-pack/plugins/siem/public/components/ml/criteria/get_criteria_from_network_type.test.ts +++ b/x-pack/plugins/siem/public/common/components/ml/criteria/get_criteria_from_network_type.test.ts @@ -5,8 +5,8 @@ */ import { getCriteriaFromNetworkType } from './get_criteria_from_network_type'; -import { NetworkType } from '../../../store/network/model'; -import { FlowTarget } from '../../../graphql/types'; +import { NetworkType } from '../../../../network/store/model'; +import { FlowTarget } from '../../../../graphql/types'; describe('get_criteria_from_network_type', () => { test('returns network names from criteria if the network type is details and it is source', () => { diff --git a/x-pack/plugins/siem/public/components/ml/criteria/get_criteria_from_network_type.ts b/x-pack/plugins/siem/public/common/components/ml/criteria/get_criteria_from_network_type.ts similarity index 86% rename from x-pack/plugins/siem/public/components/ml/criteria/get_criteria_from_network_type.ts rename to x-pack/plugins/siem/public/common/components/ml/criteria/get_criteria_from_network_type.ts index 75c7e580f93c04..d717edea97cce1 100644 --- a/x-pack/plugins/siem/public/components/ml/criteria/get_criteria_from_network_type.ts +++ b/x-pack/plugins/siem/public/common/components/ml/criteria/get_criteria_from_network_type.ts @@ -5,8 +5,8 @@ */ import { CriteriaFields } from '../types'; -import { NetworkType } from '../../../store/network/model'; -import { FlowTarget } from '../../../graphql/types'; +import { NetworkType } from '../../../../network/store/model'; +import { FlowTarget } from '../../../../graphql/types'; export const getCriteriaFromNetworkType = ( type: NetworkType, diff --git a/x-pack/plugins/siem/public/components/ml/criteria/host_to_criteria.test.ts b/x-pack/plugins/siem/public/common/components/ml/criteria/host_to_criteria.test.ts similarity index 95% rename from x-pack/plugins/siem/public/components/ml/criteria/host_to_criteria.test.ts rename to x-pack/plugins/siem/public/common/components/ml/criteria/host_to_criteria.test.ts index 8cc672ab4321cb..bdd107145516f3 100644 --- a/x-pack/plugins/siem/public/components/ml/criteria/host_to_criteria.test.ts +++ b/x-pack/plugins/siem/public/common/components/ml/criteria/host_to_criteria.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HostItem } from '../../../graphql/types'; +import { HostItem } from '../../../../graphql/types'; import { CriteriaFields } from '../types'; import { hostToCriteria } from './host_to_criteria'; diff --git a/x-pack/plugins/siem/public/components/ml/criteria/host_to_criteria.ts b/x-pack/plugins/siem/public/common/components/ml/criteria/host_to_criteria.ts similarity index 91% rename from x-pack/plugins/siem/public/components/ml/criteria/host_to_criteria.ts rename to x-pack/plugins/siem/public/common/components/ml/criteria/host_to_criteria.ts index aeb5fa2646822a..f708bd43b8c9bf 100644 --- a/x-pack/plugins/siem/public/components/ml/criteria/host_to_criteria.ts +++ b/x-pack/plugins/siem/public/common/components/ml/criteria/host_to_criteria.ts @@ -5,7 +5,7 @@ */ import { CriteriaFields } from '../types'; -import { HostItem } from '../../../graphql/types'; +import { HostItem } from '../../../../graphql/types'; export const hostToCriteria = (hostItem: HostItem): CriteriaFields[] => { if (hostItem.host != null && hostItem.host.name != null) { diff --git a/x-pack/plugins/siem/public/components/ml/criteria/network_to_criteria.test.ts b/x-pack/plugins/siem/public/common/components/ml/criteria/network_to_criteria.test.ts similarity index 95% rename from x-pack/plugins/siem/public/components/ml/criteria/network_to_criteria.test.ts rename to x-pack/plugins/siem/public/common/components/ml/criteria/network_to_criteria.test.ts index d6abb4a42e80f6..6c0d2fc60a6264 100644 --- a/x-pack/plugins/siem/public/components/ml/criteria/network_to_criteria.test.ts +++ b/x-pack/plugins/siem/public/common/components/ml/criteria/network_to_criteria.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FlowTarget } from '../../../graphql/types'; +import { FlowTarget } from '../../../../graphql/types'; import { CriteriaFields } from '../types'; import { networkToCriteria } from './network_to_criteria'; diff --git a/x-pack/plugins/siem/public/components/ml/criteria/network_to_criteria.ts b/x-pack/plugins/siem/public/common/components/ml/criteria/network_to_criteria.ts similarity index 91% rename from x-pack/plugins/siem/public/components/ml/criteria/network_to_criteria.ts rename to x-pack/plugins/siem/public/common/components/ml/criteria/network_to_criteria.ts index a859931d6e228f..de2cc35007e870 100644 --- a/x-pack/plugins/siem/public/components/ml/criteria/network_to_criteria.ts +++ b/x-pack/plugins/siem/public/common/components/ml/criteria/network_to_criteria.ts @@ -5,7 +5,7 @@ */ import { CriteriaFields } from '../types'; -import { FlowTarget } from '../../../graphql/types'; +import { FlowTarget } from '../../../../graphql/types'; export const networkToCriteria = (ip: string, flowTarget: FlowTarget): CriteriaFields[] => { if (flowTarget === FlowTarget.source) { diff --git a/x-pack/plugins/siem/public/components/ml/entity_draggable.test.tsx b/x-pack/plugins/siem/public/common/components/ml/entity_draggable.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/ml/entity_draggable.test.tsx rename to x-pack/plugins/siem/public/common/components/ml/entity_draggable.test.tsx diff --git a/x-pack/plugins/siem/public/components/ml/entity_draggable.tsx b/x-pack/plugins/siem/public/common/components/ml/entity_draggable.tsx similarity index 88% rename from x-pack/plugins/siem/public/components/ml/entity_draggable.tsx rename to x-pack/plugins/siem/public/common/components/ml/entity_draggable.tsx index b0636b08a56346..9024aec17400c8 100644 --- a/x-pack/plugins/siem/public/components/ml/entity_draggable.tsx +++ b/x-pack/plugins/siem/public/common/components/ml/entity_draggable.tsx @@ -6,8 +6,8 @@ import React from 'react'; import { DraggableWrapper, DragEffects } from '../drag_and_drop/draggable_wrapper'; -import { IS_OPERATOR } from '../timeline/data_providers/data_provider'; -import { Provider } from '../timeline/data_providers/provider'; +import { IS_OPERATOR } from '../../../timelines/components/timeline/data_providers/data_provider'; +import { Provider } from '../../../timelines/components/timeline/data_providers/provider'; import { escapeDataProviderId } from '../drag_and_drop/helpers'; interface Props { diff --git a/x-pack/plugins/siem/public/components/ml/get_entries.test.ts b/x-pack/plugins/siem/public/common/components/ml/get_entries.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/get_entries.test.ts rename to x-pack/plugins/siem/public/common/components/ml/get_entries.test.ts diff --git a/x-pack/plugins/siem/public/components/ml/get_entries.ts b/x-pack/plugins/siem/public/common/components/ml/get_entries.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/get_entries.ts rename to x-pack/plugins/siem/public/common/components/ml/get_entries.ts diff --git a/x-pack/plugins/siem/public/components/ml/influencers/__snapshots__/create_influencers.test.tsx.snap b/x-pack/plugins/siem/public/common/components/ml/influencers/__snapshots__/create_influencers.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/ml/influencers/__snapshots__/create_influencers.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/ml/influencers/__snapshots__/create_influencers.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/ml/influencers/create_influencers.test.tsx b/x-pack/plugins/siem/public/common/components/ml/influencers/create_influencers.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/ml/influencers/create_influencers.test.tsx rename to x-pack/plugins/siem/public/common/components/ml/influencers/create_influencers.test.tsx diff --git a/x-pack/plugins/siem/public/components/ml/influencers/create_influencers.tsx b/x-pack/plugins/siem/public/common/components/ml/influencers/create_influencers.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/ml/influencers/create_influencers.tsx rename to x-pack/plugins/siem/public/common/components/ml/influencers/create_influencers.tsx diff --git a/x-pack/plugins/siem/public/components/ml/influencers/get_host_name_from_influencers.test.ts b/x-pack/plugins/siem/public/common/components/ml/influencers/get_host_name_from_influencers.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/influencers/get_host_name_from_influencers.test.ts rename to x-pack/plugins/siem/public/common/components/ml/influencers/get_host_name_from_influencers.test.ts diff --git a/x-pack/plugins/siem/public/components/ml/influencers/get_host_name_from_influencers.ts b/x-pack/plugins/siem/public/common/components/ml/influencers/get_host_name_from_influencers.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/influencers/get_host_name_from_influencers.ts rename to x-pack/plugins/siem/public/common/components/ml/influencers/get_host_name_from_influencers.ts diff --git a/x-pack/plugins/siem/public/components/ml/influencers/get_network_from_influencers.test.ts b/x-pack/plugins/siem/public/common/components/ml/influencers/get_network_from_influencers.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/influencers/get_network_from_influencers.test.ts rename to x-pack/plugins/siem/public/common/components/ml/influencers/get_network_from_influencers.test.ts diff --git a/x-pack/plugins/siem/public/components/ml/influencers/get_network_from_influencers.ts b/x-pack/plugins/siem/public/common/components/ml/influencers/get_network_from_influencers.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/influencers/get_network_from_influencers.ts rename to x-pack/plugins/siem/public/common/components/ml/influencers/get_network_from_influencers.ts diff --git a/x-pack/plugins/siem/public/components/ml/influencers/host_to_influencers.test.ts b/x-pack/plugins/siem/public/common/components/ml/influencers/host_to_influencers.test.ts similarity index 95% rename from x-pack/plugins/siem/public/components/ml/influencers/host_to_influencers.test.ts rename to x-pack/plugins/siem/public/common/components/ml/influencers/host_to_influencers.test.ts index 47a1fd52e947f9..8e67168b6acd46 100644 --- a/x-pack/plugins/siem/public/components/ml/influencers/host_to_influencers.test.ts +++ b/x-pack/plugins/siem/public/common/components/ml/influencers/host_to_influencers.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HostItem } from '../../../graphql/types'; +import { HostItem } from '../../../../graphql/types'; import { InfluencerInput } from '../types'; import { hostToInfluencers } from './host_to_influencers'; diff --git a/x-pack/plugins/siem/public/components/ml/influencers/host_to_influencers.ts b/x-pack/plugins/siem/public/common/components/ml/influencers/host_to_influencers.ts similarity index 92% rename from x-pack/plugins/siem/public/components/ml/influencers/host_to_influencers.ts rename to x-pack/plugins/siem/public/common/components/ml/influencers/host_to_influencers.ts index 69d1b6e26ac724..ae7698a1bac882 100644 --- a/x-pack/plugins/siem/public/components/ml/influencers/host_to_influencers.ts +++ b/x-pack/plugins/siem/public/common/components/ml/influencers/host_to_influencers.ts @@ -5,7 +5,7 @@ */ import { InfluencerInput } from '../types'; -import { HostItem } from '../../../graphql/types'; +import { HostItem } from '../../../../graphql/types'; export const hostToInfluencers = (hostItem: HostItem): InfluencerInput[] | null => { if (hostItem.host != null && hostItem.host.name != null) { diff --git a/x-pack/plugins/siem/public/components/ml/influencers/network_to_influencers.test.ts b/x-pack/plugins/siem/public/common/components/ml/influencers/network_to_influencers.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/influencers/network_to_influencers.test.ts rename to x-pack/plugins/siem/public/common/components/ml/influencers/network_to_influencers.test.ts diff --git a/x-pack/plugins/siem/public/components/ml/influencers/network_to_influencers.ts b/x-pack/plugins/siem/public/common/components/ml/influencers/network_to_influencers.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/influencers/network_to_influencers.ts rename to x-pack/plugins/siem/public/common/components/ml/influencers/network_to_influencers.ts diff --git a/x-pack/plugins/siem/public/components/ml/links/create_explorer_link.test.ts b/x-pack/plugins/siem/public/common/components/ml/links/create_explorer_link.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/links/create_explorer_link.test.ts rename to x-pack/plugins/siem/public/common/components/ml/links/create_explorer_link.test.ts diff --git a/x-pack/plugins/siem/public/components/ml/links/create_explorer_link.ts b/x-pack/plugins/siem/public/common/components/ml/links/create_explorer_link.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/links/create_explorer_link.ts rename to x-pack/plugins/siem/public/common/components/ml/links/create_explorer_link.ts diff --git a/x-pack/plugins/siem/public/components/ml/links/create_series_link.test.ts b/x-pack/plugins/siem/public/common/components/ml/links/create_series_link.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/links/create_series_link.test.ts rename to x-pack/plugins/siem/public/common/components/ml/links/create_series_link.test.ts diff --git a/x-pack/plugins/siem/public/components/ml/links/create_series_link.ts b/x-pack/plugins/siem/public/common/components/ml/links/create_series_link.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/links/create_series_link.ts rename to x-pack/plugins/siem/public/common/components/ml/links/create_series_link.ts diff --git a/x-pack/plugins/siem/public/components/ml/mock.ts b/x-pack/plugins/siem/public/common/components/ml/mock.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/mock.ts rename to x-pack/plugins/siem/public/common/components/ml/mock.ts diff --git a/x-pack/plugins/siem/public/components/ml/permissions/ml_capabilities_provider.tsx b/x-pack/plugins/siem/public/common/components/ml/permissions/ml_capabilities_provider.tsx similarity index 88% rename from x-pack/plugins/siem/public/components/ml/permissions/ml_capabilities_provider.tsx rename to x-pack/plugins/siem/public/common/components/ml/permissions/ml_capabilities_provider.tsx index eee44abb44204d..1d5c1b36e22af2 100644 --- a/x-pack/plugins/siem/public/components/ml/permissions/ml_capabilities_provider.tsx +++ b/x-pack/plugins/siem/public/common/components/ml/permissions/ml_capabilities_provider.tsx @@ -6,14 +6,14 @@ import React, { useState, useEffect } from 'react'; -import { MlCapabilities } from '../types'; +import { MlCapabilitiesResponse } from '../../../../../../ml/public'; +import { emptyMlCapabilities } from '../../../../../common/machine_learning/empty_ml_capabilities'; import { getMlCapabilities } from '../api/get_ml_capabilities'; -import { emptyMlCapabilities } from '../empty_ml_capabilities'; import { errorToToaster, useStateToaster } from '../../toasters'; import * as i18n from './translations'; -interface MlCapabilitiesProvider extends MlCapabilities { +interface MlCapabilitiesProvider extends MlCapabilitiesResponse { capabilitiesFetched: boolean; } diff --git a/x-pack/plugins/siem/public/components/ml/permissions/translations.ts b/x-pack/plugins/siem/public/common/components/ml/permissions/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/permissions/translations.ts rename to x-pack/plugins/siem/public/common/components/ml/permissions/translations.ts diff --git a/x-pack/plugins/siem/public/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap b/x-pack/plugins/siem/public/common/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/ml/score/__snapshots__/anomaly_score.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/ml/score/__snapshots__/anomaly_scores.test.tsx.snap b/x-pack/plugins/siem/public/common/components/ml/score/__snapshots__/anomaly_scores.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/ml/score/__snapshots__/anomaly_scores.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/ml/score/__snapshots__/anomaly_scores.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/ml/score/__snapshots__/create_descriptions_list.test.tsx.snap b/x-pack/plugins/siem/public/common/components/ml/score/__snapshots__/create_descriptions_list.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/ml/score/__snapshots__/create_descriptions_list.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/ml/score/__snapshots__/create_descriptions_list.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/ml/score/__snapshots__/draggable_score.test.tsx.snap b/x-pack/plugins/siem/public/common/components/ml/score/__snapshots__/draggable_score.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/ml/score/__snapshots__/draggable_score.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/ml/score/__snapshots__/draggable_score.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/ml/score/anomaly_score.test.tsx b/x-pack/plugins/siem/public/common/components/ml/score/anomaly_score.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/ml/score/anomaly_score.test.tsx rename to x-pack/plugins/siem/public/common/components/ml/score/anomaly_score.test.tsx diff --git a/x-pack/plugins/siem/public/components/ml/score/anomaly_score.tsx b/x-pack/plugins/siem/public/common/components/ml/score/anomaly_score.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/ml/score/anomaly_score.tsx rename to x-pack/plugins/siem/public/common/components/ml/score/anomaly_score.tsx diff --git a/x-pack/plugins/siem/public/components/ml/score/anomaly_scores.test.tsx b/x-pack/plugins/siem/public/common/components/ml/score/anomaly_scores.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/ml/score/anomaly_scores.test.tsx rename to x-pack/plugins/siem/public/common/components/ml/score/anomaly_scores.test.tsx diff --git a/x-pack/plugins/siem/public/components/ml/score/anomaly_scores.tsx b/x-pack/plugins/siem/public/common/components/ml/score/anomaly_scores.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/ml/score/anomaly_scores.tsx rename to x-pack/plugins/siem/public/common/components/ml/score/anomaly_scores.tsx diff --git a/x-pack/plugins/siem/public/components/ml/score/create_description_list.tsx b/x-pack/plugins/siem/public/common/components/ml/score/create_description_list.tsx similarity index 97% rename from x-pack/plugins/siem/public/components/ml/score/create_description_list.tsx rename to x-pack/plugins/siem/public/common/components/ml/score/create_description_list.tsx index e7615bf3b89ba8..0651bc5874860b 100644 --- a/x-pack/plugins/siem/public/components/ml/score/create_description_list.tsx +++ b/x-pack/plugins/siem/public/common/components/ml/score/create_description_list.tsx @@ -8,7 +8,7 @@ import { EuiText, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic import React from 'react'; import styled from 'styled-components'; -import { DescriptionList } from '../../../../common/utility_types'; +import { DescriptionList } from '../../../../../common/utility_types'; import { Anomaly, NarrowDateRange } from '../types'; import { getScoreString } from './score_health'; import { PreferenceFormattedDate } from '../../formatted_date'; diff --git a/x-pack/plugins/siem/public/components/ml/score/create_descriptions_list.test.tsx b/x-pack/plugins/siem/public/common/components/ml/score/create_descriptions_list.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/ml/score/create_descriptions_list.test.tsx rename to x-pack/plugins/siem/public/common/components/ml/score/create_descriptions_list.test.tsx diff --git a/x-pack/plugins/siem/public/components/ml/score/create_entities_from_score.test.ts b/x-pack/plugins/siem/public/common/components/ml/score/create_entities_from_score.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/score/create_entities_from_score.test.ts rename to x-pack/plugins/siem/public/common/components/ml/score/create_entities_from_score.test.ts diff --git a/x-pack/plugins/siem/public/components/ml/score/create_entities_from_score.ts b/x-pack/plugins/siem/public/common/components/ml/score/create_entities_from_score.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/score/create_entities_from_score.ts rename to x-pack/plugins/siem/public/common/components/ml/score/create_entities_from_score.ts diff --git a/x-pack/plugins/siem/public/components/ml/score/draggable_score.test.tsx b/x-pack/plugins/siem/public/common/components/ml/score/draggable_score.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/ml/score/draggable_score.test.tsx rename to x-pack/plugins/siem/public/common/components/ml/score/draggable_score.test.tsx diff --git a/x-pack/plugins/siem/public/components/ml/score/draggable_score.tsx b/x-pack/plugins/siem/public/common/components/ml/score/draggable_score.tsx similarity index 89% rename from x-pack/plugins/siem/public/components/ml/score/draggable_score.tsx rename to x-pack/plugins/siem/public/common/components/ml/score/draggable_score.tsx index 732eaf4bc5e788..c849476f0c3db7 100644 --- a/x-pack/plugins/siem/public/components/ml/score/draggable_score.tsx +++ b/x-pack/plugins/siem/public/common/components/ml/score/draggable_score.tsx @@ -7,8 +7,8 @@ import React from 'react'; import { DraggableWrapper, DragEffects } from '../../drag_and_drop/draggable_wrapper'; import { Anomaly } from '../types'; -import { IS_OPERATOR } from '../../timeline/data_providers/data_provider'; -import { Provider } from '../../timeline/data_providers/provider'; +import { IS_OPERATOR } from '../../../../timelines/components/timeline/data_providers/data_provider'; +import { Provider } from '../../../../timelines/components/timeline/data_providers/provider'; import { Spacer } from '../../page'; import { getScoreString } from './score_health'; diff --git a/x-pack/plugins/siem/public/components/ml/score/get_score_string.test.ts b/x-pack/plugins/siem/public/common/components/ml/score/get_score_string.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/score/get_score_string.test.ts rename to x-pack/plugins/siem/public/common/components/ml/score/get_score_string.test.ts diff --git a/x-pack/plugins/siem/public/components/ml/score/get_top_severity.test.ts b/x-pack/plugins/siem/public/common/components/ml/score/get_top_severity.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/score/get_top_severity.test.ts rename to x-pack/plugins/siem/public/common/components/ml/score/get_top_severity.test.ts diff --git a/x-pack/plugins/siem/public/components/ml/score/get_top_severity.ts b/x-pack/plugins/siem/public/common/components/ml/score/get_top_severity.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/score/get_top_severity.ts rename to x-pack/plugins/siem/public/common/components/ml/score/get_top_severity.ts diff --git a/x-pack/plugins/siem/public/components/ml/score/score_health.tsx b/x-pack/plugins/siem/public/common/components/ml/score/score_health.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/ml/score/score_health.tsx rename to x-pack/plugins/siem/public/common/components/ml/score/score_health.tsx diff --git a/x-pack/plugins/siem/public/components/ml/score/score_interval_to_datetime.test.ts b/x-pack/plugins/siem/public/common/components/ml/score/score_interval_to_datetime.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/score/score_interval_to_datetime.test.ts rename to x-pack/plugins/siem/public/common/components/ml/score/score_interval_to_datetime.test.ts diff --git a/x-pack/plugins/siem/public/components/ml/score/score_interval_to_datetime.ts b/x-pack/plugins/siem/public/common/components/ml/score/score_interval_to_datetime.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/score/score_interval_to_datetime.ts rename to x-pack/plugins/siem/public/common/components/ml/score/score_interval_to_datetime.ts diff --git a/x-pack/plugins/siem/public/components/ml/score/translations.ts b/x-pack/plugins/siem/public/common/components/ml/score/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/score/translations.ts rename to x-pack/plugins/siem/public/common/components/ml/score/translations.ts diff --git a/x-pack/plugins/siem/public/components/ml/tables/anomalies_host_table.tsx b/x-pack/plugins/siem/public/common/components/ml/tables/anomalies_host_table.tsx similarity index 96% rename from x-pack/plugins/siem/public/components/ml/tables/anomalies_host_table.tsx rename to x-pack/plugins/siem/public/common/components/ml/tables/anomalies_host_table.tsx index 16bde076ef7636..d6e343265b6e74 100644 --- a/x-pack/plugins/siem/public/components/ml/tables/anomalies_host_table.tsx +++ b/x-pack/plugins/siem/public/common/components/ml/tables/anomalies_host_table.tsx @@ -9,13 +9,13 @@ import React from 'react'; import { useAnomaliesTableData } from '../anomaly/use_anomalies_table_data'; import { HeaderSection } from '../../header_section'; +import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions'; import * as i18n from './translations'; import { getAnomaliesHostTableColumnsCurated } from './get_anomalies_host_table_columns'; import { convertAnomaliesToHosts } from './convert_anomalies_to_hosts'; import { Loader } from '../../loader'; import { getIntervalFromAnomalies } from '../anomaly/get_interval_from_anomalies'; import { AnomaliesHostTableProps } from '../types'; -import { hasMlUserPermissions } from '../permissions/has_ml_user_permissions'; import { useMlCapabilities } from '../../ml_popover/hooks/use_ml_capabilities'; import { BasicTable } from './basic_table'; import { hostEquality } from './host_equality'; diff --git a/x-pack/plugins/siem/public/components/ml/tables/anomalies_network_table.tsx b/x-pack/plugins/siem/public/common/components/ml/tables/anomalies_network_table.tsx similarity index 96% rename from x-pack/plugins/siem/public/components/ml/tables/anomalies_network_table.tsx rename to x-pack/plugins/siem/public/common/components/ml/tables/anomalies_network_table.tsx index bba6355f0b8b97..c7a49202bf239c 100644 --- a/x-pack/plugins/siem/public/components/ml/tables/anomalies_network_table.tsx +++ b/x-pack/plugins/siem/public/common/components/ml/tables/anomalies_network_table.tsx @@ -8,13 +8,13 @@ import React from 'react'; import { useAnomaliesTableData } from '../anomaly/use_anomalies_table_data'; import { HeaderSection } from '../../header_section'; +import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions'; import * as i18n from './translations'; import { convertAnomaliesToNetwork } from './convert_anomalies_to_network'; import { Loader } from '../../loader'; import { AnomaliesNetworkTableProps } from '../types'; import { getAnomaliesNetworkTableColumnsCurated } from './get_anomalies_network_table_columns'; import { useMlCapabilities } from '../../ml_popover/hooks/use_ml_capabilities'; -import { hasMlUserPermissions } from '../permissions/has_ml_user_permissions'; import { BasicTable } from './basic_table'; import { networkEquality } from './network_equality'; import { getCriteriaFromNetworkType } from '../criteria/get_criteria_from_network_type'; diff --git a/x-pack/plugins/siem/public/components/ml/tables/basic_table.tsx b/x-pack/plugins/siem/public/common/components/ml/tables/basic_table.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/ml/tables/basic_table.tsx rename to x-pack/plugins/siem/public/common/components/ml/tables/basic_table.tsx diff --git a/x-pack/plugins/siem/public/components/ml/tables/convert_anomalies_to_hosts.test.ts b/x-pack/plugins/siem/public/common/components/ml/tables/convert_anomalies_to_hosts.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/tables/convert_anomalies_to_hosts.test.ts rename to x-pack/plugins/siem/public/common/components/ml/tables/convert_anomalies_to_hosts.test.ts diff --git a/x-pack/plugins/siem/public/components/ml/tables/convert_anomalies_to_hosts.ts b/x-pack/plugins/siem/public/common/components/ml/tables/convert_anomalies_to_hosts.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/tables/convert_anomalies_to_hosts.ts rename to x-pack/plugins/siem/public/common/components/ml/tables/convert_anomalies_to_hosts.ts diff --git a/x-pack/plugins/siem/public/components/ml/tables/convert_anomalies_to_network.test.ts b/x-pack/plugins/siem/public/common/components/ml/tables/convert_anomalies_to_network.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/tables/convert_anomalies_to_network.test.ts rename to x-pack/plugins/siem/public/common/components/ml/tables/convert_anomalies_to_network.test.ts diff --git a/x-pack/plugins/siem/public/components/ml/tables/convert_anomalies_to_network.ts b/x-pack/plugins/siem/public/common/components/ml/tables/convert_anomalies_to_network.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/tables/convert_anomalies_to_network.ts rename to x-pack/plugins/siem/public/common/components/ml/tables/convert_anomalies_to_network.ts diff --git a/x-pack/plugins/siem/public/components/ml/tables/create_compound_key.test.ts b/x-pack/plugins/siem/public/common/components/ml/tables/create_compound_key.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/tables/create_compound_key.test.ts rename to x-pack/plugins/siem/public/common/components/ml/tables/create_compound_key.test.ts diff --git a/x-pack/plugins/siem/public/components/ml/tables/create_compound_key.ts b/x-pack/plugins/siem/public/common/components/ml/tables/create_compound_key.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/tables/create_compound_key.ts rename to x-pack/plugins/siem/public/common/components/ml/tables/create_compound_key.ts diff --git a/x-pack/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.test.tsx b/x-pack/plugins/siem/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx similarity index 98% rename from x-pack/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.test.tsx rename to x-pack/plugins/siem/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx index 80980756d21305..ae9133f23c0b29 100644 --- a/x-pack/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.test.tsx +++ b/x-pack/plugins/siem/public/common/components/ml/tables/get_anomalies_host_table_columns.test.tsx @@ -5,7 +5,7 @@ */ import { getAnomaliesHostTableColumnsCurated } from './get_anomalies_host_table_columns'; -import { HostsType } from '../../../store/hosts/model'; +import { HostsType } from '../../../../hosts/store/model'; import * as i18n from './translations'; import { AnomaliesByHost, Anomaly } from '../types'; import { Columns } from '../../paginated_table'; diff --git a/x-pack/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.tsx b/x-pack/plugins/siem/public/common/components/ml/tables/get_anomalies_host_table_columns.tsx similarity index 98% rename from x-pack/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.tsx rename to x-pack/plugins/siem/public/common/components/ml/tables/get_anomalies_host_table_columns.tsx index 4e6484c23613f1..4697eb1fbf86e0 100644 --- a/x-pack/plugins/siem/public/components/ml/tables/get_anomalies_host_table_columns.tsx +++ b/x-pack/plugins/siem/public/common/components/ml/tables/get_anomalies_host_table_columns.tsx @@ -19,7 +19,7 @@ import * as i18n from './translations'; import { getEntries } from '../get_entries'; import { DraggableScore } from '../score/draggable_score'; import { createExplorerLink } from '../links/create_explorer_link'; -import { HostsType } from '../../../store/hosts/model'; +import { HostsType } from '../../../../hosts/store/model'; import { escapeDataProviderId } from '../../drag_and_drop/helpers'; import { FormattedRelativePreferenceDate } from '../../formatted_date'; diff --git a/x-pack/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.test.tsx b/x-pack/plugins/siem/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx similarity index 98% rename from x-pack/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.test.tsx rename to x-pack/plugins/siem/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx index 658444bfeda5c1..37cb99b33c793a 100644 --- a/x-pack/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.test.tsx +++ b/x-pack/plugins/siem/public/common/components/ml/tables/get_anomalies_network_table_columns.test.tsx @@ -5,7 +5,7 @@ */ import { getAnomaliesNetworkTableColumnsCurated } from './get_anomalies_network_table_columns'; -import { NetworkType } from '../../../store/network/model'; +import { NetworkType } from '../../../../network/store/model'; import * as i18n from './translations'; import { AnomaliesByNetwork, Anomaly } from '../types'; import { Columns } from '../../paginated_table'; diff --git a/x-pack/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.tsx b/x-pack/plugins/siem/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx similarity index 97% rename from x-pack/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.tsx rename to x-pack/plugins/siem/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx index f6a493f80eb78f..f09a4d0779ac7a 100644 --- a/x-pack/plugins/siem/public/components/ml/tables/get_anomalies_network_table_columns.tsx +++ b/x-pack/plugins/siem/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx @@ -21,9 +21,9 @@ import { getEntries } from '../get_entries'; import { DraggableScore } from '../score/draggable_score'; import { createExplorerLink } from '../links/create_explorer_link'; import { FormattedRelativePreferenceDate } from '../../formatted_date'; -import { NetworkType } from '../../../store/network/model'; +import { NetworkType } from '../../../../network/store/model'; import { escapeDataProviderId } from '../../drag_and_drop/helpers'; -import { FlowTarget } from '../../../graphql/types'; +import { FlowTarget } from '../../../../graphql/types'; export const getAnomaliesNetworkTableColumns = ( startDate: number, diff --git a/x-pack/plugins/siem/public/components/ml/tables/host_equality.test.ts b/x-pack/plugins/siem/public/common/components/ml/tables/host_equality.test.ts similarity index 98% rename from x-pack/plugins/siem/public/components/ml/tables/host_equality.test.ts rename to x-pack/plugins/siem/public/common/components/ml/tables/host_equality.test.ts index c5054d40f94ab0..89b87f95e5159d 100644 --- a/x-pack/plugins/siem/public/components/ml/tables/host_equality.test.ts +++ b/x-pack/plugins/siem/public/common/components/ml/tables/host_equality.test.ts @@ -6,7 +6,7 @@ import { hostEquality } from './host_equality'; import { AnomaliesHostTableProps } from '../types'; -import { HostsType } from '../../../store/hosts/model'; +import { HostsType } from '../../../../hosts/store/model'; describe('host_equality', () => { test('it returns true if start and end date are equal', () => { diff --git a/x-pack/plugins/siem/public/components/ml/tables/host_equality.ts b/x-pack/plugins/siem/public/common/components/ml/tables/host_equality.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/tables/host_equality.ts rename to x-pack/plugins/siem/public/common/components/ml/tables/host_equality.ts diff --git a/x-pack/plugins/siem/public/components/ml/tables/network_equality.test.ts b/x-pack/plugins/siem/public/common/components/ml/tables/network_equality.test.ts similarity index 97% rename from x-pack/plugins/siem/public/components/ml/tables/network_equality.test.ts rename to x-pack/plugins/siem/public/common/components/ml/tables/network_equality.test.ts index cb053d1a43d2f9..8b3e30c3290314 100644 --- a/x-pack/plugins/siem/public/components/ml/tables/network_equality.test.ts +++ b/x-pack/plugins/siem/public/common/components/ml/tables/network_equality.test.ts @@ -6,8 +6,8 @@ import { networkEquality } from './network_equality'; import { AnomaliesNetworkTableProps } from '../types'; -import { NetworkType } from '../../../store/network/model'; -import { FlowTarget } from '../../../graphql/types'; +import { NetworkType } from '../../../../network/store/model'; +import { FlowTarget } from '../../../../graphql/types'; describe('network_equality', () => { test('it returns true if start and end date are equal', () => { diff --git a/x-pack/plugins/siem/public/components/ml/tables/network_equality.ts b/x-pack/plugins/siem/public/common/components/ml/tables/network_equality.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/tables/network_equality.ts rename to x-pack/plugins/siem/public/common/components/ml/tables/network_equality.ts diff --git a/x-pack/plugins/siem/public/components/ml/tables/translations.ts b/x-pack/plugins/siem/public/common/components/ml/tables/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/tables/translations.ts rename to x-pack/plugins/siem/public/common/components/ml/tables/translations.ts diff --git a/x-pack/plugins/siem/public/components/ml/types.test.ts b/x-pack/plugins/siem/public/common/components/ml/types.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml/types.test.ts rename to x-pack/plugins/siem/public/common/components/ml/types.test.ts diff --git a/x-pack/plugins/siem/public/common/components/ml/types.ts b/x-pack/plugins/siem/public/common/components/ml/types.ts new file mode 100644 index 00000000000000..13bceaa473a84f --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/ml/types.ts @@ -0,0 +1,107 @@ +/* + * 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 { Influencer } from '../../../../../ml/public'; + +import { HostsType } from '../../../hosts/store/model'; +import { NetworkType } from '../../../network/store/model'; +import { FlowTarget } from '../../../graphql/types'; + +export interface Source { + job_id: string; + result_type: string; + probability: number; + multi_bucket_impact: number; + record_score: number; + initial_record_score: number; + bucket_span: number; + detector_index: number; + is_interim: boolean; + timestamp: number; + by_field_name: string; + by_field_value: string; + partition_field_name: string; + partition_field_value: string; + function: string; + function_description: string; + typical: number[]; + actual: number[]; + influencers: Influencer[]; +} + +export interface CriteriaFields { + fieldName: string; + fieldValue: string; +} + +export interface InfluencerInput { + fieldName: string; + fieldValue: string; +} + +export interface Anomaly { + detectorIndex: number; + entityName: string; + entityValue: string; + influencers?: Array>; + jobId: string; + rowId: string; + severity: number; + time: number; + source: Source; +} + +export interface Anomalies { + anomalies: Anomaly[]; + interval: string; +} + +export type NarrowDateRange = (score: Anomaly, interval: string) => void; + +export interface AnomaliesByHost { + hostName: string; + anomaly: Anomaly; +} + +export type DestinationOrSource = 'source.ip' | 'destination.ip'; + +export interface AnomaliesByNetwork { + type: DestinationOrSource; + ip: string; + anomaly: Anomaly; +} + +export interface HostOrNetworkProps { + startDate: number; + endDate: number; + narrowDateRange: NarrowDateRange; + skip: boolean; +} + +export type AnomaliesHostTableProps = HostOrNetworkProps & { + hostName?: string; + type: HostsType; +}; + +export type AnomaliesNetworkTableProps = HostOrNetworkProps & { + ip?: string; + type: NetworkType; + flowTarget?: FlowTarget; +}; + +const sourceOrDestination = ['source.ip', 'destination.ip']; + +export const isDestinationOrSource = (value: string | null): value is DestinationOrSource => + value != null && sourceOrDestination.includes(value); + +export interface MlError { + msg: string; + response: string; + statusCode: number; + path?: string; + query?: {}; + body?: string; +} diff --git a/x-pack/plugins/siem/public/components/ml_popover/__mocks__/api.tsx b/x-pack/plugins/siem/public/common/components/ml_popover/__mocks__/api.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/ml_popover/__mocks__/api.tsx rename to x-pack/plugins/siem/public/common/components/ml_popover/__mocks__/api.tsx diff --git a/x-pack/plugins/siem/public/components/ml_popover/__snapshots__/popover_description.test.tsx.snap b/x-pack/plugins/siem/public/common/components/ml_popover/__snapshots__/popover_description.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/ml_popover/__snapshots__/popover_description.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/ml_popover/__snapshots__/popover_description.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/ml_popover/__snapshots__/upgrade_contents.test.tsx.snap b/x-pack/plugins/siem/public/common/components/ml_popover/__snapshots__/upgrade_contents.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/ml_popover/__snapshots__/upgrade_contents.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/ml_popover/__snapshots__/upgrade_contents.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/ml_popover/api.tsx b/x-pack/plugins/siem/public/common/components/ml_popover/api.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/ml_popover/api.tsx rename to x-pack/plugins/siem/public/common/components/ml_popover/api.tsx diff --git a/x-pack/plugins/siem/public/common/components/ml_popover/helpers.test.tsx b/x-pack/plugins/siem/public/common/components/ml_popover/helpers.test.tsx new file mode 100644 index 00000000000000..0b8da6be57e1b5 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/ml_popover/helpers.test.tsx @@ -0,0 +1,54 @@ +/* + * 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 { mockSiemJobs } from './__mocks__/api'; +import { filterJobs, getStablePatternTitles, searchFilter } from './helpers'; + +describe('helpers', () => { + describe('filterJobs', () => { + test('returns all jobs when no filter is suplied', () => { + const filteredJobs = filterJobs({ + jobs: mockSiemJobs, + selectedGroups: [], + showCustomJobs: false, + showElasticJobs: false, + filterQuery: '', + }); + expect(filteredJobs.length).toEqual(3); + }); + }); + + describe('searchFilter', () => { + test('returns all jobs when nullfilterQuery is provided', () => { + const jobsToDisplay = searchFilter(mockSiemJobs); + expect(jobsToDisplay.length).toEqual(mockSiemJobs.length); + }); + + test('returns correct DisplayJobs when filterQuery matches job.id', () => { + const jobsToDisplay = searchFilter(mockSiemJobs, 'rare_process'); + expect(jobsToDisplay.length).toEqual(2); + }); + + test('returns correct DisplayJobs when filterQuery matches job.description', () => { + const jobsToDisplay = searchFilter(mockSiemJobs, 'Detect unusually'); + expect(jobsToDisplay.length).toEqual(2); + }); + }); + + describe('getStablePatternTitles', () => { + test('it returns a stable reference two times in a row with standard strings', () => { + const one = getStablePatternTitles(['a', 'b', 'c']); + const two = getStablePatternTitles(['a', 'b', 'c']); + expect(one).toBe(two); + }); + + test('it returns a stable reference two times in a row with strings interchanged', () => { + const one = getStablePatternTitles(['c', 'b', 'a']); + const two = getStablePatternTitles(['a', 'b', 'c']); + expect(one).toBe(two); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/components/ml_popover/helpers.tsx b/x-pack/plugins/siem/public/common/components/ml_popover/helpers.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/ml_popover/helpers.tsx rename to x-pack/plugins/siem/public/common/components/ml_popover/helpers.tsx diff --git a/x-pack/plugins/siem/public/components/ml_popover/hooks/translations.ts b/x-pack/plugins/siem/public/common/components/ml_popover/hooks/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml_popover/hooks/translations.ts rename to x-pack/plugins/siem/public/common/components/ml_popover/hooks/translations.ts diff --git a/x-pack/plugins/siem/public/components/ml_popover/hooks/use_ml_capabilities.tsx b/x-pack/plugins/siem/public/common/components/ml_popover/hooks/use_ml_capabilities.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/ml_popover/hooks/use_ml_capabilities.tsx rename to x-pack/plugins/siem/public/common/components/ml_popover/hooks/use_ml_capabilities.tsx diff --git a/x-pack/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs.tsx b/x-pack/plugins/siem/public/common/components/ml_popover/hooks/use_siem_jobs.tsx similarity index 94% rename from x-pack/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs.tsx rename to x-pack/plugins/siem/public/common/components/ml_popover/hooks/use_siem_jobs.tsx index 7bcbf4afa10cc4..a84d88782926c3 100644 --- a/x-pack/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs.tsx +++ b/x-pack/plugins/siem/public/common/components/ml_popover/hooks/use_siem_jobs.tsx @@ -6,10 +6,10 @@ import { useEffect, useState } from 'react'; -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { DEFAULT_INDEX_KEY } from '../../../../../common/constants'; import { checkRecognizer, getJobsSummary, getModules } from '../api'; import { SiemJob } from '../types'; -import { hasMlUserPermissions } from '../../ml/permissions/has_ml_user_permissions'; +import { hasMlUserPermissions } from '../../../../../common/machine_learning/has_ml_user_permissions'; import { errorToToaster, useStateToaster } from '../../toasters'; import { useUiSetting$ } from '../../../lib/kibana'; diff --git a/x-pack/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs_helpers.test.tsx b/x-pack/plugins/siem/public/common/components/ml_popover/hooks/use_siem_jobs_helpers.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs_helpers.test.tsx rename to x-pack/plugins/siem/public/common/components/ml_popover/hooks/use_siem_jobs_helpers.test.tsx diff --git a/x-pack/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs_helpers.tsx b/x-pack/plugins/siem/public/common/components/ml_popover/hooks/use_siem_jobs_helpers.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/ml_popover/hooks/use_siem_jobs_helpers.tsx rename to x-pack/plugins/siem/public/common/components/ml_popover/hooks/use_siem_jobs_helpers.tsx diff --git a/x-pack/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/job_switch.test.tsx.snap b/x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/__snapshots__/job_switch.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/job_switch.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/__snapshots__/job_switch.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/jobs_table.test.tsx.snap b/x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/__snapshots__/jobs_table.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/jobs_table.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/__snapshots__/jobs_table.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/showing_count.test.tsx.snap b/x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/__snapshots__/showing_count.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/ml_popover/jobs_table/__snapshots__/showing_count.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/__snapshots__/showing_count.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap b/x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/filters/__snapshots__/groups_filter_popover.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap b/x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/filters/__snapshots__/jobs_table_filters.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/groups_filter_popover.test.tsx b/x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/filters/groups_filter_popover.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/groups_filter_popover.test.tsx rename to x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/filters/groups_filter_popover.test.tsx diff --git a/x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx b/x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx rename to x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/filters/groups_filter_popover.tsx diff --git a/x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx b/x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx rename to x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.test.tsx diff --git a/x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx b/x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx similarity index 97% rename from x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx rename to x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx index 551ed5f08bd76e..8cb35fc6891858 100644 --- a/x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx +++ b/x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/filters/jobs_table_filters.tsx @@ -14,7 +14,7 @@ import { // @ts-ignore no-exported-member EuiSearchBar, } from '@elastic/eui'; -import { EuiSearchBarQuery } from '../../../open_timeline/types'; +import { EuiSearchBarQuery } from '../../../../../timelines/components/open_timeline/types'; import * as i18n from './translations'; import { JobsFilters, SiemJob } from '../../types'; import { GroupsFilterPopover } from './groups_filter_popover'; diff --git a/x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/toggle_selected_group.test.tsx b/x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/filters/toggle_selected_group.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/toggle_selected_group.test.tsx rename to x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/filters/toggle_selected_group.test.tsx diff --git a/x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/toggle_selected_group.tsx b/x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/filters/toggle_selected_group.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/toggle_selected_group.tsx rename to x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/filters/toggle_selected_group.tsx diff --git a/x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/translations.ts b/x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/filters/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml_popover/jobs_table/filters/translations.ts rename to x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/filters/translations.ts diff --git a/x-pack/plugins/siem/public/components/ml_popover/jobs_table/job_switch.test.tsx b/x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/job_switch.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/ml_popover/jobs_table/job_switch.test.tsx rename to x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/job_switch.test.tsx diff --git a/x-pack/plugins/siem/public/components/ml_popover/jobs_table/job_switch.tsx b/x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/job_switch.tsx similarity index 97% rename from x-pack/plugins/siem/public/components/ml_popover/jobs_table/job_switch.tsx rename to x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/job_switch.tsx index e7b14f2e80bf24..732f5cc062bf14 100644 --- a/x-pack/plugins/siem/public/components/ml_popover/jobs_table/job_switch.tsx +++ b/x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/job_switch.tsx @@ -11,7 +11,7 @@ import { isJobLoading, isJobFailed, isJobStarted, -} from '../../../../common/detection_engine/ml_helpers'; +} from '../../../../../common/machine_learning/helpers'; import { SiemJob } from '../types'; const StaticSwitch = styled(EuiSwitch)` diff --git a/x-pack/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.test.tsx b/x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/jobs_table.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.test.tsx rename to x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/jobs_table.test.tsx diff --git a/x-pack/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.tsx b/x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/jobs_table.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/ml_popover/jobs_table/jobs_table.tsx rename to x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/jobs_table.tsx diff --git a/x-pack/plugins/siem/public/components/ml_popover/jobs_table/showing_count.test.tsx b/x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/showing_count.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/ml_popover/jobs_table/showing_count.test.tsx rename to x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/showing_count.test.tsx diff --git a/x-pack/plugins/siem/public/components/ml_popover/jobs_table/showing_count.tsx b/x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/showing_count.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/ml_popover/jobs_table/showing_count.tsx rename to x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/showing_count.tsx diff --git a/x-pack/plugins/siem/public/components/ml_popover/jobs_table/translations.ts b/x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml_popover/jobs_table/translations.ts rename to x-pack/plugins/siem/public/common/components/ml_popover/jobs_table/translations.ts diff --git a/x-pack/plugins/siem/public/components/ml_popover/ml_modules.tsx b/x-pack/plugins/siem/public/common/components/ml_popover/ml_modules.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/ml_popover/ml_modules.tsx rename to x-pack/plugins/siem/public/common/components/ml_popover/ml_modules.tsx diff --git a/x-pack/plugins/siem/public/components/ml_popover/ml_popover.test.tsx b/x-pack/plugins/siem/public/common/components/ml_popover/ml_popover.test.tsx similarity index 88% rename from x-pack/plugins/siem/public/components/ml_popover/ml_popover.test.tsx rename to x-pack/plugins/siem/public/common/components/ml_popover/ml_popover.test.tsx index 3c93e1c195cd7a..cf4ac87bdb5e77 100644 --- a/x-pack/plugins/siem/public/components/ml_popover/ml_popover.test.tsx +++ b/x-pack/plugins/siem/public/common/components/ml_popover/ml_popover.test.tsx @@ -11,10 +11,6 @@ import { MlPopover } from './ml_popover'; jest.mock('../../lib/kibana'); -jest.mock('../ml/permissions/has_ml_admin_permissions', () => ({ - hasMlAdminPermissions: () => true, -})); - describe('MlPopover', () => { test('shows upgrade popover on mouse click', () => { const wrapper = mountWithIntl(); diff --git a/x-pack/plugins/siem/public/components/ml_popover/ml_popover.tsx b/x-pack/plugins/siem/public/common/components/ml_popover/ml_popover.tsx similarity index 98% rename from x-pack/plugins/siem/public/components/ml_popover/ml_popover.tsx rename to x-pack/plugins/siem/public/common/components/ml_popover/ml_popover.tsx index 6ea5cba4b37e43..292b5286e9f3e9 100644 --- a/x-pack/plugins/siem/public/components/ml_popover/ml_popover.tsx +++ b/x-pack/plugins/siem/public/common/components/ml_popover/ml_popover.tsx @@ -12,7 +12,7 @@ import styled from 'styled-components'; import { useKibana } from '../../lib/kibana'; import { METRIC_TYPE, TELEMETRY_EVENT, track } from '../../lib/telemetry'; -import { hasMlAdminPermissions } from '../ml/permissions/has_ml_admin_permissions'; +import { hasMlAdminPermissions } from '../../../../common/machine_learning/has_ml_admin_permissions'; import { errorToToaster, useStateToaster, ActionToaster } from '../toasters'; import { setupMlJob, startDatafeeds, stopDatafeeds } from './api'; import { filterJobs } from './helpers'; diff --git a/x-pack/plugins/siem/public/components/ml_popover/popover_description.test.tsx b/x-pack/plugins/siem/public/common/components/ml_popover/popover_description.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/ml_popover/popover_description.test.tsx rename to x-pack/plugins/siem/public/common/components/ml_popover/popover_description.test.tsx diff --git a/x-pack/plugins/siem/public/components/ml_popover/popover_description.tsx b/x-pack/plugins/siem/public/common/components/ml_popover/popover_description.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/ml_popover/popover_description.tsx rename to x-pack/plugins/siem/public/common/components/ml_popover/popover_description.tsx diff --git a/x-pack/plugins/siem/public/components/ml_popover/translations.ts b/x-pack/plugins/siem/public/common/components/ml_popover/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ml_popover/translations.ts rename to x-pack/plugins/siem/public/common/components/ml_popover/translations.ts diff --git a/x-pack/plugins/siem/public/common/components/ml_popover/types.ts b/x-pack/plugins/siem/public/common/components/ml_popover/types.ts new file mode 100644 index 00000000000000..f39daa0b9a7fbe --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/ml_popover/types.ts @@ -0,0 +1,203 @@ +/* + * 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 { AuditMessageBase } from '../../../../../ml/public'; +import { MlError } from '../ml/types'; + +export interface Group { + id: string; + jobIds: string[]; + calendarIds: string[]; +} + +export interface CheckRecognizerProps { + indexPatternName: string[]; + signal: AbortSignal; +} + +export interface RecognizerModule { + id: string; + title: string; + query: Record; + description: string; + logo: { + icon: string; + }; +} + +export interface GetModulesProps { + moduleId?: string; + signal: AbortSignal; +} + +export interface Module { + id: string; + title: string; + description: string; + type: string; + logoFile: string; + defaultIndexPattern: string; + query: Record; + jobs: ModuleJob[]; + datafeeds: ModuleDatafeed[]; + kibana: object; +} + +/** + * Representation of an ML Job as returned from `the ml/modules/get_module` API + */ +export interface ModuleJob { + id: string; + config: { + groups: string[]; + description: string; + analysis_config: { + bucket_span: string; + summary_count_field_name?: string; + detectors: Detector[]; + influencers: string[]; + }; + analysis_limits: { + model_memory_limit: string; + }; + data_description: { + time_field: string; + time_format?: string; + }; + model_plot_config?: { + enabled: boolean; + }; + custom_settings: { + created_by: string; + custom_urls: CustomURL[]; + }; + job_type: string; + }; +} + +// TODO: Speak to ML team about why the get_module API will sometimes return indexes and other times indices +// See mockGetModuleResponse for examples +export interface ModuleDatafeed { + id: string; + config: { + job_id: string; + indexes?: string[]; + indices?: string[]; + query: Record; + }; +} + +export interface MlSetupArgs { + configTemplate: string; + indexPatternName: string; + jobIdErrorFilter: string[]; + groups: string[]; + prefix?: string; +} + +/** + * Representation of an ML Job as returned from the `ml/jobs/jobs_summary` API + */ +export interface JobSummary { + auditMessage?: AuditMessageBase; + datafeedId: string; + datafeedIndices: string[]; + datafeedState: string; + description: string; + earliestTimestampMs?: number; + latestResultsTimestampMs?: number; + groups: string[]; + hasDatafeed: boolean; + id: string; + isSingleMetricViewerJob: boolean; + jobState: string; + latestTimestampMs?: number; + memory_status: string; + nodeName?: string; + processed_record_count: number; +} + +export interface Detector { + detector_description: string; + function: string; + by_field_name: string; + partition_field_name?: string; +} + +export interface CustomURL { + url_name: string; + url_value: string; +} + +/** + * Representation of an ML Job as used by the SIEM App -- a composition of ModuleJob and JobSummary + * that includes necessary metadata like moduleName, defaultIndexPattern, etc. + */ +export interface SiemJob extends JobSummary { + moduleId: string; + defaultIndexPattern: string; + isCompatible: boolean; + isInstalled: boolean; + isElasticJob: boolean; +} + +export interface AugmentedSiemJobFields { + moduleId: string; + defaultIndexPattern: string; + isCompatible: boolean; + isElasticJob: boolean; +} + +export interface SetupMlResponseJob { + id: string; + success: boolean; + error?: MlError; +} + +export interface SetupMlResponseDatafeed { + id: string; + success: boolean; + started: boolean; + error?: MlError; +} + +export interface SetupMlResponse { + jobs: SetupMlResponseJob[]; + datafeeds: SetupMlResponseDatafeed[]; + kibana: {}; +} + +export interface StartDatafeedResponse { + [key: string]: { + started: boolean; + error?: string; + }; +} + +export interface ErrorResponse { + statusCode?: number; + error?: string; + message?: string; +} + +export interface StopDatafeedResponse { + [key: string]: { + stopped: boolean; + }; +} + +export interface CloseJobsResponse { + [key: string]: { + closed: boolean; + }; +} + +export interface JobsFilters { + filterQuery: string; + showCustomJobs: boolean; + showElasticJobs: boolean; + selectedGroups: string[]; +} diff --git a/x-pack/plugins/siem/public/components/ml_popover/upgrade_contents.test.tsx b/x-pack/plugins/siem/public/common/components/ml_popover/upgrade_contents.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/ml_popover/upgrade_contents.test.tsx rename to x-pack/plugins/siem/public/common/components/ml_popover/upgrade_contents.test.tsx diff --git a/x-pack/plugins/siem/public/components/ml_popover/upgrade_contents.tsx b/x-pack/plugins/siem/public/common/components/ml_popover/upgrade_contents.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/ml_popover/upgrade_contents.tsx rename to x-pack/plugins/siem/public/common/components/ml_popover/upgrade_contents.tsx diff --git a/x-pack/plugins/siem/public/components/navigation/breadcrumbs/index.test.ts b/x-pack/plugins/siem/public/common/components/navigation/breadcrumbs/index.test.ts similarity index 98% rename from x-pack/plugins/siem/public/components/navigation/breadcrumbs/index.test.ts rename to x-pack/plugins/siem/public/common/components/navigation/breadcrumbs/index.test.ts index 2acae92c390dde..9ec2542c52db23 100644 --- a/x-pack/plugins/siem/public/components/navigation/breadcrumbs/index.test.ts +++ b/x-pack/plugins/siem/public/common/components/navigation/breadcrumbs/index.test.ts @@ -7,10 +7,10 @@ import '../../../mock/match_media'; import { encodeIpv6 } from '../../../lib/helpers'; import { getBreadcrumbsForRoute, setBreadcrumbs } from '.'; -import { HostsTableType } from '../../../store/hosts/model'; +import { HostsTableType } from '../../../../hosts/store/model'; import { RouteSpyState, SiemRouteType } from '../../../utils/route/types'; import { TabNavigationProps } from '../tab_navigation/types'; -import { NetworkRouteType } from '../../../pages/network/navigation/types'; +import { NetworkRouteType } from '../../../../network/pages/navigation/types'; const setBreadcrumbsMock = jest.fn(); const chromeMock = { diff --git a/x-pack/plugins/siem/public/common/components/navigation/breadcrumbs/index.ts b/x-pack/plugins/siem/public/common/components/navigation/breadcrumbs/index.ts new file mode 100644 index 00000000000000..16ae1b1e096ca8 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/navigation/breadcrumbs/index.ts @@ -0,0 +1,171 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr, omit } from 'lodash/fp'; + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ChromeBreadcrumb } from '../../../../../../../../src/core/public'; +import { APP_NAME } from '../../../../../common/constants'; +import { StartServices } from '../../../../plugin'; +import { getBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../../hosts/pages/details/utils'; +import { getBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../../network/pages/ip_details'; +import { getBreadcrumbs as getCaseDetailsBreadcrumbs } from '../../../../cases/pages/utils'; +import { getBreadcrumbs as getDetectionRulesBreadcrumbs } from '../../../../alerts/pages/detection_engine/rules/utils'; +import { getBreadcrumbs as getTimelinesBreadcrumbs } from '../../../../timelines/pages'; +import { SiemPageName } from '../../../../app/types'; +import { + RouteSpyState, + HostRouteSpyState, + NetworkRouteSpyState, + TimelineRouteSpyState, +} from '../../../utils/route/types'; +import { getOverviewUrl } from '../../link_to'; + +import { TabNavigationProps } from '../tab_navigation/types'; +import { getSearch } from '../helpers'; +import { SearchNavTab } from '../types'; + +export const setBreadcrumbs = ( + spyState: RouteSpyState & TabNavigationProps, + chrome: StartServices['chrome'] +) => { + const breadcrumbs = getBreadcrumbsForRoute(spyState); + if (breadcrumbs) { + chrome.setBreadcrumbs(breadcrumbs); + } +}; + +export const siemRootBreadcrumb: ChromeBreadcrumb[] = [ + { + text: APP_NAME, + href: getOverviewUrl(), + }, +]; + +const isNetworkRoutes = (spyState: RouteSpyState): spyState is NetworkRouteSpyState => + spyState != null && spyState.pageName === SiemPageName.network; + +const isHostsRoutes = (spyState: RouteSpyState): spyState is HostRouteSpyState => + spyState != null && spyState.pageName === SiemPageName.hosts; + +const isTimelinesRoutes = (spyState: RouteSpyState): spyState is TimelineRouteSpyState => + spyState != null && spyState.pageName === SiemPageName.timelines; + +const isCaseRoutes = (spyState: RouteSpyState): spyState is RouteSpyState => + spyState != null && spyState.pageName === SiemPageName.case; + +const isDetectionsRoutes = (spyState: RouteSpyState) => + spyState != null && spyState.pageName === SiemPageName.detections; + +export const getBreadcrumbsForRoute = ( + object: RouteSpyState & TabNavigationProps +): ChromeBreadcrumb[] | null => { + const spyState: RouteSpyState = omit('navTabs', object); + if (isHostsRoutes(spyState) && object.navTabs) { + const tempNav: SearchNavTab = { urlKey: 'host', isDetailPage: false }; + let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; + if (spyState.tabName != null) { + urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; + } + return [ + ...siemRootBreadcrumb, + ...getHostDetailsBreadcrumbs( + spyState, + urlStateKeys.reduce( + (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], + [] + ) + ), + ]; + } + if (isNetworkRoutes(spyState) && object.navTabs) { + const tempNav: SearchNavTab = { urlKey: 'network', isDetailPage: false }; + let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; + if (spyState.tabName != null) { + urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; + } + return [ + ...siemRootBreadcrumb, + ...getIPDetailsBreadcrumbs( + spyState, + urlStateKeys.reduce( + (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], + [] + ) + ), + ]; + } + if (isDetectionsRoutes(spyState) && object.navTabs) { + const tempNav: SearchNavTab = { urlKey: 'detections', isDetailPage: false }; + let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; + if (spyState.tabName != null) { + urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; + } + + return [ + ...siemRootBreadcrumb, + ...getDetectionRulesBreadcrumbs( + spyState, + urlStateKeys.reduce( + (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], + [] + ) + ), + ]; + } + if (isCaseRoutes(spyState) && object.navTabs) { + const tempNav: SearchNavTab = { urlKey: 'case', isDetailPage: false }; + let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; + if (spyState.tabName != null) { + urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; + } + + return [ + ...siemRootBreadcrumb, + ...getCaseDetailsBreadcrumbs( + spyState, + urlStateKeys.reduce( + (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], + [] + ) + ), + ]; + } + if (isTimelinesRoutes(spyState) && object.navTabs) { + const tempNav: SearchNavTab = { urlKey: 'timeline', isDetailPage: false }; + let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; + if (spyState.tabName != null) { + urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; + } + + return [ + ...siemRootBreadcrumb, + ...getTimelinesBreadcrumbs( + spyState, + urlStateKeys.reduce( + (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], + [] + ) + ), + ]; + } + if ( + spyState != null && + object.navTabs && + spyState.pageName && + object.navTabs[spyState.pageName] + ) { + return [ + ...siemRootBreadcrumb, + { + text: object.navTabs[spyState.pageName].name, + href: '', + }, + ]; + } + + return null; +}; diff --git a/x-pack/plugins/siem/public/common/components/navigation/helpers.ts b/x-pack/plugins/siem/public/common/components/navigation/helpers.ts new file mode 100644 index 00000000000000..8f5a3ac63fa1ad --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/navigation/helpers.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty } from 'lodash/fp'; +import { Location } from 'history'; + +import { UrlInputsModel } from '../../store/inputs/model'; +import { TimelineUrl } from '../../../timelines/store/timeline/model'; +import { CONSTANTS } from '../url_state/constants'; +import { URL_STATE_KEYS, KeyUrlState, UrlState } from '../url_state/types'; +import { + replaceQueryStringInLocation, + replaceStateKeyInQueryString, + getQueryStringFromLocation, +} from '../url_state/helpers'; +import { Query, Filter } from '../../../../../../../src/plugins/data/public'; + +import { SearchNavTab } from './types'; + +export const getSearch = (tab: SearchNavTab, urlState: UrlState): string => { + if (tab && tab.urlKey != null && URL_STATE_KEYS[tab.urlKey] != null) { + return URL_STATE_KEYS[tab.urlKey].reduce( + (myLocation: Location, urlKey: KeyUrlState) => { + let urlStateToReplace: UrlInputsModel | Query | Filter[] | TimelineUrl | string = ''; + + if (urlKey === CONSTANTS.appQuery && urlState.query != null) { + if (urlState.query.query === '') { + urlStateToReplace = ''; + } else { + urlStateToReplace = urlState.query; + } + } else if (urlKey === CONSTANTS.filters && urlState.filters != null) { + if (isEmpty(urlState.filters)) { + urlStateToReplace = ''; + } else { + urlStateToReplace = urlState.filters; + } + } else if (urlKey === CONSTANTS.timerange) { + urlStateToReplace = urlState[CONSTANTS.timerange]; + } else if (urlKey === CONSTANTS.timeline && urlState[CONSTANTS.timeline] != null) { + const timeline = urlState[CONSTANTS.timeline]; + if (timeline.id === '') { + urlStateToReplace = ''; + } else { + urlStateToReplace = timeline; + } + } + return replaceQueryStringInLocation( + myLocation, + replaceStateKeyInQueryString( + urlKey, + urlStateToReplace + )(getQueryStringFromLocation(myLocation.search)) + ); + }, + { + pathname: '', + hash: '', + search: '', + state: '', + } + ).search; + } + return ''; +}; diff --git a/x-pack/plugins/siem/public/common/components/navigation/index.test.tsx b/x-pack/plugins/siem/public/common/components/navigation/index.test.tsx new file mode 100644 index 00000000000000..ff3f9ba0694a9c --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/navigation/index.test.tsx @@ -0,0 +1,238 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; + +import { CONSTANTS } from '../url_state/constants'; +import { SiemNavigationComponent } from './'; +import { setBreadcrumbs } from './breadcrumbs'; +import { navTabs } from '../../../app/home/home_navigations'; +import { HostsTableType } from '../../../hosts/store/model'; +import { RouteSpyState } from '../../utils/route/types'; +import { SiemNavigationProps, SiemNavigationComponentProps } from './types'; + +jest.mock('./breadcrumbs', () => ({ + setBreadcrumbs: jest.fn(), +})); + +describe('SIEM Navigation', () => { + const mockProps: SiemNavigationComponentProps & SiemNavigationProps & RouteSpyState = { + pageName: 'hosts', + pathName: '/hosts', + detailName: undefined, + search: '', + tabName: HostsTableType.authentications, + navTabs, + urlState: { + [CONSTANTS.timerange]: { + global: { + [CONSTANTS.timerange]: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + linkTo: ['timeline'], + }, + timeline: { + [CONSTANTS.timerange]: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + linkTo: ['global'], + }, + }, + [CONSTANTS.appQuery]: { query: '', language: 'kuery' }, + [CONSTANTS.filters]: [], + [CONSTANTS.timeline]: { + id: '', + isOpen: false, + }, + }, + }; + const wrapper = mount(); + test('it calls setBreadcrumbs with correct path on mount', () => { + expect(setBreadcrumbs).toHaveBeenNthCalledWith( + 1, + { + detailName: undefined, + navTabs: { + case: { + disabled: false, + href: '#/link-to/case', + id: 'case', + name: 'Cases', + urlKey: 'case', + }, + detections: { + disabled: false, + href: '#/link-to/detections', + id: 'detections', + name: 'Detections', + urlKey: 'detections', + }, + hosts: { + disabled: false, + href: '#/link-to/hosts', + id: 'hosts', + name: 'Hosts', + urlKey: 'host', + }, + network: { + disabled: false, + href: '#/link-to/network', + id: 'network', + name: 'Network', + urlKey: 'network', + }, + overview: { + disabled: false, + href: '#/link-to/overview', + id: 'overview', + name: 'Overview', + urlKey: 'overview', + }, + timelines: { + disabled: false, + href: '#/link-to/timelines', + id: 'timelines', + name: 'Timelines', + urlKey: 'timeline', + }, + }, + pageName: 'hosts', + pathName: '/hosts', + search: '', + tabName: 'authentications', + query: { query: '', language: 'kuery' }, + filters: [], + savedQuery: undefined, + timeline: { + id: '', + isOpen: false, + }, + timerange: { + global: { + linkTo: ['timeline'], + timerange: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + }, + timeline: { + linkTo: ['global'], + timerange: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + }, + }, + }, + undefined + ); + }); + test('it calls setBreadcrumbs with correct path on update', () => { + wrapper.setProps({ + pageName: 'network', + pathName: '/network', + tabName: undefined, + }); + wrapper.update(); + expect(setBreadcrumbs).toHaveBeenNthCalledWith( + 1, + { + detailName: undefined, + filters: [], + navTabs: { + case: { + disabled: false, + href: '#/link-to/case', + id: 'case', + name: 'Cases', + urlKey: 'case', + }, + detections: { + disabled: false, + href: '#/link-to/detections', + id: 'detections', + name: 'Detections', + urlKey: 'detections', + }, + hosts: { + disabled: false, + href: '#/link-to/hosts', + id: 'hosts', + name: 'Hosts', + urlKey: 'host', + }, + network: { + disabled: false, + href: '#/link-to/network', + id: 'network', + name: 'Network', + urlKey: 'network', + }, + overview: { + disabled: false, + href: '#/link-to/overview', + id: 'overview', + name: 'Overview', + urlKey: 'overview', + }, + timelines: { + disabled: false, + href: '#/link-to/timelines', + id: 'timelines', + name: 'Timelines', + urlKey: 'timeline', + }, + }, + pageName: 'hosts', + pathName: '/hosts', + query: { language: 'kuery', query: '' }, + savedQuery: undefined, + search: '', + state: undefined, + tabName: 'authentications', + timeline: { id: '', isOpen: false }, + timerange: { + global: { + linkTo: ['timeline'], + timerange: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + }, + timeline: { + linkTo: ['global'], + timerange: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + }, + }, + }, + undefined + ); + }); +}); diff --git a/x-pack/plugins/siem/public/components/navigation/index.tsx b/x-pack/plugins/siem/public/common/components/navigation/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/navigation/index.tsx rename to x-pack/plugins/siem/public/common/components/navigation/index.tsx diff --git a/x-pack/plugins/siem/public/common/components/navigation/tab_navigation/index.test.tsx b/x-pack/plugins/siem/public/common/components/navigation/tab_navigation/index.test.tsx new file mode 100644 index 00000000000000..b9572caece94fa --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/navigation/tab_navigation/index.test.tsx @@ -0,0 +1,157 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; + +import { navTabs } from '../../../../app/home/home_navigations'; +import { SiemPageName } from '../../../../app/types'; +import { navTabsHostDetails } from '../../../../hosts/pages/details/nav_tabs'; +import { HostsTableType } from '../../../../hosts/store/model'; +import { RouteSpyState } from '../../../utils/route/types'; +import { CONSTANTS } from '../../url_state/constants'; +import { TabNavigationComponent } from './'; +import { TabNavigationProps } from './types'; + +describe('Tab Navigation', () => { + const pageName = SiemPageName.hosts; + const hostName = 'siem-window'; + const tabName = HostsTableType.authentications; + const pathName = `/${pageName}/${hostName}/${tabName}`; + + describe('Page Navigation', () => { + const mockProps: TabNavigationProps & RouteSpyState = { + pageName, + pathName, + detailName: undefined, + search: '', + tabName, + navTabs, + [CONSTANTS.timerange]: { + global: { + [CONSTANTS.timerange]: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + linkTo: ['timeline'], + }, + timeline: { + [CONSTANTS.timerange]: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + linkTo: ['global'], + }, + }, + [CONSTANTS.appQuery]: { query: 'host.name:"siem-es"', language: 'kuery' }, + [CONSTANTS.filters]: [], + [CONSTANTS.timeline]: { + id: '', + isOpen: false, + }, + }; + test('it mounts with correct tab highlighted', () => { + const wrapper = mount(); + const hostsTab = wrapper.find('EuiTab[data-test-subj="navigation-hosts"]'); + expect(hostsTab.prop('isSelected')).toBeTruthy(); + }); + test('it changes active tab when nav changes by props', () => { + const wrapper = mount(); + const networkTab = () => wrapper.find('EuiTab[data-test-subj="navigation-network"]').first(); + expect(networkTab().prop('isSelected')).toBeFalsy(); + wrapper.setProps({ + pageName: 'network', + pathName: '/network', + tabName: undefined, + }); + wrapper.update(); + expect(networkTab().prop('isSelected')).toBeTruthy(); + }); + test('it carries the url state in the link', () => { + const wrapper = mount(); + const firstTab = wrapper.find('EuiTab[data-test-subj="navigation-network"]'); + expect(firstTab.props().href).toBe( + "#/link-to/network?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))" + ); + }); + }); + + describe('Table Navigation', () => { + const mockHasMlUserPermissions = true; + const mockProps: TabNavigationProps & RouteSpyState = { + pageName: 'hosts', + pathName: '/hosts', + detailName: undefined, + search: '', + tabName: HostsTableType.authentications, + navTabs: navTabsHostDetails(hostName, mockHasMlUserPermissions), + [CONSTANTS.timerange]: { + global: { + [CONSTANTS.timerange]: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + linkTo: ['timeline'], + }, + timeline: { + [CONSTANTS.timerange]: { + from: 1558048243696, + fromStr: 'now-24h', + kind: 'relative', + to: 1558134643697, + toStr: 'now', + }, + linkTo: ['global'], + }, + }, + [CONSTANTS.appQuery]: { query: 'host.name:"siem-es"', language: 'kuery' }, + [CONSTANTS.filters]: [], + [CONSTANTS.timeline]: { + id: '', + isOpen: false, + }, + }; + test('it mounts with correct tab highlighted', () => { + const wrapper = mount(); + const tableNavigationTab = wrapper.find( + `EuiTab[data-test-subj="navigation-${HostsTableType.authentications}"]` + ); + + expect(tableNavigationTab.prop('isSelected')).toBeTruthy(); + }); + test('it changes active tab when nav changes by props', () => { + const wrapper = mount(); + const tableNavigationTab = () => + wrapper.find(`[data-test-subj="navigation-${HostsTableType.events}"]`).first(); + expect(tableNavigationTab().prop('isSelected')).toBeFalsy(); + wrapper.setProps({ + pageName: SiemPageName.hosts, + pathName: `/${SiemPageName.hosts}`, + tabName: HostsTableType.events, + }); + wrapper.update(); + expect(tableNavigationTab().prop('isSelected')).toBeTruthy(); + }); + test('it carries the url state in the link', () => { + const wrapper = mount(); + const firstTab = wrapper.find( + `EuiTab[data-test-subj="navigation-${HostsTableType.authentications}"]` + ); + expect(firstTab.props().href).toBe( + `#/${pageName}/${hostName}/${HostsTableType.authentications}?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))` + ); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/components/navigation/tab_navigation/index.tsx b/x-pack/plugins/siem/public/common/components/navigation/tab_navigation/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/navigation/tab_navigation/index.tsx rename to x-pack/plugins/siem/public/common/components/navigation/tab_navigation/index.tsx diff --git a/x-pack/plugins/siem/public/common/components/navigation/tab_navigation/types.ts b/x-pack/plugins/siem/public/common/components/navigation/tab_navigation/types.ts new file mode 100644 index 00000000000000..a283691cfe0df4 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/navigation/tab_navigation/types.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UrlInputsModel } from '../../../store/inputs/model'; +import { CONSTANTS } from '../../url_state/constants'; +import { HostsTableType } from '../../../../hosts/store/model'; +import { TimelineUrl } from '../../../../timelines/store/timeline/model'; +import { Filter, Query } from '../../../../../../../../src/plugins/data/public'; + +import { SiemNavigationProps } from '../types'; + +export interface TabNavigationProps extends SiemNavigationProps { + pathName: string; + pageName: string; + tabName: HostsTableType | undefined; + [CONSTANTS.appQuery]?: Query; + [CONSTANTS.filters]?: Filter[]; + [CONSTANTS.savedQuery]?: string; + [CONSTANTS.timerange]: UrlInputsModel; + [CONSTANTS.timeline]: TimelineUrl; +} + +export interface TabNavigationItemProps { + href: string; + hrefWithSearch: string; + id: string; + disabled: boolean; + name: string; + isSelected: boolean; +} diff --git a/x-pack/plugins/siem/public/common/components/navigation/types.ts b/x-pack/plugins/siem/public/common/components/navigation/types.ts new file mode 100644 index 00000000000000..f0256813c29e78 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/navigation/types.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Filter, Query } from '../../../../../../../src/plugins/data/public'; +import { HostsTableType } from '../../../hosts/store/model'; +import { UrlInputsModel } from '../../store/inputs/model'; +import { TimelineUrl } from '../../../timelines/store/timeline/model'; +import { CONSTANTS, UrlStateType } from '../url_state/constants'; + +export interface SiemNavigationProps { + display?: 'default' | 'condensed'; + navTabs: Record; +} + +export interface SiemNavigationComponentProps { + pathName: string; + pageName: string; + tabName: HostsTableType | undefined; + urlState: { + [CONSTANTS.appQuery]?: Query; + [CONSTANTS.filters]?: Filter[]; + [CONSTANTS.savedQuery]?: string; + [CONSTANTS.timerange]: UrlInputsModel; + [CONSTANTS.timeline]: TimelineUrl; + }; +} + +export type SearchNavTab = NavTab | { urlKey: UrlStateType; isDetailPage: boolean }; + +export interface NavTab { + id: string; + name: string; + href: string; + disabled: boolean; + urlKey: UrlStateType; + isDetailPage?: boolean; +} diff --git a/x-pack/plugins/siem/public/components/navigation/use_get_url_search.tsx b/x-pack/plugins/siem/public/common/components/navigation/use_get_url_search.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/navigation/use_get_url_search.tsx rename to x-pack/plugins/siem/public/common/components/navigation/use_get_url_search.tsx diff --git a/x-pack/plugins/siem/public/common/components/news_feed/helpers.test.ts b/x-pack/plugins/siem/public/common/components/news_feed/helpers.test.ts new file mode 100644 index 00000000000000..cdd04b50a6d506 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/news_feed/helpers.test.ts @@ -0,0 +1,492 @@ +/* + * 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 { NEWS_FEED_URL_SETTING_DEFAULT } from '../../../../common/constants'; +import { KibanaServices } from '../../lib/kibana'; +import { rawNewsApiResponse } from '../../mock/news'; +import { rawNewsJSON } from '../../mock/raw_news'; + +import { + fetchNews, + getLocale, + getNewsFeedUrl, + getNewsItemsFromApiResponse, + removeSnapshotFromVersion, + showNewsItem, +} from './helpers'; +import { NewsItem, RawNewsApiResponse } from './types'; + +jest.mock('../../lib/kibana'); + +describe('helpers', () => { + describe('removeSnapshotFromVersion', () => { + test('it should remove an all-caps `-SNAPSHOT`', () => { + const version = '8.0.0-SNAPSHOT'; + + expect(removeSnapshotFromVersion(version)).toEqual('8.0.0'); + }); + + test('it should remove a mixed-case `-SnApShoT`', () => { + const version = '8.0.0-SnApShoT'; + + expect(removeSnapshotFromVersion(version)).toEqual('8.0.0'); + }); + + test('it should remove all occurrences of `-SNAPSHOT`, regardless of where they appear in the version', () => { + const version = '-SNAPSHOT8.0.0-SNAPSHOT'; + + expect(removeSnapshotFromVersion(version)).toEqual('8.0.0'); + }); + + test('it should NOT transform a version when it does not contain a `-SNAPSHOT`', () => { + const version = '8.0.0'; + + expect(removeSnapshotFromVersion(version)).toEqual('8.0.0'); + }); + + test('it should NOT transform a version if it omits the dash in `SNAPSHOT`', () => { + const version = '8.0.0SNAPSHOT'; + + expect(removeSnapshotFromVersion(version)).toEqual('8.0.0SNAPSHOT'); + }); + + test('it should NOT transform a version if has only a partial `-SNAPSHOT`', () => { + const version = '8.0.0-SNAP'; + + expect(removeSnapshotFromVersion(version)).toEqual('8.0.0-SNAP'); + }); + + test('it should NOT transform an undefined version', () => { + const version = undefined; + + expect(removeSnapshotFromVersion(version)).toBeUndefined(); + }); + + test('it should NOT transform an empty version', () => { + const version = ''; + + expect(removeSnapshotFromVersion(version)).toEqual(''); + }); + }); + + describe('getNewsFeedUrl', () => { + const getKibanaVersion = () => '8.0.0'; + + test('it combines the (default) base URL from settings and the Kibana version to return the expected URL', () => { + expect( + getNewsFeedUrl({ newsFeedUrlSetting: NEWS_FEED_URL_SETTING_DEFAULT, getKibanaVersion }) + ).toEqual('https://feeds.elastic.co/security-solution/v8.0.0.json'); + }); + + test('it combines a URL with extra whitespace and the Kibana version to return the expected URL', () => { + const withExtraWhitespace = ` ${NEWS_FEED_URL_SETTING_DEFAULT} `; + + expect(getNewsFeedUrl({ newsFeedUrlSetting: withExtraWhitespace, getKibanaVersion })).toEqual( + 'https://feeds.elastic.co/security-solution/v8.0.0.json' + ); + }); + + test('it combines a URL with a trailing slash and the Kibana version to return the expected URL', () => { + const withTrailingSlash = `${NEWS_FEED_URL_SETTING_DEFAULT}/`; + + expect(getNewsFeedUrl({ newsFeedUrlSetting: withTrailingSlash, getKibanaVersion })).toEqual( + 'https://feeds.elastic.co/security-solution/v8.0.0.json' + ); + }); + + test('it combines a URL with a trailing slash plus whitespace and the Kibana version to return the expected URL', () => { + const withTrailingSlashPlusWhitespace = ` ${NEWS_FEED_URL_SETTING_DEFAULT}/ `; + + expect( + getNewsFeedUrl({ newsFeedUrlSetting: withTrailingSlashPlusWhitespace, getKibanaVersion }) + ).toEqual('https://feeds.elastic.co/security-solution/v8.0.0.json'); + }); + + test('it combines a URL and a Kibana version with a `-SNAPSHOT` to return the expected URL', () => { + const getKibanaVersionWithSnapshot = () => '8.0.0-SNAPSHOT'; + + expect( + getNewsFeedUrl({ + newsFeedUrlSetting: NEWS_FEED_URL_SETTING_DEFAULT, + getKibanaVersion: getKibanaVersionWithSnapshot, + }) + ).toEqual('https://feeds.elastic.co/security-solution/v8.0.0.json'); + }); + }); + + describe('getLocale', () => { + const fallback = 'wowzers'; + + test('it returns language specified in the document', () => { + const lang = 'ja'; + + document.documentElement.lang = lang; + + expect(getLocale(fallback)).toEqual(lang); + }); + + test('it returns the fallback when the language in the document is an empty string', () => { + document.documentElement.lang = ''; + + expect(getLocale(fallback)).toEqual(fallback); + }); + }); + + describe('getNewsItemsFromApiResponse', () => { + const expectedNewsItems: NewsItem[] = [ + { + description: + "There's an awesome community of Elastic SIEM users out there. Join the discussion about configuring, learning, and using the Elastic SIEM app, and detecting threats!", + expireOn: expect.any(Date), + hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', + imageUrl: + 'https://aws1.discourse-cdn.com/elastic/original/3X/f/8/f8c3d0b9971cfcd0be349d973aa5799f71d280cc.png?blade=securitysolutionfeed', + linkUrl: 'https://discuss.elastic.co/c/siem?blade=securitysolutionfeed', + publishOn: expect.any(Date), + title: 'Got SIEM Questions?', + }, + { + description: + 'Elastic Security combines the threat hunting and analytics of Elastic SIEM with the prevention and response provided by Elastic Endpoint Security.', + expireOn: expect.any(Date), + hash: 'edcb2d396ffdd80bfd5a97fbc0dc9f4b73477f9be556863fe0a1caf086679420', + imageUrl: + 'https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1caa35177420c61b/5d0d0394d8ff351753cbf2c5/illustrated-screenshot-hero-siem.png?blade=securitysolutionfeed', + linkUrl: + 'https://www.elastic.co/blog/elastic-security-7-5-0-released?blade=securitysolutionfeed', + publishOn: expect.any(Date), + title: 'Elastic Security 7.5.0 released', + }, + { + description: + 'At Elastic, we’re bringing endpoint protection and SIEM together into the same experience to streamline how you secure your organization.', + expireOn: expect.any(Date), + hash: 'ec970adc85e9eede83f77e4cc6a6fea00cd7822cbe48a71dc2c5f1df10939196', + imageUrl: + 'https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd0eb8689eafe398a/5d970ecc1970e80e85277925/illustration-endpoint-hero.png?blade=securitysolutionfeed', + linkUrl: + 'https://www.elastic.co/webinars/elastic-endpoint-security-overview-security-starts-at-the-endpoint?blade=securitysolutionfeed', + publishOn: expect.any(Date), + title: 'Elastic Endpoint Security Overview Webinar', + }, + { + description: + 'For small businesses and homes, having access to effective security analytics can come at a high cost of either time or money. Well, until now!', + expireOn: expect.any(Date), + hash: 'aa243fd5845356a5ccd54a7a11b208ed307e0d88158873b1fcf7d1164b739bac', + imageUrl: + 'https://images.contentstack.io/v3/assets/bltefdd0b53724fa2ce/blt024c26b7636cb24f/5daf4e293a326d6df6c0e025/home-siem-blog-1-map.jpg?blade=securitysolutionfeed', + linkUrl: + 'https://www.elastic.co/blog/elastic-siem-for-small-business-and-home-1-getting-started?blade=securitysolutionfeed', + publishOn: expect.any(Date), + title: 'Trying Elastic SIEM at Home?', + }, + { + description: + 'Elastic is excited to announce the introduction of Elastic Endpoint Security, based on Elastic’s acquisition of Endgame, a pioneer and industry-recognized leader in endpoint threat prevention, detection, and response.', + expireOn: expect.any(Date), + hash: '3c64576c9749d33ff98726d641cdf2fb2bfde3dd9a6f99ff2573ac8d8c5b2c02', + imageUrl: + 'https://images.contentstack.io/v3/assets/bltefdd0b53724fa2ce/blt1f87637fb7870298/5d9fe27bf8ca980f8717f6f8/screenshot-resolver-trickbot-enrichments-showing-defender-shutdown-endgame-2-optimized.png?blade=securitysolutionfeed', + linkUrl: + 'https://www.elastic.co/blog/introducing-elastic-endpoint-security?blade=securitysolutionfeed', + publishOn: expect.any(Date), + title: 'Introducing Elastic Endpoint Security', + }, + { + description: + 'Elastic SIEM is powered by Elastic Common Schema. With ECS, analytics content such as dashboards, rules, and machine learning jobs can be applied more broadly, searches can be crafted more narrowly, and field names are easier to remember.', + expireOn: expect.any(Date), + hash: 'b8a0d3d21e9638bde891ab5eb32594b3d7a3daacc7f0900c6dd506d5d7b42410', + imageUrl: + 'https://images.contentstack.io/v3/assets/bltefdd0b53724fa2ce/blt71256f06dc672546/5c98d595975fd58f4d12646d/ecs-intro-dashboard-1360.jpg?blade=securitysolutionfeed', + linkUrl: + 'https://www.elastic.co/blog/introducing-the-elastic-common-schema?blade=securitysolutionfeed', + publishOn: expect.any(Date), + title: 'What is Elastic Common Schema (ECS)?', + }, + ]; + + test('it returns an empty collection of news items when the response is undefined', () => { + expect(getNewsItemsFromApiResponse(undefined)).toEqual([]); + }); + + test('it returns an empty collection of news items when the response is null', () => { + expect(getNewsItemsFromApiResponse(null)).toEqual([]); + }); + + test('it returns an empty collection of news items when the response items are undefined', () => { + expect(getNewsItemsFromApiResponse({ items: undefined })).toEqual([]); + }); + + test('it returns an empty collection of news items when the response items are null', () => { + expect(getNewsItemsFromApiResponse({ items: null })).toEqual([]); + }); + + test('it returns the expected news items when the browser language matches the i18n values in the response', () => { + const lang = 'en'; + + document.documentElement.lang = lang; + + expect(getNewsItemsFromApiResponse(rawNewsApiResponse)).toEqual(expectedNewsItems); + }); + + test('it returns the expected news items when an ALL CAPS the browser language matches the i18n values in the response', () => { + const allCapsLang = 'EN'; + + document.documentElement.lang = allCapsLang; + + expect(getNewsItemsFromApiResponse(rawNewsApiResponse)).toEqual(expectedNewsItems); + }); + + test('it returns the expected news items when the browser language does NOT match the i18n values in the response', () => { + const nonMatchingLang = 'ja'; + + document.documentElement.lang = nonMatchingLang; + + expect(getNewsItemsFromApiResponse(rawNewsApiResponse)).toEqual(expectedNewsItems); + }); + + test('it returns the expected news items when the browser language is an empty string', () => { + const emptyLang = ''; + + document.documentElement.lang = emptyLang; + + expect(getNewsItemsFromApiResponse(rawNewsApiResponse)).toEqual(expectedNewsItems); + }); + + test('it returns the expected news item when parsing a raw JSON response', () => { + const lang = 'en'; + + document.documentElement.lang = lang; + + expect(getNewsItemsFromApiResponse(JSON.parse(rawNewsJSON))).toEqual(expectedNewsItems); + }); + + describe('translated items', () => { + const translatedDescription = + 'Elastic SIEMユーザーの素晴らしいコミュニティがそこにあります。 Elastic SIEMアプリの設定、学習、使用、および脅威の検出に関するディスカッションに参加してください!'; + const translatedImageUrl = 'https://aws1.discourse-cdn.com/elastic/translated-image-url'; + const translatedLinkUrl = 'https://discuss.elastic.co/translated-link-url'; + const translatedTitle = 'SIEMに関する質問はありますか?'; + + const withNonDefaultTranslations: RawNewsApiResponse = { + items: [ + { + title: { en: 'Got SIEM Questions?', ja: translatedTitle }, + description: { + en: + "There's an awesome community of Elastic SIEM users out there. Join the discussion about configuring, learning, and using the Elastic SIEM app, and detecting threats!", + ja: translatedDescription, + }, + link_text: null, + link_url: { + en: 'https://discuss.elastic.co/c/siem?blade=securitysolutionfeed', + ja: translatedLinkUrl, + }, + languages: null, + badge: { en: '7.6' }, + image_url: { + en: + 'https://aws1.discourse-cdn.com/elastic/original/3X/f/8/f8c3d0b9971cfcd0be349d973aa5799f71d280cc.png?blade=securitysolutionfeed', + ja: translatedImageUrl, + }, + publish_on: new Date('2020-01-01T00:00:00'), + expire_on: new Date('2020-12-31T00:00:00'), + }, + ], + }; + + test('it returns a translated description when the browser language matches additional translated content', () => { + const lang = 'ja'; // an additional translation for this language is provided in the response + + document.documentElement.lang = lang; + + expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].description).toEqual( + translatedDescription + ); + }); + + test('it returns a translated imageUrl when the browser language matches additional translated content', () => { + const lang = 'ja'; // a translation for this language is provided in the response + + document.documentElement.lang = lang; + + expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].imageUrl).toEqual( + translatedImageUrl + ); + }); + + test('it returns a translated linkUrl when the browser language matches additional translated content', () => { + const lang = 'ja'; // a translation for this language is provided in the response + + document.documentElement.lang = lang; + + expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].linkUrl).toEqual( + translatedLinkUrl + ); + }); + + test('it returns a translated title when the browser language matches additional translated content', () => { + const lang = 'ja'; // a translation for this language is provided in the response + + document.documentElement.lang = lang; + + expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].title).toEqual( + translatedTitle + ); + }); + + test('it returns the default translated title when the browser language matches additional translated content', () => { + const lang = 'fr'; // no translation for this language + + document.documentElement.lang = lang; + + expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].title).toEqual( + 'Got SIEM Questions?' + ); + }); + + test('it returns the default translated title when the browser language is an empty string', () => { + const lang = ''; // just an empty string + + document.documentElement.lang = lang; + + expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].title).toEqual( + 'Got SIEM Questions?' + ); + }); + }); + + test('it generates a news item hash when an item does NOT include it', () => { + const lang = 'en'; + + const itemHasNoHash: RawNewsApiResponse = { + items: [ + { + title: { en: 'Got SIEM Questions?' }, + description: { + en: 'some description', + }, + link_text: null, + link_url: { en: 'https://example.com/link-url' }, + languages: null, + badge: { en: '7.6' }, + image_url: { + en: 'https://example.com/image-url', + }, + publish_on: new Date('2020-01-01T00:00:00'), + expire_on: new Date('2020-12-31T00:00:00'), + }, + ], + }; + + document.documentElement.lang = lang; + + expect(getNewsItemsFromApiResponse(itemHasNoHash)[0].hash.length).toBeGreaterThan(0); + }); + }); + + describe('fetchNews', () => { + const mockKibanaServices = KibanaServices.get as jest.Mock; + const fetchMock = jest.fn(); + mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } }); + + beforeEach(() => { + fetchMock.mockClear(); + fetchMock.mockResolvedValue(rawNewsApiResponse); + }); + + test('it returns the raw API response from the news feed', async () => { + const newsFeedUrl = 'https://feeds.elastic.co/security-solution/v8.0.0.json'; + expect(await fetchNews({ newsFeedUrl })).toEqual(rawNewsApiResponse); + }); + }); + + describe('showNewsItem', () => { + const MOCK_DATE_NOW = 1579848101395; // 2020-01-24T06:41:41.395Z + + let dateNowSpy: { mockRestore: () => void }; + + beforeAll(() => { + dateNowSpy = jest.spyOn(Date, 'now').mockImplementation(() => MOCK_DATE_NOW); + }); + + afterAll(() => { + dateNowSpy.mockRestore(); + }); + + test('it should return true when the article has already been published, and will expire in the future', () => { + const alreadyPublishedAndNotExpired: NewsItem = { + description: 'description', + expireOn: new Date(MOCK_DATE_NOW + 1000), + hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', + imageUrl: 'https://example.com', + linkUrl: 'https://example.com', + publishOn: new Date(MOCK_DATE_NOW - 1000), + title: 'Show this post', + }; + + expect(showNewsItem(alreadyPublishedAndNotExpired)).toEqual(true); + }); + + test('it should return false when the article was published exactly "now", and will expire in the future', () => { + const publishedJustNowAndNotExpired: NewsItem = { + description: 'description', + expireOn: new Date(MOCK_DATE_NOW + 1000), + hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', + imageUrl: 'https://example.com', + linkUrl: 'https://example.com', + publishOn: new Date(MOCK_DATE_NOW), + title: 'Do NOT show this post', + }; + + expect(showNewsItem(publishedJustNowAndNotExpired)).toEqual(false); + }); + + test('it should return false when the article has not been published yet, and has not expired yet', () => { + const notPublishedAndNotExpired: NewsItem = { + description: 'description', + expireOn: new Date(MOCK_DATE_NOW + 5000), + hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', + imageUrl: 'https://example.com', + linkUrl: 'https://example.com', + publishOn: new Date(MOCK_DATE_NOW + 1000), + title: 'Do NOT show this post', + }; + + expect(showNewsItem(notPublishedAndNotExpired)).toEqual(false); + }); + + test('it should return false when the article was published in the past, and will expire exactly now', () => { + const alreadyPublishedAndExpiredNow: NewsItem = { + description: 'description', + expireOn: new Date(MOCK_DATE_NOW), + hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', + imageUrl: 'https://example.com', + linkUrl: 'https://example.com', + publishOn: new Date(MOCK_DATE_NOW - 1000), + title: 'Do NOT show this post', + }; + + expect(showNewsItem(alreadyPublishedAndExpiredNow)).toEqual(false); + }); + + test('it should return false when the article was published in the past, and it already expired', () => { + const articleJustExpired: NewsItem = { + description: 'description', + expireOn: new Date(MOCK_DATE_NOW - 1000), + hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', + imageUrl: 'https://example.com', + linkUrl: 'https://example.com', + publishOn: new Date(MOCK_DATE_NOW - 5000), + title: 'Do NOT show this post', + }; + + expect(showNewsItem(articleJustExpired)).toEqual(false); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/components/news_feed/helpers.ts b/x-pack/plugins/siem/public/common/components/news_feed/helpers.ts similarity index 100% rename from x-pack/plugins/siem/public/components/news_feed/helpers.ts rename to x-pack/plugins/siem/public/common/components/news_feed/helpers.ts diff --git a/x-pack/plugins/siem/public/components/news_feed/index.tsx b/x-pack/plugins/siem/public/common/components/news_feed/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/news_feed/index.tsx rename to x-pack/plugins/siem/public/common/components/news_feed/index.tsx diff --git a/x-pack/plugins/siem/public/components/news_feed/news_feed.tsx b/x-pack/plugins/siem/public/common/components/news_feed/news_feed.tsx similarity index 86% rename from x-pack/plugins/siem/public/components/news_feed/news_feed.tsx rename to x-pack/plugins/siem/public/common/components/news_feed/news_feed.tsx index cd356212b44006..523273d1caf2e7 100644 --- a/x-pack/plugins/siem/public/components/news_feed/news_feed.tsx +++ b/x-pack/plugins/siem/public/common/components/news_feed/news_feed.tsx @@ -6,8 +6,8 @@ import React from 'react'; -import { LoadingPlaceholders } from '../page/overview/loading_placeholders'; -import { NEWS_FEED_TITLE } from '../../pages/overview/translations'; +import { LoadingPlaceholders } from '../../../overview/components/loading_placeholders'; +import { NEWS_FEED_TITLE } from '../../../overview/pages/translations'; import { SidebarHeader } from '../sidebar_header'; import { NoNews } from './no_news'; diff --git a/x-pack/plugins/siem/public/components/news_feed/news_link/index.tsx b/x-pack/plugins/siem/public/common/components/news_feed/news_link/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/news_feed/news_link/index.tsx rename to x-pack/plugins/siem/public/common/components/news_feed/news_link/index.tsx diff --git a/x-pack/plugins/siem/public/components/news_feed/no_news/index.tsx b/x-pack/plugins/siem/public/common/components/news_feed/no_news/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/news_feed/no_news/index.tsx rename to x-pack/plugins/siem/public/common/components/news_feed/no_news/index.tsx diff --git a/x-pack/plugins/siem/public/components/news_feed/post/index.tsx b/x-pack/plugins/siem/public/common/components/news_feed/post/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/news_feed/post/index.tsx rename to x-pack/plugins/siem/public/common/components/news_feed/post/index.tsx diff --git a/x-pack/plugins/siem/public/components/news_feed/translations.ts b/x-pack/plugins/siem/public/common/components/news_feed/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/news_feed/translations.ts rename to x-pack/plugins/siem/public/common/components/news_feed/translations.ts diff --git a/x-pack/plugins/siem/public/components/news_feed/types.ts b/x-pack/plugins/siem/public/common/components/news_feed/types.ts similarity index 100% rename from x-pack/plugins/siem/public/components/news_feed/types.ts rename to x-pack/plugins/siem/public/common/components/news_feed/types.ts diff --git a/x-pack/plugins/siem/public/components/page/index.tsx b/x-pack/plugins/siem/public/common/components/page/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/page/index.tsx rename to x-pack/plugins/siem/public/common/components/page/index.tsx diff --git a/x-pack/plugins/siem/public/components/page/manage_query.tsx b/x-pack/plugins/siem/public/common/components/page/manage_query.tsx similarity index 95% rename from x-pack/plugins/siem/public/components/page/manage_query.tsx rename to x-pack/plugins/siem/public/common/components/page/manage_query.tsx index 3b723c66f5af57..9e78f704b0f054 100644 --- a/x-pack/plugins/siem/public/components/page/manage_query.tsx +++ b/x-pack/plugins/siem/public/common/components/page/manage_query.tsx @@ -9,7 +9,7 @@ import { omit } from 'lodash/fp'; import React from 'react'; import { inputsModel } from '../../store'; -import { SetQuery } from '../../pages/hosts/navigation/types'; +import { SetQuery } from '../../../hosts/pages/navigation/types'; interface OwnProps { deleteQuery?: ({ id }: { id: string }) => void; diff --git a/x-pack/plugins/siem/public/components/page/translations.ts b/x-pack/plugins/siem/public/common/components/page/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/page/translations.ts rename to x-pack/plugins/siem/public/common/components/page/translations.ts diff --git a/x-pack/plugins/siem/public/components/page_route/index.tsx b/x-pack/plugins/siem/public/common/components/page_route/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/page_route/index.tsx rename to x-pack/plugins/siem/public/common/components/page_route/index.tsx diff --git a/x-pack/plugins/siem/public/components/page_route/pageroute.test.tsx b/x-pack/plugins/siem/public/common/components/page_route/pageroute.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/page_route/pageroute.test.tsx rename to x-pack/plugins/siem/public/common/components/page_route/pageroute.test.tsx diff --git a/x-pack/plugins/siem/public/components/page_route/pageroute.tsx b/x-pack/plugins/siem/public/common/components/page_route/pageroute.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/page_route/pageroute.tsx rename to x-pack/plugins/siem/public/common/components/page_route/pageroute.tsx diff --git a/x-pack/plugins/siem/public/components/paginated_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/paginated_table/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/paginated_table/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/paginated_table/helpers.test.ts b/x-pack/plugins/siem/public/common/components/paginated_table/helpers.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/paginated_table/helpers.test.ts rename to x-pack/plugins/siem/public/common/components/paginated_table/helpers.test.ts diff --git a/x-pack/plugins/siem/public/common/components/paginated_table/helpers.ts b/x-pack/plugins/siem/public/common/components/paginated_table/helpers.ts new file mode 100644 index 00000000000000..8fde81adc922a7 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/paginated_table/helpers.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 { PaginationInputPaginated } from '../../../graphql/types'; + +export const generateTablePaginationOptions = ( + activePage: number, + limit: number +): PaginationInputPaginated => { + const cursorStart = activePage * limit; + return { + activePage, + cursorStart, + fakePossibleCount: 4 <= activePage && activePage > 0 ? limit * (activePage + 2) : limit * 5, + querySize: limit + cursorStart, + }; +}; diff --git a/x-pack/plugins/siem/public/components/paginated_table/index.mock.tsx b/x-pack/plugins/siem/public/common/components/paginated_table/index.mock.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/paginated_table/index.mock.tsx rename to x-pack/plugins/siem/public/common/components/paginated_table/index.mock.tsx diff --git a/x-pack/plugins/siem/public/common/components/paginated_table/index.test.tsx b/x-pack/plugins/siem/public/common/components/paginated_table/index.test.tsx new file mode 100644 index 00000000000000..108ae19b5a2b47 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/paginated_table/index.test.tsx @@ -0,0 +1,522 @@ +/* + * 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 { mount, shallow } from 'enzyme'; +import React from 'react'; + +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../common/constants'; +import { Direction } from '../../../graphql/types'; + +import { BasicTableProps, PaginatedTable } from './index'; +import { getHostsColumns, mockData, rowItems, sortedHosts } from './index.mock'; +import { ThemeProvider } from 'styled-components'; +import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; + +jest.mock('react', () => { + const r = jest.requireActual('react'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return { ...r, memo: (x: any) => x }; +}); + +describe('Paginated Table Component', () => { + const theme = () => ({ eui: euiDarkVars, darkMode: true }); + let loadPage: jest.Mock; + let updateLimitPagination: jest.Mock; + let updateActivePage: jest.Mock; + beforeEach(() => { + loadPage = jest.fn(); + updateLimitPagination = jest.fn(); + updateActivePage = jest.fn(); + }); + + describe('rendering', () => { + test('it renders the default load more table', () => { + const wrapper = shallow( + + {'My test supplement.'}

} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={1} + loading={false} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={10} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> +
+ ); + + expect(wrapper).toMatchSnapshot(); + }); + + test('it renders the loading panel at the beginning ', () => { + const wrapper = mount( + + {'My test supplement.'}

} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={1} + loading={true} + loadPage={loadPage} + pageOfItems={[]} + showMorePagesIndicator={true} + totalCount={10} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> +
+ ); + + expect( + wrapper.find('[data-test-subj="initialLoadingPanelPaginatedTable"]').exists() + ).toBeTruthy(); + }); + + test('it renders the over loading panel after data has been in the table ', () => { + const wrapper = mount( + + {'My test supplement.'}

} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={1} + loading={true} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={10} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> +
+ ); + + expect(wrapper.find('[data-test-subj="loadingPanelPaginatedTable"]').exists()).toBeTruthy(); + }); + + test('it renders the correct amount of pages and starts at activePage: 0', () => { + const wrapper = mount( + + {'My test supplement.'}

} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={1} + loading={false} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={10} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> +
+ ); + + const paginiationProps = wrapper + .find('[data-test-subj="numberedPagination"]') + .first() + .props(); + + const expectedPaginationProps = { + 'data-test-subj': 'numberedPagination', + pageCount: 10, + activePage: 0, + }; + expect(JSON.stringify(paginiationProps)).toEqual(JSON.stringify(expectedPaginationProps)); + }); + + test('it render popover to select new limit in table', () => { + const wrapper = mount( + + {'My test supplement.'}

} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={2} + loading={false} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={10} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> +
+ ); + + wrapper + .find('[data-test-subj="loadingMoreSizeRowPopover"] button') + .first() + .simulate('click'); + expect(wrapper.find('[data-test-subj="loadingMorePickSizeRow"]').exists()).toBeTruthy(); + }); + + test('it will NOT render popover to select new limit in table if props itemsPerRow is empty', () => { + const wrapper = mount( + + {'My test supplement.'}

} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={[]} + limit={2} + loading={false} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={10} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> +
+ ); + + expect(wrapper.find('[data-test-subj="loadingMoreSizeRowPopover"]').exists()).toBeFalsy(); + }); + + test('It should render a sort icon if sorting is defined', () => { + const mockOnChange = jest.fn(); + const wrapper = mount( + + {'My test supplement.'}

} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={2} + loading={false} + loadPage={jest.fn()} + onChange={mockOnChange} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + sorting={{ direction: Direction.asc, field: 'node.host.name' }} + totalCount={10} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> +
+ ); + + expect(wrapper.find('.euiTable thead tr th button svg')).toBeTruthy(); + }); + + test('Should display toast when user reaches end of results max', () => { + const wrapper = mount( + + {'My test supplement.'}

} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={DEFAULT_MAX_TABLE_QUERY_SIZE} + loading={false} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={DEFAULT_MAX_TABLE_QUERY_SIZE * 3} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> +
+ ); + wrapper + .find('[data-test-subj="pagination-button-next"]') + .first() + .simulate('click'); + expect(updateActivePage.mock.calls.length).toEqual(0); + }); + + test('Should show items per row if totalCount is greater than items', () => { + const wrapper = mount( + + {'My test supplement.'}

} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={DEFAULT_MAX_TABLE_QUERY_SIZE} + loading={false} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={30} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> +
+ ); + expect(wrapper.find('[data-test-subj="loadingMoreSizeRowPopover"]').exists()).toBeTruthy(); + }); + + test('Should hide items per row if totalCount is less than items', () => { + const wrapper = mount( + + {'My test supplement.'}

} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={DEFAULT_MAX_TABLE_QUERY_SIZE} + loading={false} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={1} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> +
+ ); + expect(wrapper.find('[data-test-subj="loadingMoreSizeRowPopover"]').exists()).toBeFalsy(); + }); + }); + + describe('Events', () => { + test('should call updateActivePage with 1 when clicking to the first page', () => { + const wrapper = mount( + + {'My test supplement.'}

} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={1} + loading={false} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={10} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> +
+ ); + wrapper + .find('[data-test-subj="pagination-button-next"]') + .first() + .simulate('click'); + expect(updateActivePage.mock.calls[0][0]).toEqual(1); + }); + + test('Should call updateActivePage with 0 when you pick a new limit', () => { + const wrapper = mount( + + {'My test supplement.'}

} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={2} + loading={false} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={10} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> +
+ ); + wrapper + .find('[data-test-subj="pagination-button-next"]') + .first() + .simulate('click'); + + wrapper + .find('[data-test-subj="loadingMoreSizeRowPopover"] button') + .first() + .simulate('click'); + + wrapper + .find('[data-test-subj="loadingMorePickSizeRow"] button') + .first() + .simulate('click'); + expect(updateActivePage.mock.calls[1][0]).toEqual(0); + }); + + test('should update the page when the activePage is changed from redux', () => { + const ourProps: BasicTableProps = { + activePage: 3, + columns: getHostsColumns(), + headerCount: 1, + headerSupplement:

{'My test supplement.'}

, + headerTitle: 'Hosts', + headerTooltip: 'My test tooltip', + headerUnit: 'Test Unit', + itemsPerRow: rowItems, + limit: 1, + loading: false, + loadPage, + pageOfItems: mockData.Hosts.edges, + showMorePagesIndicator: true, + totalCount: 10, + updateActivePage, + updateLimitPagination: limit => updateLimitPagination({ limit }), + }; + + // enzyme does not allow us to pass props to child of HOC + // so we make a component to pass it the props context + // ComponentWithContext will pass the changed props to Component + // https://github.com/airbnb/enzyme/issues/1853#issuecomment-443475903 + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const ComponentWithContext = (props: BasicTableProps) => { + return ( + + + + ); + }; + + const wrapper = mount(); + expect( + wrapper + .find('[data-test-subj="numberedPagination"]') + .first() + .prop('activePage') + ).toEqual(3); + wrapper.setProps({ activePage: 0 }); + wrapper.update(); + expect( + wrapper + .find('[data-test-subj="numberedPagination"]') + .first() + .prop('activePage') + ).toEqual(0); + }); + + test('Should call updateLimitPagination when you pick a new limit', () => { + const wrapper = mount( + + {'My test supplement.'}

} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={2} + loading={false} + loadPage={loadPage} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + totalCount={10} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> +
+ ); + + wrapper + .find('[data-test-subj="loadingMoreSizeRowPopover"] button') + .first() + .simulate('click'); + + wrapper + .find('[data-test-subj="loadingMorePickSizeRow"] button') + .first() + .simulate('click'); + expect(updateLimitPagination).toBeCalled(); + }); + + test('Should call onChange when you choose a new sort in the table', () => { + const mockOnChange = jest.fn(); + const wrapper = mount( + + {'My test supplement.'}

} + headerTitle="Hosts" + headerTooltip="My test tooltip" + headerUnit="Test Unit" + itemsPerRow={rowItems} + limit={2} + loading={false} + loadPage={jest.fn()} + onChange={mockOnChange} + pageOfItems={mockData.Hosts.edges} + showMorePagesIndicator={true} + sorting={{ direction: Direction.asc, field: 'node.host.name' }} + totalCount={10} + updateActivePage={updateActivePage} + updateLimitPagination={limit => updateLimitPagination({ limit })} + /> +
+ ); + + wrapper + .find('.euiTable thead tr th button') + .first() + .simulate('click'); + + expect(mockOnChange).toBeCalled(); + expect(mockOnChange.mock.calls[0]).toEqual([ + { page: undefined, sort: { direction: 'desc', field: 'node.host.name' } }, + ]); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/common/components/paginated_table/index.tsx b/x-pack/plugins/siem/public/common/components/paginated_table/index.tsx new file mode 100644 index 00000000000000..3b3130af77cfd7 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/paginated_table/index.tsx @@ -0,0 +1,350 @@ +/* + * 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 { + EuiBasicTable, + EuiBasicTableProps, + EuiButtonEmpty, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiGlobalToastListToast as Toast, + EuiLoadingContent, + EuiPagination, + EuiPopover, + Direction, +} from '@elastic/eui'; +import { noop } from 'lodash/fp'; +import React, { FC, memo, useState, useEffect, ComponentType } from 'react'; +import styled from 'styled-components'; + +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../common/constants'; +import { AuthTableColumns } from '../../../hosts/components/authentications_table'; +import { HostsTableColumns } from '../../../hosts/components/hosts_table'; +import { NetworkDnsColumns } from '../../../network/components/network_dns_table/columns'; +import { NetworkHttpColumns } from '../../../network/components/network_http_table/columns'; +import { + NetworkTopNFlowColumns, + NetworkTopNFlowColumnsIpDetails, +} from '../../../network/components/network_top_n_flow_table/columns'; +import { + NetworkTopCountriesColumns, + NetworkTopCountriesColumnsIpDetails, +} from '../../../network/components/network_top_countries_table/columns'; +import { TlsColumns } from '../../../network/components/tls_table/columns'; +import { UncommonProcessTableColumns } from '../../../hosts/components/uncommon_process_table'; +import { UsersColumns } from '../../../network/components/users_table/columns'; +import { HeaderSection } from '../header_section'; +import { Loader } from '../loader'; +import { useStateToaster } from '../toasters'; + +import * as i18n from './translations'; +import { Panel } from '../panel'; +import { InspectButtonContainer } from '../inspect'; + +const DEFAULT_DATA_TEST_SUBJ = 'paginated-table'; + +export interface ItemsPerRow { + text: string; + numberOfRow: number; +} + +export interface SortingBasicTable { + field: string; + direction: Direction; + allowNeutralSort?: boolean; +} + +export interface Criteria { + page?: { index: number; size: number }; + sort?: SortingBasicTable; +} + +declare type HostsTableColumnsTest = [ + Columns, + Columns, + Columns, + Columns +]; + +declare type BasicTableColumns = + | AuthTableColumns + | HostsTableColumns + | HostsTableColumnsTest + | NetworkDnsColumns + | NetworkHttpColumns + | NetworkTopCountriesColumns + | NetworkTopCountriesColumnsIpDetails + | NetworkTopNFlowColumns + | NetworkTopNFlowColumnsIpDetails + | TlsColumns + | UncommonProcessTableColumns + | UsersColumns; + +declare type SiemTables = BasicTableProps; + +// Using telescoping templates to remove 'any' that was polluting downstream column type checks +export interface BasicTableProps { + activePage: number; + columns: T; + dataTestSubj?: string; + headerCount: number; + headerSupplement?: React.ReactElement; + headerTitle: string | React.ReactElement; + headerTooltip?: string; + headerUnit: string | React.ReactElement; + id?: string; + itemsPerRow?: ItemsPerRow[]; + isInspect?: boolean; + limit: number; + loading: boolean; + loadPage: (activePage: number) => void; + onChange?: (criteria: Criteria) => void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + pageOfItems: any[]; + showMorePagesIndicator: boolean; + sorting?: SortingBasicTable; + totalCount: number; + updateActivePage: (activePage: number) => void; + updateLimitPagination: (limit: number) => void; +} +type Func = (arg: T) => string | number; + +export interface Columns { + align?: string; + field?: string; + hideForMobile?: boolean; + isMobileHeader?: boolean; + name: string | React.ReactNode; + render?: (item: T, node: U) => React.ReactNode; + sortable?: boolean | Func; + truncateText?: boolean; + width?: string; +} + +const PaginatedTableComponent: FC = ({ + activePage, + columns, + dataTestSubj = DEFAULT_DATA_TEST_SUBJ, + headerCount, + headerSupplement, + headerTitle, + headerTooltip, + headerUnit, + id, + isInspect, + itemsPerRow, + limit, + loading, + loadPage, + onChange = noop, + pageOfItems, + showMorePagesIndicator, + sorting = null, + totalCount, + updateActivePage, + updateLimitPagination, +}) => { + const [myLoading, setMyLoading] = useState(loading); + const [myActivePage, setActivePage] = useState(activePage); + const [loadingInitial, setLoadingInitial] = useState(headerCount === -1); + const [isPopoverOpen, setPopoverOpen] = useState(false); + + const pageCount = Math.ceil(totalCount / limit); + const dispatchToaster = useStateToaster()[1]; + + useEffect(() => { + setActivePage(activePage); + }, [activePage]); + + useEffect(() => { + if (headerCount >= 0 && loadingInitial) { + setLoadingInitial(false); + } + }, [loadingInitial, headerCount]); + + useEffect(() => { + setMyLoading(loading); + }, [loading]); + + const onButtonClick = () => { + setPopoverOpen(!isPopoverOpen); + }; + + const closePopover = () => { + setPopoverOpen(false); + }; + + const goToPage = (newActivePage: number) => { + if ((newActivePage + 1) * limit >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + const toast: Toast = { + id: 'PaginationWarningMsg', + title: headerTitle + i18n.TOAST_TITLE, + color: 'warning', + iconType: 'alert', + toastLifeTimeMs: 10000, + text: i18n.TOAST_TEXT, + }; + return dispatchToaster({ + type: 'addToaster', + toast, + }); + } + setActivePage(newActivePage); + loadPage(newActivePage); + updateActivePage(newActivePage); + }; + + const button = ( + + {`${i18n.ROWS}: ${limit}`} + + ); + + const rowItems = + itemsPerRow && + itemsPerRow.map((item: ItemsPerRow) => ( + { + closePopover(); + updateLimitPagination(item.numberOfRow); + updateActivePage(0); // reset results to first page + }} + > + {item.text} + + )); + const PaginationWrapper = showMorePagesIndicator ? PaginationEuiFlexItem : EuiFlexItem; + + return ( + + + = 0 ? headerCount.toLocaleString() : 0} ${headerUnit}` + } + title={headerTitle} + tooltip={headerTooltip} + > + {!loadingInitial && headerSupplement} + + + {loadingInitial ? ( + + ) : ( + <> + + + + {itemsPerRow && itemsPerRow.length > 0 && totalCount >= itemsPerRow[0].numberOfRow && ( + + + + )} + + + + + + + {(isInspect || myLoading) && ( + + )} + + )} + + + ); +}; + +export const PaginatedTable = memo(PaginatedTableComponent); + +type BasicTableType = ComponentType>; // eslint-disable-line @typescript-eslint/no-explicit-any +const BasicTable = styled(EuiBasicTable as BasicTableType)` + tbody { + th, + td { + vertical-align: top; + } + + .euiTableCellContent { + display: block; + } + } +` as any; // eslint-disable-line @typescript-eslint/no-explicit-any + +BasicTable.displayName = 'BasicTable'; + +const FooterAction = styled(EuiFlexGroup).attrs(() => ({ + alignItems: 'center', + responsive: false, +}))` + margin-top: ${({ theme }) => theme.eui.euiSizeXS}; +`; + +FooterAction.displayName = 'FooterAction'; + +const PaginationEuiFlexItem = styled(EuiFlexItem)` + @media only screen and (min-width: ${({ theme }) => theme.eui.euiBreakpoints.m}) { + .euiButtonIcon:last-child { + margin-left: 28px; + } + + .euiPagination { + position: relative; + } + + .euiPagination::before { + bottom: 0; + color: ${({ theme }) => theme.eui.euiButtonColorDisabled}; + content: '\\2026'; + font-size: ${({ theme }) => theme.eui.euiFontSizeS}; + padding: 5px ${({ theme }) => theme.eui.euiSizeS}; + position: absolute; + right: ${({ theme }) => theme.eui.euiSizeL}; + } + } +`; + +PaginationEuiFlexItem.displayName = 'PaginationEuiFlexItem'; diff --git a/x-pack/plugins/siem/public/components/paginated_table/translations.ts b/x-pack/plugins/siem/public/common/components/paginated_table/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/paginated_table/translations.ts rename to x-pack/plugins/siem/public/common/components/paginated_table/translations.ts diff --git a/x-pack/plugins/siem/public/components/panel/index.test.tsx b/x-pack/plugins/siem/public/common/components/panel/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/panel/index.test.tsx rename to x-pack/plugins/siem/public/common/components/panel/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/panel/index.tsx b/x-pack/plugins/siem/public/common/components/panel/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/panel/index.tsx rename to x-pack/plugins/siem/public/common/components/panel/index.tsx diff --git a/x-pack/plugins/siem/public/components/progress_inline/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/common/components/progress_inline/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/progress_inline/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/progress_inline/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/progress_inline/index.test.tsx b/x-pack/plugins/siem/public/common/components/progress_inline/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/progress_inline/index.test.tsx rename to x-pack/plugins/siem/public/common/components/progress_inline/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/progress_inline/index.tsx b/x-pack/plugins/siem/public/common/components/progress_inline/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/progress_inline/index.tsx rename to x-pack/plugins/siem/public/common/components/progress_inline/index.tsx diff --git a/x-pack/plugins/siem/public/common/components/query_bar/index.test.tsx b/x-pack/plugins/siem/public/common/components/query_bar/index.test.tsx new file mode 100644 index 00000000000000..74c07640b83286 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/query_bar/index.test.tsx @@ -0,0 +1,338 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; + +import { DEFAULT_FROM, DEFAULT_TO } from '../../../../common/constants'; +import { TestProviders, mockIndexPattern } from '../../mock'; +import { createKibanaCoreStartMock } from '../../mock/kibana_core'; +import { FilterManager, SearchBar } from '../../../../../../../src/plugins/data/public'; +import { QueryBar, QueryBarComponentProps } from '.'; +import { createKibanaContextProviderMock } from '../../mock/kibana_react'; + +const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; + +describe('QueryBar ', () => { + // We are doing that because we need to wrapped this component with redux + // and redux does not like to be updated and since we need to update our + // child component (BODY) and we do not want to scare anyone with this error + // we are hiding it!!! + // eslint-disable-next-line no-console + const originalError = console.error; + beforeAll(() => { + // eslint-disable-next-line no-console + console.error = (...args: string[]) => { + if (/ does not support changing `store` on the fly/.test(args[0])) { + return; + } + originalError.call(console, ...args); + }; + }); + + const mockOnChangeQuery = jest.fn(); + const mockOnSubmitQuery = jest.fn(); + const mockOnSavedQuery = jest.fn(); + + beforeEach(() => { + mockOnChangeQuery.mockClear(); + mockOnSubmitQuery.mockClear(); + mockOnSavedQuery.mockClear(); + }); + + test('check if we format the appropriate props to QueryBar', () => { + const wrapper = mount( + + + + ); + const { + customSubmitButton, + timeHistory, + onClearSavedQuery, + onFiltersUpdated, + onQueryChange, + onQuerySubmit, + onSaved, + onSavedQueryUpdated, + ...searchBarProps + } = wrapper.find(SearchBar).props(); + + expect(searchBarProps).toEqual({ + dataTestSubj: undefined, + dateRangeFrom: 'now-24h', + dateRangeTo: 'now', + filters: [], + indexPatterns: [ + { + fields: [ + { + aggregatable: true, + name: '@timestamp', + searchable: true, + type: 'date', + }, + { + aggregatable: true, + name: '@version', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.ephemeral_id', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.hostname', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.id', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test1', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test2', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test3', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test4', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test5', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test6', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test7', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'agent.test8', + searchable: true, + type: 'string', + }, + { + aggregatable: true, + name: 'host.name', + searchable: true, + type: 'string', + }, + ], + title: 'filebeat-*,auditbeat-*,packetbeat-*', + }, + ], + isLoading: false, + isRefreshPaused: true, + query: { + language: 'kuery', + query: 'here: query', + }, + refreshInterval: undefined, + showAutoRefreshOnly: false, + showDatePicker: false, + showFilterBar: true, + showQueryBar: true, + showQueryInput: true, + showSaveQuery: true, + }); + }); + + describe('#onQueryChange', () => { + test(' is the only reference that changed when filterQueryDraft props get updated', () => { + const KibanaWithStorageProvider = createKibanaContextProviderMock(); + + const Proxy = (props: QueryBarComponentProps) => ( + + + + + + ); + + const wrapper = mount( + + ); + const searchBarProps = wrapper.find(SearchBar).props(); + const onChangedQueryRef = searchBarProps.onQueryChange; + const onSubmitQueryRef = searchBarProps.onQuerySubmit; + const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; + + const queryInput = wrapper.find(QueryBar).find('input[data-test-subj="queryInput"]'); + queryInput.simulate('change', { target: { value: 'hello: world' } }); + wrapper.update(); + + expect(onChangedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQueryChange); + expect(onSubmitQueryRef).toEqual(wrapper.find(SearchBar).props().onQuerySubmit); + expect(onSavedQueryRef).toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated); + }); + }); + + describe('#onQuerySubmit', () => { + test(' is the only reference that changed when filterQuery props get updated', () => { + const Proxy = (props: QueryBarComponentProps) => ( + + + + ); + + const wrapper = mount( + + ); + const searchBarProps = wrapper.find(SearchBar).props(); + const onChangedQueryRef = searchBarProps.onQueryChange; + const onSubmitQueryRef = searchBarProps.onQuerySubmit; + const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; + + wrapper.setProps({ filterQuery: { expression: 'new: one', kind: 'kuery' } }); + wrapper.update(); + + expect(onSubmitQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQuerySubmit); + expect(onChangedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQueryChange); + expect(onSavedQueryRef).toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated); + }); + + test(' is only reference that changed when timelineId props get updated', () => { + const Proxy = (props: QueryBarComponentProps) => ( + + + + ); + + const wrapper = mount( + + ); + const searchBarProps = wrapper.find(SearchBar).props(); + const onChangedQueryRef = searchBarProps.onQueryChange; + const onSubmitQueryRef = searchBarProps.onQuerySubmit; + const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; + + wrapper.setProps({ onSubmitQuery: jest.fn() }); + wrapper.update(); + + expect(onSubmitQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQuerySubmit); + expect(onChangedQueryRef).toEqual(wrapper.find(SearchBar).props().onQueryChange); + expect(onSavedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated); + }); + }); + + describe('#onSavedQueryUpdated', () => { + test('is only reference that changed when dataProviders props get updated', () => { + const Proxy = (props: QueryBarComponentProps) => ( + + + + ); + + const wrapper = mount( + + ); + const searchBarProps = wrapper.find(SearchBar).props(); + const onChangedQueryRef = searchBarProps.onQueryChange; + const onSubmitQueryRef = searchBarProps.onQuerySubmit; + const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; + + wrapper.setProps({ onSavedQuery: jest.fn() }); + wrapper.update(); + + expect(onSavedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated); + expect(onChangedQueryRef).toEqual(wrapper.find(SearchBar).props().onQueryChange); + expect(onSubmitQueryRef).toEqual(wrapper.find(SearchBar).props().onQuerySubmit); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/common/components/query_bar/index.tsx b/x-pack/plugins/siem/public/common/components/query_bar/index.tsx new file mode 100644 index 00000000000000..557d389aefee92 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/query_bar/index.tsx @@ -0,0 +1,153 @@ +/* + * 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, useState, useEffect, useMemo, useCallback } from 'react'; +import deepEqual from 'fast-deep-equal'; + +import { + Filter, + IIndexPattern, + FilterManager, + Query, + TimeHistory, + TimeRange, + SavedQuery, + SearchBar, + SavedQueryTimeFilter, +} from '../../../../../../../src/plugins/data/public'; +import { Storage } from '../../../../../../../src/plugins/kibana_utils/public'; + +export interface QueryBarComponentProps { + dataTestSubj?: string; + dateRangeFrom?: string; + dateRangeTo?: string; + hideSavedQuery?: boolean; + indexPattern: IIndexPattern; + isLoading?: boolean; + isRefreshPaused?: boolean; + filterQuery: Query; + filterManager: FilterManager; + filters: Filter[]; + onChangedQuery: (query: Query) => void; + onSubmitQuery: (query: Query, timefilter?: SavedQueryTimeFilter) => void; + refreshInterval?: number; + savedQuery?: SavedQuery | null; + onSavedQuery: (savedQuery: SavedQuery | null) => void; +} + +export const QueryBar = memo( + ({ + dateRangeFrom, + dateRangeTo, + hideSavedQuery = false, + indexPattern, + isLoading = false, + isRefreshPaused, + filterQuery, + filterManager, + filters, + onChangedQuery, + onSubmitQuery, + refreshInterval, + savedQuery, + onSavedQuery, + dataTestSubj, + }) => { + const [draftQuery, setDraftQuery] = useState(filterQuery); + + useEffect(() => { + setDraftQuery(filterQuery); + }, [filterQuery]); + + const onQuerySubmit = useCallback( + (payload: { dateRange: TimeRange; query?: Query }) => { + if (payload.query != null && !deepEqual(payload.query, filterQuery)) { + onSubmitQuery(payload.query); + } + }, + [filterQuery, onSubmitQuery] + ); + + const onQueryChange = useCallback( + (payload: { dateRange: TimeRange; query?: Query }) => { + if (payload.query != null && !deepEqual(payload.query, draftQuery)) { + setDraftQuery(payload.query); + onChangedQuery(payload.query); + } + }, + [draftQuery, onChangedQuery, setDraftQuery] + ); + + const onSaved = useCallback( + (newSavedQuery: SavedQuery) => { + onSavedQuery(newSavedQuery); + }, + [onSavedQuery] + ); + + const onSavedQueryUpdated = useCallback( + (savedQueryUpdated: SavedQuery) => { + const { query: newQuery, filters: newFilters, timefilter } = savedQueryUpdated.attributes; + onSubmitQuery(newQuery, timefilter); + filterManager.setFilters(newFilters || []); + onSavedQuery(savedQueryUpdated); + }, + [filterManager, onSubmitQuery, onSavedQuery] + ); + + const onClearSavedQuery = useCallback(() => { + if (savedQuery != null) { + onSubmitQuery({ + query: '', + language: savedQuery.attributes.query.language, + }); + filterManager.setFilters([]); + onSavedQuery(null); + } + }, [filterManager, onSubmitQuery, onSavedQuery, savedQuery]); + + const onFiltersUpdated = useCallback( + (newFilters: Filter[]) => { + filterManager.setFilters(newFilters); + }, + [filterManager] + ); + + const CustomButton = <>{null}; + const indexPatterns = useMemo(() => [indexPattern], [indexPattern]); + + const searchBarProps = savedQuery != null ? { savedQuery } : {}; + + return ( + + ); + } +); diff --git a/x-pack/plugins/siem/public/components/scroll_to_top/index.test.tsx b/x-pack/plugins/siem/public/common/components/scroll_to_top/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/scroll_to_top/index.test.tsx rename to x-pack/plugins/siem/public/common/components/scroll_to_top/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/scroll_to_top/index.tsx b/x-pack/plugins/siem/public/common/components/scroll_to_top/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/scroll_to_top/index.tsx rename to x-pack/plugins/siem/public/common/components/scroll_to_top/index.tsx diff --git a/x-pack/plugins/siem/public/common/components/search_bar/index.tsx b/x-pack/plugins/siem/public/common/components/search_bar/index.tsx new file mode 100644 index 00000000000000..995955cff54f57 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/search_bar/index.tsx @@ -0,0 +1,387 @@ +/* + * 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 { getOr, set } from 'lodash/fp'; +import React, { memo, useEffect, useCallback, useMemo } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import { Dispatch } from 'redux'; +import { Subscription } from 'rxjs'; +import styled from 'styled-components'; +import deepEqual from 'fast-deep-equal'; +import { + FilterManager, + IIndexPattern, + TimeRange, + Query, + Filter, + SavedQuery, +} from 'src/plugins/data/public'; + +import { OnTimeChangeProps } from '@elastic/eui'; + +import { inputsActions } from '../../store/inputs'; +import { InputsRange } from '../../store/inputs/model'; +import { InputsModelId } from '../../store/inputs/constants'; +import { State, inputsModel } from '../../store'; +import { formatDate } from '../super_date_picker'; +import { + endSelector, + filterQuerySelector, + fromStrSelector, + isLoadingSelector, + kindSelector, + queriesSelector, + savedQuerySelector, + startSelector, + toStrSelector, +} from './selectors'; +import { hostsActions } from '../../../hosts/store'; +import { networkActions } from '../../../network/store'; +import { timelineActions } from '../../../timelines/store/timeline'; +import { useKibana } from '../../lib/kibana'; + +interface SiemSearchBarProps { + id: InputsModelId; + indexPattern: IIndexPattern; + timelineId?: string; + dataTestSubj?: string; +} + +const SearchBarContainer = styled.div` + .globalQueryBar { + padding: 0px; + } +`; + +const SearchBarComponent = memo( + ({ + end, + filterQuery, + fromStr, + id, + indexPattern, + isLoading = false, + queries, + savedQuery, + setSavedQuery, + setSearchBarFilter, + start, + toStr, + updateSearch, + dataTestSubj, + }) => { + const { data } = useKibana().services; + const { + timefilter: { timefilter }, + filterManager, + } = data.query; + + if (fromStr != null && toStr != null) { + timefilter.setTime({ from: fromStr, to: toStr }); + } else if (start != null && end != null) { + timefilter.setTime({ + from: new Date(start).toISOString(), + to: new Date(end).toISOString(), + }); + } + + const onQuerySubmit = useCallback( + (payload: { dateRange: TimeRange; query?: Query }) => { + const isQuickSelection = + payload.dateRange.from.includes('now') || payload.dateRange.to.includes('now'); + let updateSearchBar: UpdateReduxSearchBar = { + id, + end: toStr != null ? toStr : new Date(end).toISOString(), + start: fromStr != null ? fromStr : new Date(start).toISOString(), + isInvalid: false, + isQuickSelection, + updateTime: false, + filterManager, + }; + let isStateUpdated = false; + + if ( + (isQuickSelection && + (fromStr !== payload.dateRange.from || toStr !== payload.dateRange.to)) || + (!isQuickSelection && + (start !== formatDate(payload.dateRange.from) || + end !== formatDate(payload.dateRange.to))) + ) { + isStateUpdated = true; + updateSearchBar.updateTime = true; + updateSearchBar.end = payload.dateRange.to; + updateSearchBar.start = payload.dateRange.from; + } + + if (payload.query != null && !deepEqual(payload.query, filterQuery)) { + isStateUpdated = true; + updateSearchBar = set('query', payload.query, updateSearchBar); + } + + if (!isStateUpdated) { + // That mean we are doing a refresh! + if (isQuickSelection) { + updateSearchBar.updateTime = true; + updateSearchBar.end = payload.dateRange.to; + updateSearchBar.start = payload.dateRange.from; + } else { + queries.forEach(q => q.refetch && (q.refetch as inputsModel.Refetch)()); + } + } + + window.setTimeout(() => updateSearch(updateSearchBar), 0); + }, + [id, end, filterQuery, fromStr, queries, start, toStr] + ); + + const onRefresh = useCallback( + (payload: { dateRange: TimeRange }) => { + if (payload.dateRange.from.includes('now') || payload.dateRange.to.includes('now')) { + updateSearch({ + id, + end: payload.dateRange.to, + start: payload.dateRange.from, + isInvalid: false, + isQuickSelection: true, + updateTime: true, + filterManager, + }); + } else { + queries.forEach(q => q.refetch && (q.refetch as inputsModel.Refetch)()); + } + }, + [id, queries, filterManager] + ); + + const onSaved = useCallback( + (newSavedQuery: SavedQuery) => { + setSavedQuery({ id, savedQuery: newSavedQuery }); + }, + [id] + ); + + const onSavedQueryUpdated = useCallback( + (savedQueryUpdated: SavedQuery) => { + const isQuickSelection = savedQueryUpdated.attributes.timefilter + ? savedQueryUpdated.attributes.timefilter.from.includes('now') || + savedQueryUpdated.attributes.timefilter.to.includes('now') + : false; + + let updateSearchBar: UpdateReduxSearchBar = { + id, + filters: savedQueryUpdated.attributes.filters || [], + end: toStr != null ? toStr : new Date(end).toISOString(), + start: fromStr != null ? fromStr : new Date(start).toISOString(), + isInvalid: false, + isQuickSelection, + updateTime: false, + filterManager, + }; + + if (savedQueryUpdated.attributes.timefilter) { + updateSearchBar.end = savedQueryUpdated.attributes.timefilter + ? savedQueryUpdated.attributes.timefilter.to + : updateSearchBar.end; + updateSearchBar.start = savedQueryUpdated.attributes.timefilter + ? savedQueryUpdated.attributes.timefilter.from + : updateSearchBar.start; + updateSearchBar.updateTime = true; + } + + updateSearchBar = set('query', savedQueryUpdated.attributes.query, updateSearchBar); + updateSearchBar = set('savedQuery', savedQueryUpdated, updateSearchBar); + + updateSearch(updateSearchBar); + }, + [id, end, fromStr, start, toStr] + ); + + const onClearSavedQuery = useCallback(() => { + if (savedQuery != null) { + updateSearch({ + id, + filters: [], + end: toStr != null ? toStr : new Date(end).toISOString(), + start: fromStr != null ? fromStr : new Date(start).toISOString(), + isInvalid: false, + isQuickSelection: false, + updateTime: false, + query: { + query: '', + language: savedQuery.attributes.query.language, + }, + resetSavedQuery: true, + savedQuery: undefined, + filterManager, + }); + } + }, [id, end, filterManager, fromStr, start, toStr, savedQuery]); + + useEffect(() => { + let isSubscribed = true; + const subscriptions = new Subscription(); + + subscriptions.add( + filterManager.getUpdates$().subscribe({ + next: () => { + if (isSubscribed) { + setSearchBarFilter({ + id, + filters: filterManager.getFilters(), + }); + } + }, + }) + ); + + return () => { + isSubscribed = false; + subscriptions.unsubscribe(); + }; + }, []); + const indexPatterns = useMemo(() => [indexPattern], [indexPattern]); + return ( + + + + ); + } +); + +const makeMapStateToProps = () => { + const getEndSelector = endSelector(); + const getFromStrSelector = fromStrSelector(); + const getIsLoadingSelector = isLoadingSelector(); + const getKindSelector = kindSelector(); + const getQueriesSelector = queriesSelector(); + const getStartSelector = startSelector(); + const getToStrSelector = toStrSelector(); + const getFilterQuerySelector = filterQuerySelector(); + const getSavedQuerySelector = savedQuerySelector(); + return (state: State, { id }: SiemSearchBarProps) => { + const inputsRange: InputsRange = getOr({}, `inputs.${id}`, state); + return { + end: getEndSelector(inputsRange), + fromStr: getFromStrSelector(inputsRange), + filterQuery: getFilterQuerySelector(inputsRange), + isLoading: getIsLoadingSelector(inputsRange), + kind: getKindSelector(inputsRange), + queries: getQueriesSelector(inputsRange), + savedQuery: getSavedQuerySelector(inputsRange), + start: getStartSelector(inputsRange), + toStr: getToStrSelector(inputsRange), + }; + }; +}; + +SearchBarComponent.displayName = 'SiemSearchBar'; + +interface UpdateReduxSearchBar extends OnTimeChangeProps { + id: InputsModelId; + filters?: Filter[]; + filterManager: FilterManager; + query?: Query; + savedQuery?: SavedQuery; + resetSavedQuery?: boolean; + timelineId?: string; + updateTime: boolean; +} + +export const dispatchUpdateSearch = (dispatch: Dispatch) => ({ + end, + filters, + id, + isQuickSelection, + query, + resetSavedQuery, + savedQuery, + start, + timelineId, + filterManager, + updateTime = false, +}: UpdateReduxSearchBar): void => { + if (updateTime) { + const fromDate = formatDate(start); + let toDate = formatDate(end, { roundUp: true }); + if (isQuickSelection) { + dispatch( + inputsActions.setRelativeRangeDatePicker({ + id, + fromStr: start, + toStr: end, + from: fromDate, + to: toDate, + }) + ); + } else { + toDate = formatDate(end); + dispatch( + inputsActions.setAbsoluteRangeDatePicker({ + id, + from: formatDate(start), + to: formatDate(end), + }) + ); + } + if (timelineId != null) { + dispatch( + timelineActions.updateRange({ + id: timelineId, + start: fromDate, + end: toDate, + }) + ); + } + } + if (query != null) { + dispatch( + inputsActions.setFilterQuery({ + id, + ...query, + }) + ); + } + if (filters != null) { + filterManager.setFilters(filters); + } + if (savedQuery != null || resetSavedQuery) { + dispatch(inputsActions.setSavedQuery({ id, savedQuery })); + } + + dispatch(hostsActions.setHostTablesActivePageToZero()); + dispatch(networkActions.setNetworkTablesActivePageToZero()); +}; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + updateSearch: dispatchUpdateSearch(dispatch), + setSavedQuery: ({ id, savedQuery }: { id: InputsModelId; savedQuery: SavedQuery | undefined }) => + dispatch(inputsActions.setSavedQuery({ id, savedQuery })), + setSearchBarFilter: ({ id, filters }: { id: InputsModelId; filters: Filter[] }) => + dispatch(inputsActions.setSearchBarFilter({ id, filters })), +}); + +export const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const SiemSearchBar = connector(SearchBarComponent); diff --git a/x-pack/plugins/siem/public/common/components/search_bar/selectors.ts b/x-pack/plugins/siem/public/common/components/search_bar/selectors.ts new file mode 100644 index 00000000000000..793737a1ad754c --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/search_bar/selectors.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createSelector } from 'reselect'; +import { InputsRange } from '../../store/inputs/model'; +import { Query, SavedQuery } from '../../../../../../../src/plugins/data/public'; + +export { + endSelector, + fromStrSelector, + isLoadingSelector, + kindSelector, + queriesSelector, + startSelector, + toStrSelector, +} from '../super_date_picker/selectors'; + +export const getFilterQuery = (inputState: InputsRange): Query => inputState.query; + +export const getSavedQuery = (inputState: InputsRange): SavedQuery | undefined => + inputState.savedQuery; + +export const filterQuerySelector = () => createSelector(getFilterQuery, filterQuery => filterQuery); + +export const savedQuerySelector = () => createSelector(getSavedQuery, savedQuery => savedQuery); diff --git a/x-pack/plugins/siem/public/components/selectable_text/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/common/components/selectable_text/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/selectable_text/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/selectable_text/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/selectable_text/index.test.tsx b/x-pack/plugins/siem/public/common/components/selectable_text/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/selectable_text/index.test.tsx rename to x-pack/plugins/siem/public/common/components/selectable_text/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/selectable_text/index.tsx b/x-pack/plugins/siem/public/common/components/selectable_text/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/selectable_text/index.tsx rename to x-pack/plugins/siem/public/common/components/selectable_text/index.tsx diff --git a/x-pack/plugins/siem/public/components/sidebar_header/index.tsx b/x-pack/plugins/siem/public/common/components/sidebar_header/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/sidebar_header/index.tsx rename to x-pack/plugins/siem/public/common/components/sidebar_header/index.tsx diff --git a/x-pack/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/common/components/stat_items/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/stat_items/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/stat_items/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/common/components/stat_items/index.test.tsx b/x-pack/plugins/siem/public/common/components/stat_items/index.test.tsx new file mode 100644 index 00000000000000..e0da50abf6b532 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/stat_items/index.test.tsx @@ -0,0 +1,294 @@ +/* + * 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. + */ + +/* eslint-disable react/display-name */ + +import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { mount, ReactWrapper } from 'enzyme'; +import React from 'react'; +import { ThemeProvider } from 'styled-components'; + +import { + StatItemsComponent, + StatItemsProps, + addValueToFields, + addValueToAreaChart, + addValueToBarChart, + useKpiMatrixStatus, + StatItems, +} from '.'; +import { BarChart } from '../charts/barchart'; +import { AreaChart } from '../charts/areachart'; +import { EuiHorizontalRule } from '@elastic/eui'; +import { fieldTitleChartMapping } from '../../../network/components/kpi_network'; +import { + mockData, + mockEnableChartsData, + mockNoChartMappings, + mockNarrowDateRange, +} from '../../../network/components/kpi_network/mock'; +import { mockGlobalState, apolloClientObservable, SUB_PLUGINS_REDUCER } from '../../mock'; +import { State, createStore } from '../../store'; +import { Provider as ReduxStoreProvider } from 'react-redux'; +import { KpiNetworkData, KpiHostsData } from '../../../graphql/types'; + +const from = new Date('2019-06-15T06:00:00.000Z').valueOf(); +const to = new Date('2019-06-18T06:00:00.000Z').valueOf(); + +jest.mock('../charts/areachart', () => { + return { AreaChart: () =>
}; +}); + +jest.mock('../charts/barchart', () => { + return { BarChart: () =>
}; +}); + +describe('Stat Items Component', () => { + const theme = () => ({ eui: euiDarkVars, darkMode: true }); + const state: State = mockGlobalState; + const store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + + describe.each([ + [ + mount( + + + + + + ), + ], + [ + mount( + + + + + + ), + ], + ])('disable charts', wrapper => { + test('it renders the default widget', () => { + expect(wrapper).toMatchSnapshot(); + }); + + test('should render titles', () => { + expect(wrapper.find('[data-test-subj="stat-title"]')).toBeTruthy(); + }); + + test('should not render icons', () => { + expect(wrapper.find('[data-test-subj="stat-icon"]').filter('EuiIcon')).toHaveLength(0); + }); + + test('should not render barChart', () => { + expect(wrapper.find(BarChart)).toHaveLength(0); + }); + + test('should not render areaChart', () => { + expect(wrapper.find(AreaChart)).toHaveLength(0); + }); + + test('should not render spliter', () => { + expect(wrapper.find(EuiHorizontalRule)).toHaveLength(0); + }); + }); + + describe('rendering kpis with charts', () => { + const mockStatItemsData: StatItemsProps = { + areaChart: [ + { + key: 'uniqueSourceIpsHistogram', + value: [ + { x: new Date('2019-05-03T13:00:00.000Z').valueOf(), y: 565975 }, + { x: new Date('2019-05-04T01:00:00.000Z').valueOf(), y: 1084366 }, + { x: new Date('2019-05-04T13:00:00.000Z').valueOf(), y: 12280 }, + ], + color: '#D36086', + }, + { + key: 'uniqueDestinationIpsHistogram', + value: [ + { x: new Date('2019-05-03T13:00:00.000Z').valueOf(), y: 565975 }, + { x: new Date('2019-05-04T01:00:00.000Z').valueOf(), y: 1084366 }, + { x: new Date('2019-05-04T13:00:00.000Z').valueOf(), y: 12280 }, + ], + color: '#9170B8', + }, + ], + barChart: [ + { key: 'uniqueSourceIps', value: [{ x: 'uniqueSourceIps', y: '1714' }], color: '#D36086' }, + { + key: 'uniqueDestinationIps', + value: [{ x: 'uniqueDestinationIps', y: 2354 }], + color: '#9170B8', + }, + ], + description: 'UNIQUE_PRIVATE_IPS', + enableAreaChart: true, + enableBarChart: true, + fields: [ + { + key: 'uniqueSourceIps', + description: 'Source', + value: 1714, + color: '#D36086', + icon: 'cross', + }, + { + key: 'uniqueDestinationIps', + description: 'Dest.', + value: 2359, + color: '#9170B8', + icon: 'cross', + }, + ], + from, + id: 'statItems', + index: 0, + key: 'mock-keys', + to, + narrowDateRange: mockNarrowDateRange, + }; + let wrapper: ReactWrapper; + beforeAll(() => { + wrapper = mount( + + + + ); + }); + test('it renders the default widget', () => { + expect(wrapper).toMatchSnapshot(); + }); + + test('should handle multiple titles', () => { + expect(wrapper.find('[data-test-subj="stat-title"]')).toHaveLength(2); + }); + + test('should render kpi icons', () => { + expect(wrapper.find('[data-test-subj="stat-icon"]').filter('EuiIcon')).toHaveLength(2); + }); + + test('should render barChart', () => { + expect(wrapper.find(BarChart)).toHaveLength(1); + }); + + test('should render areaChart', () => { + expect(wrapper.find(AreaChart)).toHaveLength(1); + }); + + test('should render separator', () => { + expect(wrapper.find(EuiHorizontalRule)).toHaveLength(1); + }); + }); +}); + +describe('addValueToFields', () => { + const mockNetworkMappings = fieldTitleChartMapping[0]; + const mockKpiNetworkData = mockData.KpiNetwork; + test('should update value from data', () => { + const result = addValueToFields(mockNetworkMappings.fields, mockKpiNetworkData); + expect(result).toEqual(mockEnableChartsData.fields); + }); +}); + +describe('addValueToAreaChart', () => { + const mockNetworkMappings = fieldTitleChartMapping[0]; + const mockKpiNetworkData = mockData.KpiNetwork; + test('should add areaChart from data', () => { + const result = addValueToAreaChart(mockNetworkMappings.fields, mockKpiNetworkData); + expect(result).toEqual(mockEnableChartsData.areaChart); + }); +}); + +describe('addValueToBarChart', () => { + const mockNetworkMappings = fieldTitleChartMapping[0]; + const mockKpiNetworkData = mockData.KpiNetwork; + test('should add areaChart from data', () => { + const result = addValueToBarChart(mockNetworkMappings.fields, mockKpiNetworkData); + expect(result).toEqual(mockEnableChartsData.barChart); + }); +}); + +describe('useKpiMatrixStatus', () => { + const mockNetworkMappings = fieldTitleChartMapping; + const mockKpiNetworkData = mockData.KpiNetwork; + const MockChildComponent = (mappedStatItemProps: StatItemsProps) => ; + const MockHookWrapperComponent = ({ + fieldsMapping, + data, + }: { + fieldsMapping: Readonly; + data: KpiNetworkData | KpiHostsData; + }) => { + const statItemsProps: StatItemsProps[] = useKpiMatrixStatus( + fieldsMapping, + data, + 'statItem', + from, + to, + mockNarrowDateRange + ); + + return ( +
+ {statItemsProps.map(mappedStatItemProps => { + return ; + })} +
+ ); + }; + + test('it updates status correctly', () => { + const wrapper = mount( + <> + + + ); + + expect(wrapper.find('MockChildComponent').get(0).props).toEqual(mockEnableChartsData); + }); + + test('it should not append areaChart if enableAreaChart is off', () => { + const wrapper = mount( + <> + + + ); + + expect(wrapper.find('MockChildComponent').get(0).props.areaChart).toBeUndefined(); + }); + + test('it should not append barChart if enableBarChart is off', () => { + const wrapper = mount( + <> + + + ); + + expect(wrapper.find('MockChildComponent').get(0).props.barChart).toBeUndefined(); + }); +}); diff --git a/x-pack/plugins/siem/public/common/components/stat_items/index.tsx b/x-pack/plugins/siem/public/common/components/stat_items/index.tsx new file mode 100644 index 00000000000000..b2543f70e9401e --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/stat_items/index.tsx @@ -0,0 +1,286 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ScaleType, Rotation, BrushEndListener, ElementClickListener } from '@elastic/charts'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiPanel, + EuiHorizontalRule, + EuiIcon, + EuiTitle, + IconType, +} from '@elastic/eui'; +import { get, getOr } from 'lodash/fp'; +import React, { useState, useEffect } from 'react'; +import styled from 'styled-components'; + +import { KpiHostsData, KpiNetworkData } from '../../../graphql/types'; +import { AreaChart } from '../charts/areachart'; +import { BarChart } from '../charts/barchart'; +import { ChartSeriesData, ChartData, ChartSeriesConfigs, UpdateDateRange } from '../charts/common'; +import { histogramDateTimeFormatter } from '../utils'; +import { getEmptyTagValue } from '../empty_value'; + +import { InspectButton, InspectButtonContainer } from '../inspect'; + +const FlexItem = styled(EuiFlexItem)` + min-width: 0; +`; + +FlexItem.displayName = 'FlexItem'; + +const StatValue = styled(EuiTitle)` + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +StatValue.displayName = 'StatValue'; + +interface StatItem { + color?: string; + description?: string; + icon?: IconType; + key: string; + name?: string; + value: number | undefined | null; +} + +export interface StatItems { + areachartConfigs?: ChartSeriesConfigs; + barchartConfigs?: ChartSeriesConfigs; + description?: string; + enableAreaChart?: boolean; + enableBarChart?: boolean; + fields: StatItem[]; + grow?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | true | false | null; + index: number; + key: string; + statKey?: string; +} + +export interface StatItemsProps extends StatItems { + areaChart?: ChartSeriesData[]; + barChart?: ChartSeriesData[]; + from: number; + id: string; + narrowDateRange: UpdateDateRange; + to: number; +} + +export const numberFormatter = (value: string | number): string => value.toLocaleString(); +const statItemBarchartRotation: Rotation = 90; +const statItemChartCustomHeight = 74; + +export const areachartConfigs = (config?: { + xTickFormatter: (value: number) => string; + onBrushEnd?: BrushEndListener; +}) => ({ + series: { + xScaleType: ScaleType.Time, + yScaleType: ScaleType.Linear, + }, + axis: { + xTickFormatter: get('xTickFormatter', config), + yTickFormatter: numberFormatter, + }, + settings: { + onBrushEnd: getOr(() => {}, 'onBrushEnd', config), + }, + customHeight: statItemChartCustomHeight, +}); + +export const barchartConfigs = (config?: { onElementClick?: ElementClickListener }) => ({ + series: { + xScaleType: ScaleType.Ordinal, + yScaleType: ScaleType.Linear, + stackAccessors: ['y0'], + }, + axis: { + xTickFormatter: numberFormatter, + }, + settings: { + onElementClick: getOr(() => {}, 'onElementClick', config), + rotation: statItemBarchartRotation, + }, + customHeight: statItemChartCustomHeight, +}); + +export const addValueToFields = ( + fields: StatItem[], + data: KpiHostsData | KpiNetworkData +): StatItem[] => fields.map(field => ({ ...field, value: get(field.key, data) })); + +export const addValueToAreaChart = ( + fields: StatItem[], + data: KpiHostsData | KpiNetworkData +): ChartSeriesData[] => + fields + .filter(field => get(`${field.key}Histogram`, data) != null) + .map(field => ({ + ...field, + value: get(`${field.key}Histogram`, data), + key: `${field.key}Histogram`, + })); + +export const addValueToBarChart = ( + fields: StatItem[], + data: KpiHostsData | KpiNetworkData +): ChartSeriesData[] => { + if (fields.length === 0) return []; + return fields.reduce((acc: ChartSeriesData[], field: StatItem, idx: number) => { + const { key, color } = field; + const y: number | null = getOr(null, key, data); + const x: string = get(`${idx}.name`, fields) || getOr('', `${idx}.description`, fields); + const value: [ChartData] = [ + { + x, + y, + g: key, + y0: 0, + }, + ]; + + return [ + ...acc, + { + key, + color, + value, + }, + ]; + }, []); +}; + +export const useKpiMatrixStatus = ( + mappings: Readonly, + data: KpiHostsData | KpiNetworkData, + id: string, + from: number, + to: number, + narrowDateRange: UpdateDateRange +): StatItemsProps[] => { + const [statItemsProps, setStatItemsProps] = useState(mappings as StatItemsProps[]); + + useEffect(() => { + setStatItemsProps( + mappings.map(stat => { + return { + ...stat, + areaChart: stat.enableAreaChart ? addValueToAreaChart(stat.fields, data) : undefined, + barChart: stat.enableBarChart ? addValueToBarChart(stat.fields, data) : undefined, + fields: addValueToFields(stat.fields, data), + id, + key: `kpi-summary-${stat.key}`, + statKey: `${stat.key}`, + from, + to, + narrowDateRange, + }; + }) + ); + }, [data]); + + return statItemsProps; +}; + +export const StatItemsComponent = React.memo( + ({ + areaChart, + barChart, + description, + enableAreaChart, + enableBarChart, + fields, + from, + grow, + id, + index, + narrowDateRange, + statKey = 'item', + to, + }) => { + const isBarChartDataAvailable = + barChart && + barChart.length && + barChart.every(item => item.value != null && item.value.length > 0); + const isAreaChartDataAvailable = + areaChart && + areaChart.length && + areaChart.every(item => item.value != null && item.value.length > 0); + + return ( + + + + + + +
{description}
+
+
+ + + +
+ + + {fields.map(field => ( + + + {(isAreaChartDataAvailable || isBarChartDataAvailable) && field.icon && ( + + + + )} + + + +

+ {field.value != null ? field.value.toLocaleString() : getEmptyTagValue()}{' '} + {field.description} +

+
+
+
+
+ ))} +
+ + {(enableAreaChart || enableBarChart) && } + + {enableBarChart && ( + + + + )} + + {enableAreaChart && from != null && to != null && ( + + + + )} + +
+
+
+ ); + } +); + +StatItemsComponent.displayName = 'StatItemsComponent'; diff --git a/x-pack/plugins/siem/public/components/subtitle/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/common/components/subtitle/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/subtitle/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/subtitle/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/subtitle/index.test.tsx b/x-pack/plugins/siem/public/common/components/subtitle/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/subtitle/index.test.tsx rename to x-pack/plugins/siem/public/common/components/subtitle/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/subtitle/index.tsx b/x-pack/plugins/siem/public/common/components/subtitle/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/subtitle/index.tsx rename to x-pack/plugins/siem/public/common/components/subtitle/index.tsx diff --git a/x-pack/plugins/siem/public/common/components/super_date_picker/index.test.tsx b/x-pack/plugins/siem/public/common/components/super_date_picker/index.test.tsx new file mode 100644 index 00000000000000..ba4848923b2afa --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/super_date_picker/index.test.tsx @@ -0,0 +1,443 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; +import { Provider as ReduxStoreProvider } from 'react-redux'; + +import { DEFAULT_TIMEPICKER_QUICK_RANGES } from '../../../../common/constants'; +import { useUiSetting$ } from '../../lib/kibana'; +import { apolloClientObservable, mockGlobalState, SUB_PLUGINS_REDUCER } from '../../mock'; +import { createUseUiSetting$Mock } from '../../mock/kibana_react'; +import { createStore, State } from '../../store'; + +import { SuperDatePicker, makeMapStateToProps } from '.'; +import { cloneDeep } from 'lodash/fp'; + +jest.mock('../../lib/kibana'); +const mockUseUiSetting$ = useUiSetting$ as jest.Mock; +const timepickerRanges = [ + { + from: 'now/d', + to: 'now/d', + display: 'Today', + }, + { + from: 'now/w', + to: 'now/w', + display: 'This week', + }, + { + from: 'now-15m', + to: 'now', + display: 'Last 15 minutes', + }, + { + from: 'now-30m', + to: 'now', + display: 'Last 30 minutes', + }, + { + from: 'now-1h', + to: 'now', + display: 'Last 1 hour', + }, + { + from: 'now-24h', + to: 'now', + display: 'Last 24 hours', + }, + { + from: 'now-7d', + to: 'now', + display: 'Last 7 days', + }, + { + from: 'now-30d', + to: 'now', + display: 'Last 30 days', + }, + { + from: 'now-90d', + to: 'now', + display: 'Last 90 days', + }, + { + from: 'now-1y', + to: 'now', + display: 'Last 1 year', + }, +]; + +describe('SIEM Super Date Picker', () => { + describe('#SuperDatePicker', () => { + const state: State = mockGlobalState; + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + + beforeEach(() => { + jest.clearAllMocks(); + store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + mockUseUiSetting$.mockImplementation((key, defaultValue) => { + const useUiSetting$Mock = createUseUiSetting$Mock(); + + return key === DEFAULT_TIMEPICKER_QUICK_RANGES + ? [timepickerRanges, jest.fn()] + : useUiSetting$Mock(key, defaultValue); + }); + }); + + describe('Pick Relative Date', () => { + let wrapper = mount( + + + + ); + beforeEach(() => { + wrapper = mount( + + + + ); + wrapper + .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') + .first() + .simulate('click'); + wrapper.update(); + + wrapper + .find('button.euiQuickSelect__applyButton') + .first() + .simulate('click'); + wrapper.update(); + }); + + test('Make Sure it is relative date', () => { + expect(store.getState().inputs.global.timerange.kind).toBe('relative'); + }); + + test('Make Sure it is last 24 hours date', () => { + expect(store.getState().inputs.global.timerange.fromStr).toBe('now-24h'); + expect(store.getState().inputs.global.timerange.toStr).toBe('now'); + }); + + test('Make Sure it is Today date', () => { + wrapper + .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') + .first() + .simulate('click'); + wrapper.update(); + + wrapper + .find('[data-test-subj="superDatePickerCommonlyUsed_Today"]') + .first() + .simulate('click'); + wrapper.update(); + expect(store.getState().inputs.global.timerange.fromStr).toBe('now/d'); + expect(store.getState().inputs.global.timerange.toStr).toBe('now/d'); + }); + + test('Make Sure to (end date) is superior than from (start date)', () => { + expect(store.getState().inputs.global.timerange.to).toBeGreaterThan( + store.getState().inputs.global.timerange.from + ); + }); + }); + + describe('Recently used date ranges', () => { + let wrapper = mount( + + + + ); + beforeEach(() => { + wrapper = mount( + + + + ); + wrapper + .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') + .first() + .simulate('click'); + wrapper.update(); + + wrapper + .find('[data-test-subj="superDatePickerCommonlyUsed_Today"]') + .first() + .simulate('click'); + wrapper.update(); + }); + + test('Today is in Recently used date ranges', () => { + expect( + wrapper + .find('div.euiQuickSelectPopover__section') + .at(1) + .text() + ).toBe('Today'); + }); + + test('Today and Last 24 hours are in Recently used date ranges', () => { + wrapper + .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') + .first() + .simulate('click'); + wrapper.update(); + + wrapper + .find('button.euiQuickSelect__applyButton') + .first() + .simulate('click'); + wrapper.update(); + + expect( + wrapper + .find('div.euiQuickSelectPopover__section') + .at(1) + .text() + ).toBe('Last 24 hoursToday'); + }); + + test('Make sure that it does not add any duplicate if you click again on today', () => { + wrapper + .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') + .first() + .simulate('click'); + wrapper.update(); + + wrapper + .find('[data-test-subj="superDatePickerCommonlyUsed_Today"]') + .first() + .simulate('click'); + wrapper.update(); + + expect( + wrapper + .find('div.euiQuickSelectPopover__section') + .at(1) + .text() + ).toBe('Today'); + }); + }); + + describe('Refresh Every', () => { + let wrapper = mount( + + + + ); + beforeEach(() => { + wrapper = mount( + + + + ); + wrapper + .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') + .first() + .simulate('click'); + wrapper.update(); + + const wrapperFixedEuiFieldSearch = wrapper.find( + 'input[data-test-subj="superDatePickerRefreshIntervalInput"]' + ); + + wrapperFixedEuiFieldSearch.simulate('change', { target: { value: '2' } }); + wrapper.update(); + + wrapper + .find('[data-test-subj="superDatePickerToggleRefreshButton"]') + .first() + .simulate('click'); + wrapper.update(); + }); + + test('Make sure the duration get updated to 2 minutes === 120000ms', () => { + expect(store.getState().inputs.global.policy.duration).toEqual(120000); + }); + + test('Make sure the stream live started', () => { + expect(store.getState().inputs.global.policy.kind).toBe('interval'); + }); + + test('Make sure we can stop the stream live', () => { + wrapper + .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') + .first() + .simulate('click'); + wrapper.update(); + + wrapper + .find('[data-test-subj="superDatePickerToggleRefreshButton"]') + .first() + .simulate('click'); + wrapper.update(); + + expect(store.getState().inputs.global.policy.kind).toBe('manual'); + }); + }); + + describe('Pick Absolute Date', () => { + let wrapper = mount( + + + + ); + beforeEach(() => { + wrapper = mount( + + + + ); + wrapper + .find('[data-test-subj="superDatePickerShowDatesButton"]') + .first() + .simulate('click'); + wrapper.update(); + + wrapper + .find('[data-test-subj="superDatePickerstartDatePopoverButton"]') + .first() + .simulate('click'); + wrapper.update(); + + wrapper + .find('[data-test-subj="superDatePickerAbsoluteTab"]') + .first() + .simulate('click'); + wrapper.update(); + + wrapper + .find('button.react-datepicker__navigation--previous') + .first() + .simulate('click'); + wrapper.update(); + + wrapper + .find('div.react-datepicker__day') + .at(1) + .simulate('click'); + wrapper.update(); + + wrapper + .find('button[data-test-subj="superDatePickerApplyTimeButton"]') + .first() + .simulate('click'); + wrapper.update(); + }); + }); + + describe('#makeMapStateToProps', () => { + test('it should return the same shallow references given the same input twice', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const props2 = mapStateToProps(state, { id: 'global' }); + Object.keys(props1).forEach(key => { + expect((props1 as Record)[key]).toBe((props2 as Record)[key]); + }); + }); + + test('it should not return the same reference if policy kind is different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.policy.kind = 'interval'; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.policy).not.toBe(props2.policy); + }); + + test('it should not return the same reference if duration is different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.policy.duration = 99999; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.duration).not.toBe(props2.duration); + }); + + test('it should not return the same reference if timerange kind is different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.timerange.kind = 'absolute'; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.kind).not.toBe(props2.kind); + }); + + test('it should not return the same reference if timerange from is different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.timerange.from = 999; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.start).not.toBe(props2.start); + }); + + test('it should not return the same reference if timerange to is different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.timerange.to = 999; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.end).not.toBe(props2.end); + }); + + test('it should not return the same reference of toStr if toStr different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.timerange.toStr = 'some other string'; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.toStr).not.toBe(props2.toStr); + }); + + test('it should not return the same reference of fromStr if fromStr different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.timerange.fromStr = 'some other string'; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.fromStr).not.toBe(props2.fromStr); + }); + + test('it should not return the same reference of isLoadingSelector if the query different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.queries = [ + { + loading: true, + id: '1', + inspect: { dsl: [], response: [] }, + isInspected: false, + refetch: null, + selectedInspectIndex: 0, + }, + ]; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.isLoading).not.toBe(props2.isLoading); + }); + + test('it should not return the same reference of refetchSelector if the query different', () => { + const mapStateToProps = makeMapStateToProps(); + const props1 = mapStateToProps(state, { id: 'global' }); + const clone = cloneDeep(state); + clone.inputs.global.queries = [ + { + loading: true, + id: '1', + inspect: { dsl: [], response: [] }, + isInspected: false, + refetch: null, + selectedInspectIndex: 0, + }, + ]; + const props2 = mapStateToProps(clone, { id: 'global' }); + expect(props1.queries).not.toBe(props2.queries); + }); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/common/components/super_date_picker/index.tsx b/x-pack/plugins/siem/public/common/components/super_date_picker/index.tsx new file mode 100644 index 00000000000000..d1936ac61e26b5 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/super_date_picker/index.tsx @@ -0,0 +1,314 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import dateMath from '@elastic/datemath'; +import { + EuiSuperDatePicker, + OnRefreshChangeProps, + EuiSuperDatePickerRecentRange, + OnRefreshProps, + OnTimeChangeProps, +} from '@elastic/eui'; +import { getOr, take, isEmpty } from 'lodash/fp'; +import React, { useState, useCallback } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import { Dispatch } from 'redux'; + +import { DEFAULT_TIMEPICKER_QUICK_RANGES } from '../../../../common/constants'; +import { timelineActions } from '../../../timelines/store/timeline'; +import { useUiSetting$ } from '../../lib/kibana'; +import { inputsModel, State } from '../../store'; +import { inputsActions } from '../../store/actions'; +import { InputsModelId } from '../../store/inputs/constants'; +import { + policySelector, + durationSelector, + kindSelector, + startSelector, + endSelector, + fromStrSelector, + toStrSelector, + isLoadingSelector, + queriesSelector, + kqlQuerySelector, +} from './selectors'; +import { InputsRange } from '../../store/inputs/model'; + +const MAX_RECENTLY_USED_RANGES = 9; + +interface Range { + from: string; + to: string; + display: string; +} + +interface UpdateReduxTime extends OnTimeChangeProps { + id: InputsModelId; + kql?: inputsModel.GlobalKqlQuery | undefined; + timelineId?: string; +} + +interface ReturnUpdateReduxTime { + kqlHaveBeenUpdated: boolean; +} + +export type DispatchUpdateReduxTime = ({ + end, + id, + isQuickSelection, + kql, + start, + timelineId, +}: UpdateReduxTime) => ReturnUpdateReduxTime; + +interface OwnProps { + disabled?: boolean; + id: InputsModelId; + timelineId?: string; +} + +export type SuperDatePickerProps = OwnProps & PropsFromRedux; + +export const SuperDatePickerComponent = React.memo( + ({ + duration, + end, + fromStr, + id, + isLoading, + kind, + kqlQuery, + policy, + queries, + setDuration, + start, + startAutoReload, + stopAutoReload, + timelineId, + toStr, + updateReduxTime, + }) => { + const [isQuickSelection, setIsQuickSelection] = useState(true); + const [recentlyUsedRanges, setRecentlyUsedRanges] = useState( + [] + ); + const onRefresh = useCallback( + ({ start: newStart, end: newEnd }: OnRefreshProps): void => { + const { kqlHaveBeenUpdated } = updateReduxTime({ + end: newEnd, + id, + isInvalid: false, + isQuickSelection, + kql: kqlQuery, + start: newStart, + timelineId, + }); + const currentStart = formatDate(newStart); + const currentEnd = isQuickSelection + ? formatDate(newEnd, { roundUp: true }) + : formatDate(newEnd); + if ( + !kqlHaveBeenUpdated && + (!isQuickSelection || (start === currentStart && end === currentEnd)) + ) { + refetchQuery(queries); + } + }, + [end, id, isQuickSelection, kqlQuery, start, timelineId] + ); + + const onRefreshChange = useCallback( + ({ isPaused, refreshInterval }: OnRefreshChangeProps): void => { + if (duration !== refreshInterval) { + setDuration({ id, duration: refreshInterval }); + } + + if (isPaused && policy === 'interval') { + stopAutoReload({ id }); + } else if (!isPaused && policy === 'manual') { + startAutoReload({ id }); + } + + if (!isPaused && (!isQuickSelection || (isQuickSelection && toStr !== 'now'))) { + refetchQuery(queries); + } + }, + [id, isQuickSelection, duration, policy, toStr] + ); + + const refetchQuery = (newQueries: inputsModel.GlobalGraphqlQuery[]) => { + newQueries.forEach(q => q.refetch && (q.refetch as inputsModel.Refetch)()); + }; + + const onTimeChange = useCallback( + ({ + start: newStart, + end: newEnd, + isQuickSelection: newIsQuickSelection, + isInvalid, + }: OnTimeChangeProps) => { + if (!isInvalid) { + updateReduxTime({ + end: newEnd, + id, + isInvalid, + isQuickSelection: newIsQuickSelection, + kql: kqlQuery, + start: newStart, + timelineId, + }); + const newRecentlyUsedRanges = [ + { start: newStart, end: newEnd }, + ...take( + MAX_RECENTLY_USED_RANGES, + recentlyUsedRanges.filter( + recentlyUsedRange => + !(recentlyUsedRange.start === newStart && recentlyUsedRange.end === newEnd) + ) + ), + ]; + + setRecentlyUsedRanges(newRecentlyUsedRanges); + setIsQuickSelection(newIsQuickSelection); + } + }, + [recentlyUsedRanges, kqlQuery] + ); + + const endDate = kind === 'relative' ? toStr : new Date(end).toISOString(); + const startDate = kind === 'relative' ? fromStr : new Date(start).toISOString(); + + const [quickRanges] = useUiSetting$(DEFAULT_TIMEPICKER_QUICK_RANGES); + const commonlyUsedRanges = isEmpty(quickRanges) + ? [] + : quickRanges.map(({ from, to, display }) => ({ + start: from, + end: to, + label: display, + })); + + return ( + + ); + } +); + +export const formatDate = ( + date: string, + options?: { + roundUp?: boolean; + } +) => { + const momentDate = dateMath.parse(date, options); + return momentDate != null && momentDate.isValid() ? momentDate.valueOf() : 0; +}; + +export const dispatchUpdateReduxTime = (dispatch: Dispatch) => ({ + end, + id, + isQuickSelection, + kql, + start, + timelineId, +}: UpdateReduxTime): ReturnUpdateReduxTime => { + const fromDate = formatDate(start); + let toDate = formatDate(end, { roundUp: true }); + if (isQuickSelection) { + dispatch( + inputsActions.setRelativeRangeDatePicker({ + id, + fromStr: start, + toStr: end, + from: fromDate, + to: toDate, + }) + ); + } else { + toDate = formatDate(end); + dispatch( + inputsActions.setAbsoluteRangeDatePicker({ + id, + from: formatDate(start), + to: formatDate(end), + }) + ); + } + if (timelineId != null) { + dispatch( + timelineActions.updateRange({ + id: timelineId, + start: fromDate, + end: toDate, + }) + ); + } + if (kql) { + return { + kqlHaveBeenUpdated: kql.refetch(dispatch), + }; + } + + return { + kqlHaveBeenUpdated: false, + }; +}; + +export const makeMapStateToProps = () => { + const getDurationSelector = durationSelector(); + const getEndSelector = endSelector(); + const getFromStrSelector = fromStrSelector(); + const getIsLoadingSelector = isLoadingSelector(); + const getKindSelector = kindSelector(); + const getKqlQuerySelector = kqlQuerySelector(); + const getPolicySelector = policySelector(); + const getQueriesSelector = queriesSelector(); + const getStartSelector = startSelector(); + const getToStrSelector = toStrSelector(); + return (state: State, { id }: OwnProps) => { + const inputsRange: InputsRange = getOr({}, `inputs.${id}`, state); + return { + duration: getDurationSelector(inputsRange), + end: getEndSelector(inputsRange), + fromStr: getFromStrSelector(inputsRange), + isLoading: getIsLoadingSelector(inputsRange), + kind: getKindSelector(inputsRange), + kqlQuery: getKqlQuerySelector(inputsRange) as inputsModel.GlobalKqlQuery, + policy: getPolicySelector(inputsRange), + queries: getQueriesSelector(inputsRange) as inputsModel.GlobalGraphqlQuery[], + start: getStartSelector(inputsRange), + toStr: getToStrSelector(inputsRange), + }; + }; +}; + +SuperDatePickerComponent.displayName = 'SuperDatePickerComponent'; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + startAutoReload: ({ id }: { id: InputsModelId }) => + dispatch(inputsActions.startAutoReload({ id })), + stopAutoReload: ({ id }: { id: InputsModelId }) => dispatch(inputsActions.stopAutoReload({ id })), + setDuration: ({ id, duration }: { id: InputsModelId; duration: number }) => + dispatch(inputsActions.setDuration({ id, duration })), + updateReduxTime: dispatchUpdateReduxTime(dispatch), +}); + +export const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const SuperDatePicker = connector(SuperDatePickerComponent); diff --git a/x-pack/plugins/siem/public/components/super_date_picker/selectors.test.ts b/x-pack/plugins/siem/public/common/components/super_date_picker/selectors.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/super_date_picker/selectors.test.ts rename to x-pack/plugins/siem/public/common/components/super_date_picker/selectors.test.ts diff --git a/x-pack/plugins/siem/public/components/super_date_picker/selectors.ts b/x-pack/plugins/siem/public/common/components/super_date_picker/selectors.ts similarity index 100% rename from x-pack/plugins/siem/public/components/super_date_picker/selectors.ts rename to x-pack/plugins/siem/public/common/components/super_date_picker/selectors.ts diff --git a/x-pack/plugins/siem/public/components/tables/__snapshots__/helpers.test.tsx.snap b/x-pack/plugins/siem/public/common/components/tables/__snapshots__/helpers.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/tables/__snapshots__/helpers.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/tables/__snapshots__/helpers.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/tables/helpers.test.tsx b/x-pack/plugins/siem/public/common/components/tables/helpers.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/tables/helpers.test.tsx rename to x-pack/plugins/siem/public/common/components/tables/helpers.test.tsx diff --git a/x-pack/plugins/siem/public/common/components/tables/helpers.tsx b/x-pack/plugins/siem/public/common/components/tables/helpers.tsx new file mode 100644 index 00000000000000..c9d90504c36dbd --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/tables/helpers.tsx @@ -0,0 +1,240 @@ +/* + * 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 { EuiLink, EuiPopover, EuiToolTip, EuiText, EuiTextColor } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useState } from 'react'; +import styled from 'styled-components'; + +import { DragEffects, DraggableWrapper } from '../drag_and_drop/draggable_wrapper'; +import { escapeDataProviderId } from '../drag_and_drop/helpers'; +import { defaultToEmptyTag, getEmptyTagValue } from '../empty_value'; +import { MoreRowItems, Spacer } from '../page'; +import { IS_OPERATOR } from '../../../timelines/components/timeline/data_providers/data_provider'; +import { Provider } from '../../../timelines/components/timeline/data_providers/provider'; + +const Subtext = styled.div` + font-size: ${props => props.theme.eui.euiFontSizeXS}; +`; + +export const getRowItemDraggable = ({ + rowItem, + attrName, + idPrefix, + render, + dragDisplayValue, +}: { + rowItem: string | null | undefined; + attrName: string; + idPrefix: string; + render?: (item: string) => JSX.Element; + displayCount?: number; + dragDisplayValue?: string; + maxOverflow?: number; +}): JSX.Element => { + if (rowItem != null) { + const id = escapeDataProviderId(`${idPrefix}-${attrName}-${rowItem}`); + return ( + + snapshot.isDragging ? ( + + + + ) : ( + <>{render ? render(rowItem) : defaultToEmptyTag(rowItem)} + ) + } + /> + ); + } else { + return getEmptyTagValue(); + } +}; + +export const getRowItemDraggables = ({ + rowItems, + attrName, + idPrefix, + render, + dragDisplayValue, + displayCount = 5, + maxOverflow = 5, +}: { + rowItems: string[] | null | undefined; + attrName: string; + idPrefix: string; + render?: (item: string) => JSX.Element; + displayCount?: number; + dragDisplayValue?: string; + maxOverflow?: number; +}): JSX.Element => { + if (rowItems != null && rowItems.length > 0) { + const draggables = rowItems.slice(0, displayCount).map((rowItem, index) => { + const id = escapeDataProviderId(`${idPrefix}-${attrName}-${rowItem}-${index}`); + return ( + + {index !== 0 && ( + <> + {','} + + + )} + + snapshot.isDragging ? ( + + + + ) : ( + <>{render ? render(rowItem) : defaultToEmptyTag(rowItem)} + ) + } + /> + + ); + }); + + return draggables.length > 0 ? ( + <> + {draggables} {getRowItemOverflow(rowItems, idPrefix, displayCount, maxOverflow)} + + ) : ( + getEmptyTagValue() + ); + } else { + return getEmptyTagValue(); + } +}; + +export const getRowItemOverflow = ( + rowItems: string[], + idPrefix: string, + overflowIndexStart = 5, + maxOverflowItems = 5 +): JSX.Element => { + return ( + <> + {rowItems.length > overflowIndexStart && ( + + +
    + {rowItems + .slice(overflowIndexStart, overflowIndexStart + maxOverflowItems) + .map(rowItem => ( +
  • {defaultToEmptyTag(rowItem)}
  • + ))} +
+ + {rowItems.length > overflowIndexStart + maxOverflowItems && ( +

+ + {rowItems.length - overflowIndexStart - maxOverflowItems}{' '} + + +

+ )} +
+
+ )} + + ); +}; + +export const PopoverComponent = ({ + children, + count, + idPrefix, +}: { + children: React.ReactNode; + count: number; + idPrefix: string; +}) => { + const [isOpen, setIsOpen] = useState(false); + + return ( + + setIsOpen(!isOpen)}>{`+${count} More`}} + closePopover={() => setIsOpen(!isOpen)} + id={`${idPrefix}-popover`} + isOpen={isOpen} + > + {children} + + + ); +}; + +PopoverComponent.displayName = 'PopoverComponent'; + +export const Popover = React.memo(PopoverComponent); + +Popover.displayName = 'Popover'; + +export const OverflowFieldComponent = ({ + value, + showToolTip = true, + overflowLength = 50, +}: { + value: string; + showToolTip?: boolean; + overflowLength?: number; +}) => ( + + {showToolTip ? ( + + <>{value.substring(0, overflowLength)} + + ) : ( + <>{value.substring(0, overflowLength)} + )} + {value.length > overflowLength && ( + + + + )} + +); + +OverflowFieldComponent.displayName = 'OverflowFieldComponent'; + +export const OverflowField = React.memo(OverflowFieldComponent); + +OverflowField.displayName = 'OverflowField'; diff --git a/x-pack/plugins/siem/public/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap b/x-pack/plugins/siem/public/common/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/toasters/__snapshots__/modal_all_errors.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/toasters/errors.ts b/x-pack/plugins/siem/public/common/components/toasters/errors.ts similarity index 100% rename from x-pack/plugins/siem/public/components/toasters/errors.ts rename to x-pack/plugins/siem/public/common/components/toasters/errors.ts diff --git a/x-pack/plugins/siem/public/components/toasters/index.test.tsx b/x-pack/plugins/siem/public/common/components/toasters/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/toasters/index.test.tsx rename to x-pack/plugins/siem/public/common/components/toasters/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/toasters/index.tsx b/x-pack/plugins/siem/public/common/components/toasters/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/toasters/index.tsx rename to x-pack/plugins/siem/public/common/components/toasters/index.tsx diff --git a/x-pack/plugins/siem/public/components/toasters/modal_all_errors.test.tsx b/x-pack/plugins/siem/public/common/components/toasters/modal_all_errors.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/toasters/modal_all_errors.test.tsx rename to x-pack/plugins/siem/public/common/components/toasters/modal_all_errors.test.tsx diff --git a/x-pack/plugins/siem/public/components/toasters/modal_all_errors.tsx b/x-pack/plugins/siem/public/common/components/toasters/modal_all_errors.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/toasters/modal_all_errors.tsx rename to x-pack/plugins/siem/public/common/components/toasters/modal_all_errors.tsx diff --git a/x-pack/plugins/siem/public/components/toasters/translations.ts b/x-pack/plugins/siem/public/common/components/toasters/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/toasters/translations.ts rename to x-pack/plugins/siem/public/common/components/toasters/translations.ts diff --git a/x-pack/plugins/siem/public/components/toasters/utils.test.ts b/x-pack/plugins/siem/public/common/components/toasters/utils.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/toasters/utils.test.ts rename to x-pack/plugins/siem/public/common/components/toasters/utils.test.ts diff --git a/x-pack/plugins/siem/public/components/toasters/utils.ts b/x-pack/plugins/siem/public/common/components/toasters/utils.ts similarity index 100% rename from x-pack/plugins/siem/public/components/toasters/utils.ts rename to x-pack/plugins/siem/public/common/components/toasters/utils.ts diff --git a/x-pack/plugins/siem/public/components/top_n/helpers.test.tsx b/x-pack/plugins/siem/public/common/components/top_n/helpers.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/top_n/helpers.test.tsx rename to x-pack/plugins/siem/public/common/components/top_n/helpers.test.tsx diff --git a/x-pack/plugins/siem/public/common/components/top_n/helpers.ts b/x-pack/plugins/siem/public/common/components/top_n/helpers.ts new file mode 100644 index 00000000000000..a4226cc58530aa --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/top_n/helpers.ts @@ -0,0 +1,66 @@ +/* + * 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 { EventType } from '../../../timelines/store/timeline/model'; + +import * as i18n from './translations'; + +export interface TopNOption { + inputDisplay: string; + value: EventType; + 'data-test-subj': string; +} + +/** A (stable) array containing only the 'All events' option */ +export const allEvents: TopNOption[] = [ + { + value: 'all', + inputDisplay: i18n.ALL_EVENTS, + 'data-test-subj': 'option-all', + }, +]; + +/** A (stable) array containing only the 'Raw events' option */ +export const rawEvents: TopNOption[] = [ + { + value: 'raw', + inputDisplay: i18n.RAW_EVENTS, + 'data-test-subj': 'option-raw', + }, +]; + +/** A (stable) array containing only the 'Signal events' option */ +export const signalEvents: TopNOption[] = [ + { + value: 'signal', + inputDisplay: i18n.SIGNAL_EVENTS, + 'data-test-subj': 'option-signal', + }, +]; + +/** A (stable) array containing the default Top N options */ +export const defaultOptions = [...rawEvents, ...signalEvents]; + +/** + * Returns the options to be displayed in a Top N view select. When + * an `activeTimelineEventType` is provided, an array containing + * just one option (corresponding to `activeTimelineEventType`) + * will be returned, to ensure the data displayed in the Top N + * is always in sync with the `EventType` chosen by the user in + * the active timeline. + */ +export const getOptions = (activeTimelineEventType?: EventType): TopNOption[] => { + switch (activeTimelineEventType) { + case 'all': + return allEvents; + case 'raw': + return rawEvents; + case 'signal': + return signalEvents; + default: + return defaultOptions; + } +}; diff --git a/x-pack/plugins/siem/public/common/components/top_n/index.test.tsx b/x-pack/plugins/siem/public/common/components/top_n/index.test.tsx new file mode 100644 index 00000000000000..24d1939d9319d3 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/top_n/index.test.tsx @@ -0,0 +1,387 @@ +/* + * 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 { mount, ReactWrapper } from 'enzyme'; +import React from 'react'; + +import { mockBrowserFields } from '../../containers/source/mock'; +import { + apolloClientObservable, + mockGlobalState, + TestProviders, + SUB_PLUGINS_REDUCER, +} from '../../mock'; +import { createKibanaCoreStartMock } from '../../mock/kibana_core'; +import { FilterManager } from '../../../../../../../src/plugins/data/public'; +import { createStore, State } from '../../store'; +import { + TimelineContext, + TimelineTypeContext, +} from '../../../timelines/components/timeline/timeline_context'; + +import { Props } from './top_n'; +import { ACTIVE_TIMELINE_REDUX_ID, StatefulTopN } from '.'; + +jest.mock('../../lib/kibana'); + +const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; + +const field = 'process.name'; +const value = 'nice'; + +const state: State = { + ...mockGlobalState, + inputs: { + ...mockGlobalState.inputs, + global: { + ...mockGlobalState.inputs.global, + query: { + query: 'host.name : end*', + language: 'kuery', + }, + filters: [ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'host.os.name', + params: { + query: 'Linux', + }, + }, + query: { + match: { + 'host.os.name': { + query: 'Linux', + type: 'phrase', + }, + }, + }, + }, + ], + }, + timeline: { + ...mockGlobalState.inputs.timeline, + timerange: { + kind: 'relative', + fromStr: 'now-24h', + toStr: 'now', + from: 1586835969047, + to: 1586922369047, + }, + }, + }, + timeline: { + ...mockGlobalState.timeline, + timelineById: { + [ACTIVE_TIMELINE_REDUX_ID]: { + ...mockGlobalState.timeline.timelineById.test, + id: ACTIVE_TIMELINE_REDUX_ID, + dataProviders: [ + { + id: + 'draggable-badge-default-draggable-netflow-renderer-timeline-1-_qpBe3EBD7k-aQQL7v7--_qpBe3EBD7k-aQQL7v7--network_transport-tcp', + name: 'tcp', + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'network.transport', + value: 'tcp', + operator: ':', + }, + and: [], + }, + ], + eventType: 'all', + filters: [ + { + meta: { + alias: null, + disabled: false, + key: 'source.port', + negate: false, + params: { + query: '30045', + }, + type: 'phrase', + }, + query: { + match: { + 'source.port': { + query: '30045', + type: 'phrase', + }, + }, + }, + }, + ], + kqlMode: 'filter', + kqlQuery: { + filterQuery: { + kuery: { + kind: 'kuery', + expression: 'host.name : *', + }, + serializedQuery: + '{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}}', + }, + filterQueryDraft: { + kind: 'kuery', + expression: 'host.name : *', + }, + }, + }, + }, + }, +}; +const store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + +describe('StatefulTopN', () => { + // Suppress warnings about "react-beautiful-dnd" + /* eslint-disable no-console */ + const originalError = console.error; + const originalWarn = console.warn; + beforeAll(() => { + console.warn = jest.fn(); + console.error = jest.fn(); + }); + afterAll(() => { + console.error = originalError; + console.warn = originalWarn; + }); + + describe('rendering in a global NON-timeline context', () => { + let wrapper: ReactWrapper; + + beforeEach(() => { + wrapper = mount( + + + + ); + }); + + test('it has undefined combinedQueries when rendering in a global context', () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.combinedQueries).toBeUndefined(); + }); + + test(`defaults to the 'Raw events' view when rendering in a global context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.defaultView).toEqual('raw'); + }); + + test(`provides a 'deleteQuery' when rendering in a global context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.deleteQuery).toBeDefined(); + }); + + test(`provides filters from Redux state (inputs > global > filters) when rendering in a global context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.filters).toEqual([ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'host.os.name', + params: { query: 'Linux' }, + }, + query: { match: { 'host.os.name': { query: 'Linux', type: 'phrase' } } }, + }, + ]); + }); + + test(`provides 'from' via GlobalTime when rendering in a global context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.from).toEqual(0); + }); + + test('provides the global query from Redux state (inputs > global > query) when rendering in a global context', () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.query).toEqual({ query: 'host.name : end*', language: 'kuery' }); + }); + + test(`provides a 'global' 'setAbsoluteRangeDatePickerTarget' when rendering in a global context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.setAbsoluteRangeDatePickerTarget).toEqual('global'); + }); + + test(`provides 'to' via GlobalTime when rendering in a global context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.to).toEqual(1); + }); + }); + + describe('rendering in a timeline context', () => { + let filterManager: FilterManager; + let wrapper: ReactWrapper; + + beforeEach(() => { + filterManager = new FilterManager(mockUiSettingsForFilterManager); + + wrapper = mount( + + + + + + + + ); + }); + + test('it has a combinedQueries value from Redux state composed of the timeline [data providers + kql + filter-bar-filters] when rendering in a timeline context', () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.combinedQueries).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"network.transport":"tcp"}}],"minimum_should_match":1}},{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}}]}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1586835969047}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1586922369047}}}],"minimum_should_match":1}}]}}]}},{"match_phrase":{"source.port":{"query":"30045"}}}],"should":[],"must_not":[]}}' + ); + }); + + test('it provides only one view option that matches the `eventType` from redux when rendering in the context of the active timeline', () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.defaultView).toEqual('all'); + }); + + test(`provides an undefined 'deleteQuery' when rendering in a timeline context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.deleteQuery).toBeUndefined(); + }); + + test(`provides empty filters when rendering in a timeline context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.filters).toEqual([]); + }); + + test(`provides 'from' via redux state (inputs > timeline > timerange) when rendering in a timeline context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.from).toEqual(1586835969047); + }); + + test('provides an empty query when rendering in a timeline context', () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.query).toEqual({ query: '', language: 'kuery' }); + }); + + test(`provides a 'timeline' 'setAbsoluteRangeDatePickerTarget' when rendering in a timeline context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.setAbsoluteRangeDatePickerTarget).toEqual('timeline'); + }); + + test(`provides 'to' via redux state (inputs > timeline > timerange) when rendering in a timeline context`, () => { + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.to).toEqual(1586922369047); + }); + }); + + test(`defaults to the 'Signals events' option when rendering in a NON-active timeline context (e.g. the Signals table on the Detections page) when 'documentType' from 'useTimelineTypeContext()' is 'signals'`, () => { + const filterManager = new FilterManager(mockUiSettingsForFilterManager); + const wrapper = mount( + + + + + + + + ); + + const props = wrapper + .find('[data-test-subj="top-n"]') + .first() + .props() as Props; + + expect(props.defaultView).toEqual('signal'); + }); +}); diff --git a/x-pack/plugins/siem/public/common/components/top_n/index.tsx b/x-pack/plugins/siem/public/common/components/top_n/index.tsx new file mode 100644 index 00000000000000..a71b27e0bd9cb4 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/top_n/index.tsx @@ -0,0 +1,167 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { connect, ConnectedProps } from 'react-redux'; + +import { GlobalTime } from '../../containers/global_time'; +import { BrowserFields, WithSource } from '../../containers/source'; +import { useKibana } from '../../lib/kibana'; +import { esQuery, Filter, Query } from '../../../../../../../src/plugins/data/public'; +import { inputsModel, inputsSelectors, State } from '../../store'; +import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions'; +import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; +import { timelineSelectors } from '../../../timelines/store/timeline'; +import { TimelineModel } from '../../../timelines/store/timeline/model'; +import { combineQueries } from '../../../timelines/components/timeline/helpers'; +import { useTimelineTypeContext } from '../../../timelines/components/timeline/timeline_context'; + +import { getOptions } from './helpers'; +import { TopN } from './top_n'; + +/** The currently active timeline always has this Redux ID */ +export const ACTIVE_TIMELINE_REDUX_ID = 'timeline-1'; + +const EMPTY_FILTERS: Filter[] = []; +const EMPTY_QUERY: Query = { query: '', language: 'kuery' }; + +const makeMapStateToProps = () => { + const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); + const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); + const getTimeline = timelineSelectors.getTimelineByIdSelector(); + const getInputsTimeline = inputsSelectors.getTimelineSelector(); + const getKqlQueryTimeline = timelineSelectors.getKqlFilterQuerySelector(); + + // The mapped Redux state provided to this component includes the global + // filters that appear at the top of most views in the app, and all the + // filters in the active timeline: + const mapStateToProps = (state: State) => { + const activeTimeline: TimelineModel = + getTimeline(state, ACTIVE_TIMELINE_REDUX_ID) ?? timelineDefaults; + const activeTimelineFilters = activeTimeline.filters ?? EMPTY_FILTERS; + const activeTimelineInput: inputsModel.InputsRange = getInputsTimeline(state); + + return { + activeTimelineEventType: activeTimeline.eventType, + activeTimelineFilters, + activeTimelineFrom: activeTimelineInput.timerange.from, + activeTimelineKqlQueryExpression: getKqlQueryTimeline(state, ACTIVE_TIMELINE_REDUX_ID), + activeTimelineTo: activeTimelineInput.timerange.to, + dataProviders: activeTimeline.dataProviders, + globalQuery: getGlobalQuerySelector(state), + globalFilters: getGlobalFiltersQuerySelector(state), + kqlMode: activeTimeline.kqlMode, + }; + }; + + return mapStateToProps; +}; + +const mapDispatchToProps = { setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker }; + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +interface OwnProps { + browserFields: BrowserFields; + field: string; + toggleTopN: () => void; + onFilterAdded?: () => void; + value?: string[] | string | null; +} +type PropsFromRedux = ConnectedProps; +type Props = OwnProps & PropsFromRedux; + +const StatefulTopNComponent: React.FC = ({ + activeTimelineEventType, + activeTimelineFilters, + activeTimelineFrom, + activeTimelineKqlQueryExpression, + activeTimelineTo, + browserFields, + dataProviders, + field, + globalFilters = EMPTY_FILTERS, + globalQuery = EMPTY_QUERY, + kqlMode, + onFilterAdded, + setAbsoluteRangeDatePicker, + toggleTopN, + value, +}) => { + const kibana = useKibana(); + + // Regarding data from useTimelineTypeContext: + // * `documentType` (e.g. 'signals') may only be populated in some views, + // e.g. the `Signals` view on the `Detections` page. + // * `id` (`timelineId`) may only be populated when we are rendered in the + // context of the active timeline. + // * `indexToAdd`, which enables the signals index to be appended to + // the `indexPattern` returned by `WithSource`, may only be populated when + // this component is rendered in the context of the active timeline. This + // behavior enables the 'All events' view by appending the signals index + // to the index pattern. + const { documentType, id: timelineId, indexToAdd } = useTimelineTypeContext(); + + const options = getOptions( + timelineId === ACTIVE_TIMELINE_REDUX_ID ? activeTimelineEventType : undefined + ); + + return ( + + {({ from, deleteQuery, setQuery, to }) => ( + + {({ indexPattern }) => ( + + )} + + )} + + ); +}; + +StatefulTopNComponent.displayName = 'StatefulTopNComponent'; + +export const StatefulTopN = connector(React.memo(StatefulTopNComponent)); diff --git a/x-pack/plugins/siem/public/components/top_n/top_n.test.tsx b/x-pack/plugins/siem/public/common/components/top_n/top_n.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/top_n/top_n.test.tsx rename to x-pack/plugins/siem/public/common/components/top_n/top_n.test.tsx diff --git a/x-pack/plugins/siem/public/components/top_n/top_n.tsx b/x-pack/plugins/siem/public/common/components/top_n/top_n.tsx similarity index 93% rename from x-pack/plugins/siem/public/components/top_n/top_n.tsx rename to x-pack/plugins/siem/public/common/components/top_n/top_n.tsx index d8dc63ef92ec60..0ccb7e1e72f1f5 100644 --- a/x-pack/plugins/siem/public/components/top_n/top_n.tsx +++ b/x-pack/plugins/siem/public/common/components/top_n/top_n.tsx @@ -9,12 +9,12 @@ import React, { useCallback, useMemo, useState } from 'react'; import styled from 'styled-components'; import { ActionCreator } from 'typescript-fsa'; -import { EventsByDataset } from '../../pages/overview/events_by_dataset'; -import { SignalsByCategory } from '../../pages/overview/signals_by_category'; -import { Filter, IIndexPattern, Query } from '../../../../../../src/plugins/data/public'; +import { EventsByDataset } from '../../../overview/components/events_by_dataset'; +import { SignalsByCategory } from '../../../overview/components/signals_by_category'; +import { Filter, IIndexPattern, Query } from '../../../../../../../src/plugins/data/public'; import { inputsModel } from '../../store'; import { InputsModelId } from '../../store/inputs/constants'; -import { EventType } from '../../store/timeline/model'; +import { EventType } from '../../../timelines/store/timeline/model'; import { TopNOption } from './helpers'; import * as i18n from './translations'; diff --git a/x-pack/plugins/siem/public/components/top_n/translations.ts b/x-pack/plugins/siem/public/common/components/top_n/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/top_n/translations.ts rename to x-pack/plugins/siem/public/common/components/top_n/translations.ts diff --git a/x-pack/plugins/siem/public/components/truncatable_text/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/common/components/truncatable_text/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/truncatable_text/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/truncatable_text/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/truncatable_text/index.test.tsx b/x-pack/plugins/siem/public/common/components/truncatable_text/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/truncatable_text/index.test.tsx rename to x-pack/plugins/siem/public/common/components/truncatable_text/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/truncatable_text/index.tsx b/x-pack/plugins/siem/public/common/components/truncatable_text/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/truncatable_text/index.tsx rename to x-pack/plugins/siem/public/common/components/truncatable_text/index.tsx diff --git a/x-pack/plugins/siem/public/components/url_state/constants.ts b/x-pack/plugins/siem/public/common/components/url_state/constants.ts similarity index 100% rename from x-pack/plugins/siem/public/components/url_state/constants.ts rename to x-pack/plugins/siem/public/common/components/url_state/constants.ts diff --git a/x-pack/plugins/siem/public/common/components/url_state/helpers.test.ts b/x-pack/plugins/siem/public/common/components/url_state/helpers.test.ts new file mode 100644 index 00000000000000..410bd62e3a708f --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/url_state/helpers.test.ts @@ -0,0 +1,37 @@ +/* + * 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 { navTabs } from '../../../app/home/home_navigations'; +import { getTitle } from './helpers'; +import { HostsType } from '../../../hosts/store/model'; + +describe('Helpers Url_State', () => { + describe('getTitle', () => { + test('host page name', () => { + const result = getTitle('hosts', undefined, navTabs); + expect(result).toEqual('Hosts'); + }); + test('network page name', () => { + const result = getTitle('network', undefined, navTabs); + expect(result).toEqual('Network'); + }); + test('overview page name', () => { + const result = getTitle('overview', undefined, navTabs); + expect(result).toEqual('Overview'); + }); + test('timelines page name', () => { + const result = getTitle('timelines', undefined, navTabs); + expect(result).toEqual('Timelines'); + }); + test('details page name', () => { + const result = getTitle('hosts', HostsType.details, navTabs); + expect(result).toEqual(HostsType.details); + }); + test('Not existing', () => { + const result = getTitle('IamHereButNotReally', undefined, navTabs); + expect(result).toEqual(''); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/common/components/url_state/helpers.ts b/x-pack/plugins/siem/public/common/components/url_state/helpers.ts new file mode 100644 index 00000000000000..8f13e4dd0cdcf7 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/url_state/helpers.ts @@ -0,0 +1,261 @@ +/* + * 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 { isEmpty } from 'lodash/fp'; +import { parse, stringify } from 'query-string'; +import { decode, encode } from 'rison-node'; +import * as H from 'history'; + +import { Query, Filter } from '../../../../../../../src/plugins/data/public'; +import { url } from '../../../../../../../src/plugins/kibana_utils/public'; + +import { SiemPageName } from '../../../app/types'; +import { inputsSelectors, State } from '../../store'; +import { UrlInputsModel } from '../../store/inputs/model'; +import { TimelineUrl } from '../../../timelines/store/timeline/model'; +import { timelineSelectors } from '../../../timelines/store/timeline'; +import { formatDate } from '../super_date_picker'; +import { NavTab } from '../navigation/types'; +import { CONSTANTS, UrlStateType } from './constants'; +import { ReplaceStateInLocation, UpdateUrlStateString } from './types'; + +export const decodeRisonUrlState = (value: string | undefined): T | null => { + try { + return value ? ((decode(value) as unknown) as T) : null; + } catch (error) { + if (error instanceof Error && error.message.startsWith('rison decoder error')) { + return null; + } + throw error; + } +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const encodeRisonUrlState = (state: any) => encode(state); + +export const getQueryStringFromLocation = (search: string) => search.substring(1); + +export const getParamFromQueryString = (queryString: string, key: string) => { + const parsedQueryString = parse(queryString, { sort: false }); + const queryParam = parsedQueryString[key]; + + return Array.isArray(queryParam) ? queryParam[0] : queryParam; +}; + +export const replaceStateKeyInQueryString = (stateKey: string, urlState: T) => ( + queryString: string +): string => { + const previousQueryValues = parse(queryString, { sort: false }); + if (urlState == null || (typeof urlState === 'string' && urlState === '')) { + delete previousQueryValues[stateKey]; + + return stringify(url.encodeQuery(previousQueryValues), { sort: false, encode: false }); + } + + // ಠ_ಠ Code was copied from x-pack/legacy/plugins/infra/public/utils/url_state.tsx ಠ_ಠ + // Remove this if these utilities are promoted to kibana core + const encodedUrlState = + typeof urlState !== 'undefined' ? encodeRisonUrlState(urlState) : undefined; + + return stringify( + url.encodeQuery({ + ...previousQueryValues, + [stateKey]: encodedUrlState, + }), + { sort: false, encode: false } + ); +}; + +export const replaceQueryStringInLocation = ( + location: H.Location, + queryString: string +): H.Location => { + if (queryString === getQueryStringFromLocation(location.search)) { + return location; + } else { + return { + ...location, + search: `?${queryString}`, + }; + } +}; + +export const getUrlType = (pageName: string): UrlStateType => { + if (pageName === SiemPageName.overview) { + return 'overview'; + } else if (pageName === SiemPageName.hosts) { + return 'host'; + } else if (pageName === SiemPageName.network) { + return 'network'; + } else if (pageName === SiemPageName.detections) { + return 'detections'; + } else if (pageName === SiemPageName.timelines) { + return 'timeline'; + } else if (pageName === SiemPageName.case) { + return 'case'; + } + return 'overview'; +}; + +export const getTitle = ( + pageName: string, + detailName: string | undefined, + navTabs: Record +): string => { + if (detailName != null) return detailName; + return navTabs[pageName] != null ? navTabs[pageName].name : ''; +}; + +export const makeMapStateToProps = () => { + const getInputsSelector = inputsSelectors.inputsSelector(); + const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); + const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); + const getGlobalSavedQuerySelector = inputsSelectors.globalSavedQuerySelector(); + const getTimelines = timelineSelectors.getTimelines(); + const mapStateToProps = (state: State) => { + const inputState = getInputsSelector(state); + const { linkTo: globalLinkTo, timerange: globalTimerange } = inputState.global; + const { linkTo: timelineLinkTo, timerange: timelineTimerange } = inputState.timeline; + + const timeline = Object.entries(getTimelines(state)).reduce( + (obj, [timelineId, timelineObj]) => ({ + id: timelineObj.savedObjectId != null ? timelineObj.savedObjectId : '', + isOpen: timelineObj.show, + }), + { id: '', isOpen: false } + ); + + let searchAttr: { + [CONSTANTS.appQuery]?: Query; + [CONSTANTS.filters]?: Filter[]; + [CONSTANTS.savedQuery]?: string; + } = { + [CONSTANTS.appQuery]: getGlobalQuerySelector(state), + [CONSTANTS.filters]: getGlobalFiltersQuerySelector(state), + }; + const savedQuery = getGlobalSavedQuerySelector(state); + if (savedQuery != null && savedQuery.id !== '') { + searchAttr = { + [CONSTANTS.savedQuery]: savedQuery.id, + }; + } + + return { + urlState: { + ...searchAttr, + [CONSTANTS.timerange]: { + global: { + [CONSTANTS.timerange]: globalTimerange, + linkTo: globalLinkTo, + }, + timeline: { + [CONSTANTS.timerange]: timelineTimerange, + linkTo: timelineLinkTo, + }, + }, + [CONSTANTS.timeline]: timeline, + }, + }; + }; + + return mapStateToProps; +}; + +export const updateTimerangeUrl = ( + timeRange: UrlInputsModel, + isInitializing: boolean +): UrlInputsModel => { + if (timeRange.global.timerange.kind === 'relative') { + timeRange.global.timerange.from = formatDate(timeRange.global.timerange.fromStr); + timeRange.global.timerange.to = formatDate(timeRange.global.timerange.toStr, { roundUp: true }); + } + if (timeRange.timeline.timerange.kind === 'relative' && isInitializing) { + timeRange.timeline.timerange.from = formatDate(timeRange.timeline.timerange.fromStr); + timeRange.timeline.timerange.to = formatDate(timeRange.timeline.timerange.toStr, { + roundUp: true, + }); + } + return timeRange; +}; + +export const updateUrlStateString = ({ + isInitializing, + history, + newUrlStateString, + pathName, + search, + updateTimerange, + urlKey, +}: UpdateUrlStateString): string => { + if (urlKey === CONSTANTS.appQuery) { + const queryState = decodeRisonUrlState(newUrlStateString); + if (queryState != null && queryState.query === '') { + return replaceStateInLocation({ + history, + pathName, + search, + urlStateToReplace: '', + urlStateKey: urlKey, + }); + } + } else if (urlKey === CONSTANTS.timerange && updateTimerange) { + const queryState = decodeRisonUrlState(newUrlStateString); + if (queryState != null && queryState.global != null) { + return replaceStateInLocation({ + history, + pathName, + search, + urlStateToReplace: updateTimerangeUrl(queryState, isInitializing), + urlStateKey: urlKey, + }); + } + } else if (urlKey === CONSTANTS.filters) { + const queryState = decodeRisonUrlState(newUrlStateString); + if (isEmpty(queryState)) { + return replaceStateInLocation({ + history, + pathName, + search, + urlStateToReplace: '', + urlStateKey: urlKey, + }); + } + } else if (urlKey === CONSTANTS.timeline) { + const queryState = decodeRisonUrlState(newUrlStateString); + if (queryState != null && queryState.id === '') { + return replaceStateInLocation({ + history, + pathName, + search, + urlStateToReplace: '', + urlStateKey: urlKey, + }); + } + } + return search; +}; + +export const replaceStateInLocation = ({ + history, + urlStateToReplace, + urlStateKey, + pathName, + search, +}: ReplaceStateInLocation) => { + const newLocation = replaceQueryStringInLocation( + { + hash: '', + pathname: pathName, + search, + state: '', + }, + replaceStateKeyInQueryString(urlStateKey, urlStateToReplace)(getQueryStringFromLocation(search)) + ); + if (history) { + history.replace(newLocation); + } + return newLocation.search; +}; diff --git a/x-pack/plugins/siem/public/common/components/url_state/index.test.tsx b/x-pack/plugins/siem/public/common/components/url_state/index.test.tsx new file mode 100644 index 00000000000000..b901bc2b820cce --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/url_state/index.test.tsx @@ -0,0 +1,221 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; + +import { HookWrapper } from '../../mock'; +import { SiemPageName } from '../../../app/types'; +import { RouteSpyState } from '../../utils/route/types'; +import { CONSTANTS } from './constants'; +import { + getMockPropsObj, + mockHistory, + mockSetFilterQuery, + mockSetAbsoluteRangeDatePicker, + mockSetRelativeRangeDatePicker, + testCases, +} from './test_dependencies'; +import { UrlStateContainerPropTypes } from './types'; +import { useUrlStateHooks } from './use_url_state'; +import { wait } from '../../lib/helpers'; + +let mockProps: UrlStateContainerPropTypes; + +const mockRouteSpy: RouteSpyState = { + pageName: SiemPageName.network, + detailName: undefined, + tabName: undefined, + search: '', + pathName: '/network', +}; +jest.mock('../../utils/route/use_route_spy', () => ({ + useRouteSpy: () => [mockRouteSpy], +})); + +jest.mock('../super_date_picker', () => ({ + formatDate: (date: string) => { + return 11223344556677; + }, +})); + +jest.mock('../../lib/kibana', () => ({ + useKibana: () => ({ + services: { + data: { + query: { + filterManager: {}, + savedQueries: {}, + }, + }, + }, + }), +})); + +describe('UrlStateContainer', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + describe('handleInitialize', () => { + describe('URL state updates redux', () => { + describe('relative timerange actions are called with correct data on component mount', () => { + test.each(testCases)( + '%o', + (page, namespaceLower, namespaceUpper, examplePath, type, pageName, detailName) => { + mockProps = getMockPropsObj({ + page, + examplePath, + namespaceLower, + pageName, + detailName, + }).relativeTimeSearch.undefinedQuery; + mount( useUrlStateHooks(args)} />); + + expect(mockSetRelativeRangeDatePicker.mock.calls[1][0]).toEqual({ + from: 11223344556677, + fromStr: 'now-1d/d', + kind: 'relative', + to: 11223344556677, + toStr: 'now-1d/d', + id: 'global', + }); + + expect(mockSetRelativeRangeDatePicker.mock.calls[0][0]).toEqual({ + from: 11223344556677, + fromStr: 'now-15m', + kind: 'relative', + to: 11223344556677, + toStr: 'now', + id: 'timeline', + }); + } + ); + }); + + describe('absolute timerange actions are called with correct data on component mount', () => { + test.each(testCases)( + '%o', + (page, namespaceLower, namespaceUpper, examplePath, type, pageName, detailName) => { + mockProps = getMockPropsObj({ page, examplePath, namespaceLower, pageName, detailName }) + .absoluteTimeSearch.undefinedQuery; + mount( useUrlStateHooks(args)} />); + + expect(mockSetAbsoluteRangeDatePicker.mock.calls[1][0]).toEqual({ + from: 1556736012685, + kind: 'absolute', + to: 1556822416082, + id: 'global', + }); + + expect(mockSetAbsoluteRangeDatePicker.mock.calls[0][0]).toEqual({ + from: 1556736012685, + kind: 'absolute', + to: 1556822416082, + id: 'timeline', + }); + } + ); + }); + + describe('appQuery action is called with correct data on component mount', () => { + test.each(testCases.slice(0, 4))( + ' %o', + (page, namespaceLower, namespaceUpper, examplePath, type, pageName, detailName) => { + mockProps = getMockPropsObj({ page, examplePath, namespaceLower, pageName, detailName }) + .relativeTimeSearch.undefinedQuery; + mount( useUrlStateHooks(args)} />); + + expect(mockSetFilterQuery.mock.calls[0][0]).toEqual({ + id: 'global', + language: 'kuery', + query: 'host.name:"siem-es"', + }); + } + ); + }); + }); + + describe('Redux updates URL state', () => { + describe('appQuery url state is set from redux data on component mount', () => { + test.each(testCases)( + '%o', + (page, namespaceLower, namespaceUpper, examplePath, type, pageName, detailName) => { + mockProps = getMockPropsObj({ + page, + examplePath, + namespaceLower, + pageName, + detailName, + }).noSearch.definedQuery; + mount( useUrlStateHooks(args)} />); + + expect( + mockHistory.replace.mock.calls[mockHistory.replace.mock.calls.length - 1][0] + ).toEqual({ + hash: '', + pathname: examplePath, + search: `?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))`, + state: '', + }); + } + ); + }); + }); + }); + + describe('After Initialization, keep Relative Date up to date for global only on detections page', () => { + test.each(testCases)( + '%o', + async (page, namespaceLower, namespaceUpper, examplePath, type, pageName, detailName) => { + mockProps = getMockPropsObj({ + page, + examplePath, + namespaceLower, + pageName, + detailName, + }).relativeTimeSearch.undefinedQuery; + const wrapper = mount( + useUrlStateHooks(args)} /> + ); + + wrapper.setProps({ + hookProps: getMockPropsObj({ + page: CONSTANTS.hostsPage, + examplePath: '/hosts', + namespaceLower: 'hosts', + pageName: SiemPageName.hosts, + detailName: undefined, + }).relativeTimeSearch.undefinedQuery, + }); + wrapper.update(); + await wait(); + + if (CONSTANTS.detectionsPage === page) { + expect(mockSetRelativeRangeDatePicker.mock.calls[3][0]).toEqual({ + from: 11223344556677, + fromStr: 'now-1d/d', + kind: 'relative', + to: 11223344556677, + toStr: 'now-1d/d', + id: 'global', + }); + + expect(mockSetRelativeRangeDatePicker.mock.calls[2][0]).toEqual({ + from: 1558732849370, + fromStr: 'now-15m', + kind: 'relative', + to: 1558733749370, + toStr: 'now', + id: 'timeline', + }); + } else { + // There is no change in url state, so that's expected we only have two actions + expect(mockSetRelativeRangeDatePicker.mock.calls.length).toEqual(2); + } + } + ); + }); +}); diff --git a/x-pack/plugins/siem/public/common/components/url_state/index.tsx b/x-pack/plugins/siem/public/common/components/url_state/index.tsx new file mode 100644 index 00000000000000..f90e9cf62801b7 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/url_state/index.tsx @@ -0,0 +1,55 @@ +/* + * 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 { compose, Dispatch } from 'redux'; +import { connect } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; + +import { timelineActions } from '../../../timelines/store/timeline'; +import { RouteSpyState } from '../../utils/route/types'; +import { useRouteSpy } from '../../utils/route/use_route_spy'; + +import { UrlStateContainerPropTypes, UrlStateProps } from './types'; +import { useUrlStateHooks } from './use_url_state'; +import { dispatchUpdateTimeline } from '../../../timelines/components/open_timeline/helpers'; +import { dispatchSetInitialStateFromUrl } from './initialize_redux_by_url'; +import { makeMapStateToProps } from './helpers'; + +export const UrlStateContainer: React.FC = ( + props: UrlStateContainerPropTypes +) => { + useUrlStateHooks(props); + return null; +}; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + setInitialStateFromUrl: dispatchSetInitialStateFromUrl(dispatch), + updateTimeline: dispatchUpdateTimeline(dispatch), + updateTimelineIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => + dispatch(timelineActions.updateIsLoading({ id, isLoading })), +}); + +export const UrlStateRedux = compose>( + connect(makeMapStateToProps, mapDispatchToProps) +)( + React.memo( + UrlStateContainer, + (prevProps, nextProps) => + prevProps.pathName === nextProps.pathName && deepEqual(prevProps.urlState, nextProps.urlState) + ) +); + +const UseUrlStateComponent: React.FC = props => { + const [routeProps] = useRouteSpy(); + const urlStateReduxProps: RouteSpyState & UrlStateProps = { + ...routeProps, + ...props, + }; + return ; +}; + +export const UseUrlState = React.memo(UseUrlStateComponent); diff --git a/x-pack/plugins/siem/public/components/url_state/index_mocked.test.tsx b/x-pack/plugins/siem/public/common/components/url_state/index_mocked.test.tsx similarity index 99% rename from x-pack/plugins/siem/public/components/url_state/index_mocked.test.tsx rename to x-pack/plugins/siem/public/common/components/url_state/index_mocked.test.tsx index 4adc17b32e1891..122f7f6fed57ed 100644 --- a/x-pack/plugins/siem/public/components/url_state/index_mocked.test.tsx +++ b/x-pack/plugins/siem/public/common/components/url_state/index_mocked.test.tsx @@ -8,7 +8,7 @@ import { mount } from 'enzyme'; import React from 'react'; import { HookWrapper } from '../../mock/hook_wrapper'; -import { SiemPageName } from '../../pages/home/types'; +import { SiemPageName } from '../../../app/types'; import { CONSTANTS } from './constants'; import { getFilterQuery, getMockPropsObj, mockHistory, testCases } from './test_dependencies'; diff --git a/x-pack/plugins/siem/public/components/url_state/initialize_redux_by_url.tsx b/x-pack/plugins/siem/public/common/components/url_state/initialize_redux_by_url.tsx similarity index 95% rename from x-pack/plugins/siem/public/components/url_state/initialize_redux_by_url.tsx rename to x-pack/plugins/siem/public/common/components/url_state/initialize_redux_by_url.tsx index 54a196d1b81617..441424faa48dcb 100644 --- a/x-pack/plugins/siem/public/components/url_state/initialize_redux_by_url.tsx +++ b/x-pack/plugins/siem/public/common/components/url_state/initialize_redux_by_url.tsx @@ -7,7 +7,7 @@ import { get, isEmpty } from 'lodash/fp'; import { Dispatch } from 'redux'; -import { Query, Filter } from '../../../../../../src/plugins/data/public'; +import { Query, Filter } from '../../../../../../../src/plugins/data/public'; import { inputsActions } from '../../store/actions'; import { InputsModelId, TimeRangeKinds } from '../../store/inputs/constants'; import { @@ -16,12 +16,12 @@ import { AbsoluteTimeRange, RelativeTimeRange, } from '../../store/inputs/model'; -import { TimelineUrl } from '../../store/timeline/model'; +import { TimelineUrl } from '../../../timelines/store/timeline/model'; import { CONSTANTS } from './constants'; import { decodeRisonUrlState } from './helpers'; import { normalizeTimeRange } from './normalize_time_range'; import { DispatchSetInitialStateFromUrl, SetInitialStateFromUrl } from './types'; -import { queryTimelineById } from '../open_timeline/helpers'; +import { queryTimelineById } from '../../../timelines/components/open_timeline/helpers'; export const dispatchSetInitialStateFromUrl = ( dispatch: Dispatch diff --git a/x-pack/plugins/siem/public/components/url_state/normalize_time_range.test.ts b/x-pack/plugins/siem/public/common/components/url_state/normalize_time_range.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/url_state/normalize_time_range.test.ts rename to x-pack/plugins/siem/public/common/components/url_state/normalize_time_range.test.ts diff --git a/x-pack/plugins/siem/public/components/url_state/normalize_time_range.ts b/x-pack/plugins/siem/public/common/components/url_state/normalize_time_range.ts similarity index 100% rename from x-pack/plugins/siem/public/components/url_state/normalize_time_range.ts rename to x-pack/plugins/siem/public/common/components/url_state/normalize_time_range.ts diff --git a/x-pack/plugins/siem/public/components/url_state/test_dependencies.ts b/x-pack/plugins/siem/public/common/components/url_state/test_dependencies.ts similarity index 95% rename from x-pack/plugins/siem/public/components/url_state/test_dependencies.ts rename to x-pack/plugins/siem/public/common/components/url_state/test_dependencies.ts index 974bee53bc2ba0..de6a00bfadb80c 100644 --- a/x-pack/plugins/siem/public/components/url_state/test_dependencies.ts +++ b/x-pack/plugins/siem/public/common/components/url_state/test_dependencies.ts @@ -5,17 +5,18 @@ */ import { ActionCreator } from 'typescript-fsa'; -import { DispatchUpdateTimeline } from '../open_timeline/types'; -import { navTabs } from '../../pages/home/home_navigations'; -import { SiemPageName } from '../../pages/home/types'; -import { hostsModel, networkModel } from '../../store'; +import { DispatchUpdateTimeline } from '../../../timelines/components/open_timeline/types'; +import { navTabs } from '../../../app/home/home_navigations'; +import { SiemPageName } from '../../../app/types'; import { inputsActions } from '../../store/actions'; -import { HostsTableType } from '../../store/hosts/model'; import { CONSTANTS } from './constants'; import { dispatchSetInitialStateFromUrl } from './initialize_redux_by_url'; import { UrlStateContainerPropTypes, LocationTypes } from './types'; -import { Query } from '../../../../../../src/plugins/data/public'; +import { Query } from '../../../../../../../src/plugins/data/public'; +import { networkModel } from '../../../network/store'; +import { hostsModel } from '../../../hosts/store'; +import { HostsTableType } from '../../../hosts/store/model'; type Action = 'PUSH' | 'POP' | 'REPLACE'; const pop: Action = 'POP'; diff --git a/x-pack/plugins/siem/public/common/components/url_state/types.ts b/x-pack/plugins/siem/public/common/components/url_state/types.ts new file mode 100644 index 00000000000000..56578d84e12e47 --- /dev/null +++ b/x-pack/plugins/siem/public/common/components/url_state/types.ts @@ -0,0 +1,177 @@ +/* + * 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 ApolloClient from 'apollo-client'; +import * as H from 'history'; +import { ActionCreator } from 'typescript-fsa'; +import { + IIndexPattern, + Query, + Filter, + FilterManager, + SavedQueryService, +} from 'src/plugins/data/public'; + +import { UrlInputsModel } from '../../store/inputs/model'; +import { TimelineUrl } from '../../../timelines/store/timeline/model'; +import { RouteSpyState } from '../../utils/route/types'; +import { DispatchUpdateTimeline } from '../../../timelines/components/open_timeline/types'; +import { NavTab } from '../navigation/types'; + +import { CONSTANTS, UrlStateType } from './constants'; + +export const ALL_URL_STATE_KEYS: KeyUrlState[] = [ + CONSTANTS.appQuery, + CONSTANTS.filters, + CONSTANTS.savedQuery, + CONSTANTS.timerange, + CONSTANTS.timeline, +]; + +export const URL_STATE_KEYS: Record = { + detections: [ + CONSTANTS.appQuery, + CONSTANTS.filters, + CONSTANTS.savedQuery, + CONSTANTS.timerange, + CONSTANTS.timeline, + ], + host: [ + CONSTANTS.appQuery, + CONSTANTS.filters, + CONSTANTS.savedQuery, + CONSTANTS.timerange, + CONSTANTS.timeline, + ], + network: [ + CONSTANTS.appQuery, + CONSTANTS.filters, + CONSTANTS.savedQuery, + CONSTANTS.timerange, + CONSTANTS.timeline, + ], + overview: [ + CONSTANTS.appQuery, + CONSTANTS.filters, + CONSTANTS.savedQuery, + CONSTANTS.timerange, + CONSTANTS.timeline, + ], + timeline: [ + CONSTANTS.appQuery, + CONSTANTS.filters, + CONSTANTS.savedQuery, + CONSTANTS.timerange, + CONSTANTS.timeline, + ], + case: [ + CONSTANTS.appQuery, + CONSTANTS.filters, + CONSTANTS.savedQuery, + CONSTANTS.timerange, + CONSTANTS.timeline, + ], +}; + +export type LocationTypes = + | CONSTANTS.caseDetails + | CONSTANTS.casePage + | CONSTANTS.detectionsPage + | CONSTANTS.hostsDetails + | CONSTANTS.hostsPage + | CONSTANTS.networkDetails + | CONSTANTS.networkPage + | CONSTANTS.overviewPage + | CONSTANTS.timelinePage + | CONSTANTS.unknown; + +export interface UrlState { + [CONSTANTS.appQuery]?: Query; + [CONSTANTS.filters]?: Filter[]; + [CONSTANTS.savedQuery]?: string; + [CONSTANTS.timerange]: UrlInputsModel; + [CONSTANTS.timeline]: TimelineUrl; +} +export type KeyUrlState = keyof UrlState; + +export interface UrlStateProps { + navTabs: Record; + indexPattern?: IIndexPattern; + mapToUrlState?: (value: string) => UrlState; + onChange?: (urlState: UrlState, previousUrlState: UrlState) => void; + onInitialize?: (urlState: UrlState) => void; +} + +export interface UrlStateStateToPropsType { + urlState: UrlState; +} + +export interface UpdateTimelineIsLoading { + id: string; + isLoading: boolean; +} + +export interface UrlStateDispatchToPropsType { + setInitialStateFromUrl: DispatchSetInitialStateFromUrl; + updateTimeline: DispatchUpdateTimeline; + updateTimelineIsLoading: ActionCreator; +} + +export type UrlStateContainerPropTypes = RouteSpyState & + UrlStateStateToPropsType & + UrlStateDispatchToPropsType & + UrlStateProps; + +export interface PreviousLocationUrlState { + pathName: string | undefined; + pageName: string | undefined; + urlState: UrlState; +} + +export interface UrlStateToRedux { + urlKey: KeyUrlState; + newUrlStateString: string; +} + +export interface SetInitialStateFromUrl { + apolloClient: ApolloClient | ApolloClient<{}> | undefined; + detailName: string | undefined; + filterManager: FilterManager; + indexPattern: IIndexPattern | undefined; + pageName: string; + savedQueries: SavedQueryService; + updateTimeline: DispatchUpdateTimeline; + updateTimelineIsLoading: ActionCreator; + urlStateToUpdate: UrlStateToRedux[]; +} + +export type DispatchSetInitialStateFromUrl = ({ + apolloClient, + detailName, + indexPattern, + pageName, + updateTimeline, + updateTimelineIsLoading, + urlStateToUpdate, +}: SetInitialStateFromUrl) => () => void; + +export interface ReplaceStateInLocation { + history?: H.History; + urlStateToReplace: T; + urlStateKey: string; + pathName: string; + search: string; +} + +export interface UpdateUrlStateString { + isInitializing: boolean; + history?: H.History; + newUrlStateString: string; + pathName: string; + search: string; + updateTimerange: boolean; + urlKey: KeyUrlState; +} diff --git a/x-pack/plugins/siem/public/components/url_state/use_url_state.tsx b/x-pack/plugins/siem/public/common/components/url_state/use_url_state.tsx similarity index 99% rename from x-pack/plugins/siem/public/components/url_state/use_url_state.tsx rename to x-pack/plugins/siem/public/common/components/url_state/use_url_state.tsx index a7704e0e86970a..b3436a7da82970 100644 --- a/x-pack/plugins/siem/public/components/url_state/use_url_state.tsx +++ b/x-pack/plugins/siem/public/common/components/url_state/use_url_state.tsx @@ -27,7 +27,7 @@ import { ALL_URL_STATE_KEYS, UrlStateToRedux, } from './types'; -import { SiemPageName } from '../../pages/home/types'; +import { SiemPageName } from '../../../app/types'; function usePrevious(value: PreviousLocationUrlState) { const ref = useRef(value); diff --git a/x-pack/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar.test.tsx.snap b/x-pack/plugins/siem/public/common/components/utility_bar/__snapshots__/utility_bar.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/utility_bar/__snapshots__/utility_bar.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap b/x-pack/plugins/siem/public/common/components/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/utility_bar/__snapshots__/utility_bar_action.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap b/x-pack/plugins/siem/public/common/components/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/utility_bar/__snapshots__/utility_bar_group.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap b/x-pack/plugins/siem/public/common/components/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/utility_bar/__snapshots__/utility_bar_section.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap b/x-pack/plugins/siem/public/common/components/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/utility_bar/__snapshots__/utility_bar_text.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/utility_bar/index.ts b/x-pack/plugins/siem/public/common/components/utility_bar/index.ts similarity index 100% rename from x-pack/plugins/siem/public/components/utility_bar/index.ts rename to x-pack/plugins/siem/public/common/components/utility_bar/index.ts diff --git a/x-pack/plugins/siem/public/components/utility_bar/styles.tsx b/x-pack/plugins/siem/public/common/components/utility_bar/styles.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/utility_bar/styles.tsx rename to x-pack/plugins/siem/public/common/components/utility_bar/styles.tsx diff --git a/x-pack/plugins/siem/public/components/utility_bar/utility_bar.test.tsx b/x-pack/plugins/siem/public/common/components/utility_bar/utility_bar.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/utility_bar/utility_bar.test.tsx rename to x-pack/plugins/siem/public/common/components/utility_bar/utility_bar.test.tsx diff --git a/x-pack/plugins/siem/public/components/utility_bar/utility_bar.tsx b/x-pack/plugins/siem/public/common/components/utility_bar/utility_bar.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/utility_bar/utility_bar.tsx rename to x-pack/plugins/siem/public/common/components/utility_bar/utility_bar.tsx diff --git a/x-pack/plugins/siem/public/components/utility_bar/utility_bar_action.test.tsx b/x-pack/plugins/siem/public/common/components/utility_bar/utility_bar_action.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/utility_bar/utility_bar_action.test.tsx rename to x-pack/plugins/siem/public/common/components/utility_bar/utility_bar_action.test.tsx diff --git a/x-pack/plugins/siem/public/components/utility_bar/utility_bar_action.tsx b/x-pack/plugins/siem/public/common/components/utility_bar/utility_bar_action.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/utility_bar/utility_bar_action.tsx rename to x-pack/plugins/siem/public/common/components/utility_bar/utility_bar_action.tsx diff --git a/x-pack/plugins/siem/public/components/utility_bar/utility_bar_group.test.tsx b/x-pack/plugins/siem/public/common/components/utility_bar/utility_bar_group.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/utility_bar/utility_bar_group.test.tsx rename to x-pack/plugins/siem/public/common/components/utility_bar/utility_bar_group.test.tsx diff --git a/x-pack/plugins/siem/public/components/utility_bar/utility_bar_group.tsx b/x-pack/plugins/siem/public/common/components/utility_bar/utility_bar_group.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/utility_bar/utility_bar_group.tsx rename to x-pack/plugins/siem/public/common/components/utility_bar/utility_bar_group.tsx diff --git a/x-pack/plugins/siem/public/components/utility_bar/utility_bar_section.test.tsx b/x-pack/plugins/siem/public/common/components/utility_bar/utility_bar_section.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/utility_bar/utility_bar_section.test.tsx rename to x-pack/plugins/siem/public/common/components/utility_bar/utility_bar_section.test.tsx diff --git a/x-pack/plugins/siem/public/components/utility_bar/utility_bar_section.tsx b/x-pack/plugins/siem/public/common/components/utility_bar/utility_bar_section.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/utility_bar/utility_bar_section.tsx rename to x-pack/plugins/siem/public/common/components/utility_bar/utility_bar_section.tsx diff --git a/x-pack/plugins/siem/public/components/utility_bar/utility_bar_text.test.tsx b/x-pack/plugins/siem/public/common/components/utility_bar/utility_bar_text.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/utility_bar/utility_bar_text.test.tsx rename to x-pack/plugins/siem/public/common/components/utility_bar/utility_bar_text.test.tsx diff --git a/x-pack/plugins/siem/public/components/utility_bar/utility_bar_text.tsx b/x-pack/plugins/siem/public/common/components/utility_bar/utility_bar_text.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/utility_bar/utility_bar_text.tsx rename to x-pack/plugins/siem/public/common/components/utility_bar/utility_bar_text.tsx diff --git a/x-pack/plugins/siem/public/components/utils.ts b/x-pack/plugins/siem/public/common/components/utils.ts similarity index 100% rename from x-pack/plugins/siem/public/components/utils.ts rename to x-pack/plugins/siem/public/common/components/utils.ts diff --git a/x-pack/plugins/siem/public/components/with_hover_actions/index.tsx b/x-pack/plugins/siem/public/common/components/with_hover_actions/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/with_hover_actions/index.tsx rename to x-pack/plugins/siem/public/common/components/with_hover_actions/index.tsx diff --git a/x-pack/plugins/siem/public/components/wrapper_page/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/common/components/wrapper_page/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/wrapper_page/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/common/components/wrapper_page/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/wrapper_page/index.test.tsx b/x-pack/plugins/siem/public/common/components/wrapper_page/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/wrapper_page/index.test.tsx rename to x-pack/plugins/siem/public/common/components/wrapper_page/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/wrapper_page/index.tsx b/x-pack/plugins/siem/public/common/components/wrapper_page/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/wrapper_page/index.tsx rename to x-pack/plugins/siem/public/common/components/wrapper_page/index.tsx diff --git a/x-pack/plugins/siem/public/common/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts b/x-pack/plugins/siem/public/common/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts new file mode 100644 index 00000000000000..c3d470df11be77 --- /dev/null +++ b/x-pack/plugins/siem/public/common/containers/anomalies/anomalies_query_tab_body/histogram_configs.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 * as i18n from './translations'; +import { + MatrixHistogramOption, + MatrixHisrogramConfigs, +} from '../../../components/matrix_histogram/types'; +import { HistogramType } from '../../../../graphql/types'; + +export const anomaliesStackByOptions: MatrixHistogramOption[] = [ + { + text: i18n.ANOMALIES_STACK_BY_JOB_ID, + value: 'job_id', + }, +]; + +const DEFAULT_STACK_BY = i18n.ANOMALIES_STACK_BY_JOB_ID; + +export const histogramConfigs: MatrixHisrogramConfigs = { + defaultStackByOption: + anomaliesStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? anomaliesStackByOptions[0], + errorMessage: i18n.ERROR_FETCHING_ANOMALIES_DATA, + hideHistogramIfEmpty: true, + histogramType: HistogramType.anomalies, + stackByOptions: anomaliesStackByOptions, + subtitle: undefined, + title: i18n.ANOMALIES_TITLE, +}; diff --git a/x-pack/plugins/siem/public/common/containers/anomalies/anomalies_query_tab_body/index.tsx b/x-pack/plugins/siem/public/common/containers/anomalies/anomalies_query_tab_body/index.tsx new file mode 100644 index 00000000000000..a5574bd2a57c76 --- /dev/null +++ b/x-pack/plugins/siem/public/common/containers/anomalies/anomalies_query_tab_body/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 React, { useEffect } from 'react'; + +import { DEFAULT_ANOMALY_SCORE } from '../../../../../common/constants'; +import { AnomaliesQueryTabBodyProps } from './types'; +import { getAnomaliesFilterQuery } from './utils'; +import { useSiemJobs } from '../../../components/ml_popover/hooks/use_siem_jobs'; +import { useUiSetting$ } from '../../../lib/kibana'; +import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; +import { histogramConfigs } from './histogram_configs'; +const ID = 'anomaliesOverTimeQuery'; + +export const AnomaliesQueryTabBody = ({ + deleteQuery, + endDate, + setQuery, + skip, + startDate, + type, + narrowDateRange, + filterQuery, + anomaliesFilterQuery, + AnomaliesTableComponent, + flowTarget, + ip, +}: AnomaliesQueryTabBodyProps) => { + useEffect(() => { + return () => { + if (deleteQuery) { + deleteQuery({ id: ID }); + } + }; + }, []); + + const [, siemJobs] = useSiemJobs(true); + const [anomalyScore] = useUiSetting$(DEFAULT_ANOMALY_SCORE); + + const mergedFilterQuery = getAnomaliesFilterQuery( + filterQuery, + anomaliesFilterQuery, + siemJobs, + anomalyScore, + flowTarget, + ip + ); + + return ( + <> + + + + ); +}; + +AnomaliesQueryTabBody.displayName = 'AnomaliesQueryTabBody'; diff --git a/x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/translations.ts b/x-pack/plugins/siem/public/common/containers/anomalies/anomalies_query_tab_body/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/translations.ts rename to x-pack/plugins/siem/public/common/containers/anomalies/anomalies_query_tab_body/translations.ts diff --git a/x-pack/plugins/siem/public/common/containers/anomalies/anomalies_query_tab_body/types.ts b/x-pack/plugins/siem/public/common/containers/anomalies/anomalies_query_tab_body/types.ts new file mode 100644 index 00000000000000..ecf4c3590a42c2 --- /dev/null +++ b/x-pack/plugins/siem/public/common/containers/anomalies/anomalies_query_tab_body/types.ts @@ -0,0 +1,35 @@ +/* + * 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 { ESTermQuery } from '../../../../../common/typed_json'; +import { NarrowDateRange } from '../../../components/ml/types'; +import { UpdateDateRange } from '../../../components/charts/common'; +import { SetQuery } from '../../../../hosts/pages/navigation/types'; +import { FlowTarget } from '../../../../graphql/types'; +import { HostsType } from '../../../../hosts/store/model'; +import { NetworkType } from '../../../../network/store//model'; +import { AnomaliesHostTable } from '../../../components/ml/tables/anomalies_host_table'; +import { AnomaliesNetworkTable } from '../../../components/ml/tables/anomalies_network_table'; + +interface QueryTabBodyProps { + type: HostsType | NetworkType; + filterQuery?: string | ESTermQuery; +} + +export type AnomaliesQueryTabBodyProps = QueryTabBodyProps & { + anomaliesFilterQuery?: object; + AnomaliesTableComponent: typeof AnomaliesHostTable | typeof AnomaliesNetworkTable; + deleteQuery?: ({ id }: { id: string }) => void; + endDate: number; + flowTarget?: FlowTarget; + narrowDateRange: NarrowDateRange; + setQuery: SetQuery; + startDate: number; + skip: boolean; + updateDateRange?: UpdateDateRange; + hideHistogramIfEmpty?: boolean; + ip?: string; +}; diff --git a/x-pack/plugins/siem/public/common/containers/anomalies/anomalies_query_tab_body/utils.ts b/x-pack/plugins/siem/public/common/containers/anomalies/anomalies_query_tab_body/utils.ts new file mode 100644 index 00000000000000..e815db68ebcdd0 --- /dev/null +++ b/x-pack/plugins/siem/public/common/containers/anomalies/anomalies_query_tab_body/utils.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import deepmerge from 'deepmerge'; + +import { ESTermQuery } from '../../../../../common/typed_json'; +import { createFilter } from '../../helpers'; +import { SiemJob } from '../../../components/ml_popover/types'; +import { FlowTarget } from '../../../../graphql/types'; + +export const getAnomaliesFilterQuery = ( + filterQuery: string | ESTermQuery | undefined, + anomaliesFilterQuery: object = {}, + siemJobs: SiemJob[] = [], + anomalyScore: number, + flowTarget?: FlowTarget, + ip?: string +): string => { + const siemJobIds = siemJobs + .filter(job => job.isInstalled) + .map(job => job.id) + .map(jobId => ({ + match_phrase: { + job_id: jobId, + }, + })); + + const filterQueryString = createFilter(filterQuery); + const filterQueryObject = filterQueryString ? JSON.parse(filterQueryString) : {}; + const mergedFilterQuery = deepmerge.all([ + filterQueryObject, + anomaliesFilterQuery, + { + bool: { + filter: [ + { + bool: { + should: siemJobIds, + minimum_should_match: 1, + }, + }, + { + match_phrase: { + result_type: 'record', + }, + }, + flowTarget && + ip && { + match_phrase: { + [`${flowTarget}.ip`]: ip, + }, + }, + { + range: { + record_score: { + gte: anomalyScore, + }, + }, + }, + ], + }, + }, + ]); + + return JSON.stringify(mergedFilterQuery); +}; diff --git a/x-pack/plugins/siem/public/containers/errors/index.test.tsx b/x-pack/plugins/siem/public/common/containers/errors/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/containers/errors/index.test.tsx rename to x-pack/plugins/siem/public/common/containers/errors/index.test.tsx diff --git a/x-pack/plugins/siem/public/containers/errors/index.tsx b/x-pack/plugins/siem/public/common/containers/errors/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/containers/errors/index.tsx rename to x-pack/plugins/siem/public/common/containers/errors/index.tsx diff --git a/x-pack/plugins/siem/public/containers/errors/translations.ts b/x-pack/plugins/siem/public/common/containers/errors/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/errors/translations.ts rename to x-pack/plugins/siem/public/common/containers/errors/translations.ts diff --git a/x-pack/plugins/siem/public/common/containers/events/last_event_time/index.ts b/x-pack/plugins/siem/public/common/containers/events/last_event_time/index.ts new file mode 100644 index 00000000000000..17b2cb746e92b3 --- /dev/null +++ b/x-pack/plugins/siem/public/common/containers/events/last_event_time/index.ts @@ -0,0 +1,90 @@ +/* + * 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 { get } from 'lodash/fp'; +import React, { useEffect, useState } from 'react'; + +import { DEFAULT_INDEX_KEY } from '../../../../../common/constants'; +import { + GetLastEventTimeQuery, + LastEventIndexKey, + LastTimeDetails, +} from '../../../../graphql/types'; +import { inputsModel } from '../../../store'; +import { QueryTemplateProps } from '../../query_template'; +import { useUiSetting$ } from '../../../lib/kibana'; + +import { LastEventTimeGqlQuery } from './last_event_time.gql_query'; +import { useApolloClient } from '../../../utils/apollo_context'; + +export interface LastEventTimeArgs { + id: string; + errorMessage: string; + lastSeen: Date; + loading: boolean; + refetch: inputsModel.Refetch; +} + +export interface OwnProps extends QueryTemplateProps { + children: (args: LastEventTimeArgs) => React.ReactNode; + indexKey: LastEventIndexKey; +} + +export function useLastEventTimeQuery( + indexKey: LastEventIndexKey, + details: LastTimeDetails, + sourceId: string +) { + const [loading, updateLoading] = useState(false); + const [lastSeen, updateLastSeen] = useState(null); + const [errorMessage, updateErrorMessage] = useState(null); + const [currentIndexKey, updateCurrentIndexKey] = useState(null); + const [defaultIndex] = useUiSetting$(DEFAULT_INDEX_KEY); + const apolloClient = useApolloClient(); + async function fetchLastEventTime(signal: AbortSignal) { + updateLoading(true); + if (apolloClient) { + apolloClient + .query({ + query: LastEventTimeGqlQuery, + fetchPolicy: 'cache-first', + variables: { + sourceId, + indexKey, + details, + defaultIndex, + }, + context: { + fetchOptions: { + signal, + }, + }, + }) + .then( + result => { + updateLoading(false); + updateLastSeen(get('data.source.LastEventTime.lastSeen', result)); + updateErrorMessage(null); + updateCurrentIndexKey(currentIndexKey); + }, + error => { + updateLoading(false); + updateLastSeen(null); + updateErrorMessage(error.message); + } + ); + } + } + + useEffect(() => { + const abortCtrl = new AbortController(); + const signal = abortCtrl.signal; + fetchLastEventTime(signal); + return () => abortCtrl.abort(); + }, [apolloClient, indexKey, details.hostName, details.ip]); + + return { lastSeen, loading, errorMessage }; +} diff --git a/x-pack/plugins/siem/public/containers/events/last_event_time/last_event_time.gql_query.ts b/x-pack/plugins/siem/public/common/containers/events/last_event_time/last_event_time.gql_query.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/events/last_event_time/last_event_time.gql_query.ts rename to x-pack/plugins/siem/public/common/containers/events/last_event_time/last_event_time.gql_query.ts diff --git a/x-pack/plugins/siem/public/common/containers/events/last_event_time/mock.ts b/x-pack/plugins/siem/public/common/containers/events/last_event_time/mock.ts new file mode 100644 index 00000000000000..938473f92782a7 --- /dev/null +++ b/x-pack/plugins/siem/public/common/containers/events/last_event_time/mock.ts @@ -0,0 +1,61 @@ +/* + * 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_INDEX_PATTERN } from '../../../../../common/constants'; +import { GetLastEventTimeQuery, LastEventIndexKey } from '../../../../graphql/types'; + +import { LastEventTimeGqlQuery } from './last_event_time.gql_query'; + +interface MockLastEventTimeQuery { + request: { + query: GetLastEventTimeQuery.Query; + variables: GetLastEventTimeQuery.Variables; + }; + result: { + data?: { + source: { + id: string; + LastEventTime: { + lastSeen: string | null; + errorMessage: string | null; + }; + }; + }; + errors?: [{ message: string }]; + }; +} + +const getTimeTwelveMinutesAgo = () => { + const d = new Date(); + const ts = d.getTime(); + const twelveMinutes = ts - 12 * 60 * 1000; + return new Date(twelveMinutes).toISOString(); +}; + +export const mockLastEventTimeQuery: MockLastEventTimeQuery[] = [ + { + request: { + query: LastEventTimeGqlQuery, + variables: { + sourceId: 'default', + indexKey: LastEventIndexKey.hosts, + details: {}, + defaultIndex: DEFAULT_INDEX_PATTERN, + }, + }, + result: { + data: { + source: { + id: 'default', + LastEventTime: { + lastSeen: getTimeTwelveMinutesAgo(), + errorMessage: null, + }, + }, + }, + }, + }, +]; diff --git a/x-pack/plugins/siem/public/containers/global_time/index.tsx b/x-pack/plugins/siem/public/common/containers/global_time/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/containers/global_time/index.tsx rename to x-pack/plugins/siem/public/common/containers/global_time/index.tsx diff --git a/x-pack/plugins/siem/public/common/containers/helpers.test.ts b/x-pack/plugins/siem/public/common/containers/helpers.test.ts new file mode 100644 index 00000000000000..360ba28a746b07 --- /dev/null +++ b/x-pack/plugins/siem/public/common/containers/helpers.test.ts @@ -0,0 +1,29 @@ +/* + * 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 { ESQuery } from '../../../common/typed_json'; + +import { createFilter } from './helpers'; + +describe('Helpers', () => { + describe('#createFilter', () => { + test('if it is a string it returns untouched', () => { + const filter = createFilter('even invalid strings return the same'); + expect(filter).toBe('even invalid strings return the same'); + }); + + test('if it is an ESQuery object it will be returned as a string', () => { + const query: ESQuery = { term: { 'host.id': 'host-value' } }; + const filter = createFilter(query); + expect(filter).toBe(JSON.stringify(query)); + }); + + test('if it is undefined, then undefined is returned', () => { + const filter = createFilter(undefined); + expect(filter).toBe(undefined); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/common/containers/helpers.ts b/x-pack/plugins/siem/public/common/containers/helpers.ts new file mode 100644 index 00000000000000..39fd1987218fa2 --- /dev/null +++ b/x-pack/plugins/siem/public/common/containers/helpers.ts @@ -0,0 +1,15 @@ +/* + * 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 { FetchPolicy } from 'apollo-client'; +import { isString } from 'lodash/fp'; + +import { ESQuery } from '../../../common/typed_json'; + +export const createFilter = (filterQuery: ESQuery | string | undefined) => + isString(filterQuery) ? filterQuery : JSON.stringify(filterQuery); + +export const getDefaultFetchPolicy = (): FetchPolicy => 'cache-and-network'; diff --git a/x-pack/plugins/siem/public/common/containers/kuery_autocompletion/index.tsx b/x-pack/plugins/siem/public/common/containers/kuery_autocompletion/index.tsx new file mode 100644 index 00000000000000..af4eb1ff7a5e1f --- /dev/null +++ b/x-pack/plugins/siem/public/common/containers/kuery_autocompletion/index.tsx @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; +import { QuerySuggestion, IIndexPattern } from '../../../../../../../src/plugins/data/public'; +import { useKibana } from '../../lib/kibana'; + +type RendererResult = React.ReactElement | null; +type RendererFunction = (args: RenderArgs) => Result; + +interface KueryAutocompletionLifecycleProps { + children: RendererFunction<{ + isLoadingSuggestions: boolean; + loadSuggestions: (expression: string, cursorPosition: number, maxSuggestions?: number) => void; + suggestions: QuerySuggestion[]; + }>; + indexPattern: IIndexPattern; +} + +interface KueryAutocompletionCurrentRequest { + expression: string; + cursorPosition: number; +} + +export const KueryAutocompletion = React.memo( + ({ children, indexPattern }) => { + const [currentRequest, setCurrentRequest] = useState( + null + ); + const [suggestions, setSuggestions] = useState([]); + const kibana = useKibana(); + const loadSuggestions = async ( + expression: string, + cursorPosition: number, + maxSuggestions?: number + ) => { + const language = 'kuery'; + + if (!kibana.services.data.autocomplete.hasQuerySuggestions(language)) { + return; + } + + const futureRequest = { + expression, + cursorPosition, + }; + setCurrentRequest({ + expression, + cursorPosition, + }); + setSuggestions([]); + + if ( + futureRequest && + futureRequest.expression !== (currentRequest && currentRequest.expression) && + futureRequest.cursorPosition !== (currentRequest && currentRequest.cursorPosition) + ) { + const newSuggestions = + (await kibana.services.data.autocomplete.getQuerySuggestions({ + language: 'kuery', + indexPatterns: [indexPattern], + boolFilter: [], + query: expression, + selectionStart: cursorPosition, + selectionEnd: cursorPosition, + })) || []; + + setCurrentRequest(null); + setSuggestions(maxSuggestions ? newSuggestions.slice(0, maxSuggestions) : newSuggestions); + } + }; + + return children({ + isLoadingSuggestions: currentRequest !== null, + loadSuggestions, + suggestions, + }); + } +); + +KueryAutocompletion.displayName = 'KueryAutocompletion'; diff --git a/x-pack/plugins/siem/public/containers/matrix_histogram/index.gql_query.ts b/x-pack/plugins/siem/public/common/containers/matrix_histogram/index.gql_query.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/matrix_histogram/index.gql_query.ts rename to x-pack/plugins/siem/public/common/containers/matrix_histogram/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/common/containers/matrix_histogram/index.test.tsx b/x-pack/plugins/siem/public/common/containers/matrix_histogram/index.test.tsx new file mode 100644 index 00000000000000..cb988d7ebf1901 --- /dev/null +++ b/x-pack/plugins/siem/public/common/containers/matrix_histogram/index.test.tsx @@ -0,0 +1,157 @@ +/* + * 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 { useQuery } from '.'; +import { mount } from 'enzyme'; +import React from 'react'; +import { useApolloClient } from '../../utils/apollo_context'; +import { errorToToaster } from '../../components/toasters'; +import { MatrixOverTimeHistogramData, HistogramType } from '../../../graphql/types'; +import { InspectQuery, Refetch } from '../../store/inputs/model'; + +const mockQuery = jest.fn().mockResolvedValue({ + data: { + source: { + MatrixHistogram: { + matrixHistogramData: [{}], + totalCount: 1, + inspect: false, + }, + }, + }, +}); + +const mockRejectQuery = jest.fn().mockRejectedValue(new Error()); +jest.mock('../../utils/apollo_context', () => ({ + useApolloClient: jest.fn(), +})); + +jest.mock('../../lib/kibana', () => { + return { + useUiSetting$: jest.fn().mockReturnValue(['mockDefaultIndex']), + }; +}); + +jest.mock('./index.gql_query', () => { + return { + MatrixHistogramGqlQuery: 'mockGqlQuery', + }; +}); + +jest.mock('../../components/toasters/', () => ({ + useStateToaster: () => [jest.fn(), jest.fn()], + errorToToaster: jest.fn(), +})); + +describe('useQuery', () => { + let result: { + data: MatrixOverTimeHistogramData[] | null; + loading: boolean; + inspect: InspectQuery | null; + totalCount: number; + refetch: Refetch | undefined; + }; + describe('happy path', () => { + beforeAll(() => { + (useApolloClient as jest.Mock).mockReturnValue({ + query: mockQuery, + }); + const TestComponent = () => { + result = useQuery({ + endDate: 100, + errorMessage: 'fakeErrorMsg', + filterQuery: '', + histogramType: HistogramType.alerts, + isInspected: false, + stackByField: 'fakeField', + startDate: 0, + }); + + return
; + }; + + mount(); + }); + + test('should set variables', () => { + expect(mockQuery).toBeCalledWith({ + query: 'mockGqlQuery', + fetchPolicy: 'network-only', + variables: { + filterQuery: '', + sourceId: 'default', + timerange: { + interval: '12h', + from: 0, + to: 100, + }, + defaultIndex: 'mockDefaultIndex', + inspect: false, + stackByField: 'fakeField', + histogramType: 'alerts', + }, + context: { + fetchOptions: { + abortSignal: new AbortController().signal, + }, + }, + }); + }); + + test('should setData', () => { + expect(result.data).toEqual([{}]); + }); + + test('should set total count', () => { + expect(result.totalCount).toEqual(1); + }); + + test('should set inspect', () => { + expect(result.inspect).toEqual(false); + }); + }); + + describe('failure path', () => { + beforeAll(() => { + mockQuery.mockClear(); + (useApolloClient as jest.Mock).mockReset(); + (useApolloClient as jest.Mock).mockReturnValue({ + query: mockRejectQuery, + }); + const TestComponent = () => { + result = useQuery({ + endDate: 100, + errorMessage: 'fakeErrorMsg', + filterQuery: '', + histogramType: HistogramType.alerts, + isInspected: false, + stackByField: 'fakeField', + startDate: 0, + }); + + return
; + }; + + mount(); + }); + + test('should setData', () => { + expect(result.data).toEqual(null); + }); + + test('should set total count', () => { + expect(result.totalCount).toEqual(-1); + }); + + test('should set inspect', () => { + expect(result.inspect).toEqual(null); + }); + + test('should set error to toster', () => { + expect(errorToToaster).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/common/containers/matrix_histogram/index.ts b/x-pack/plugins/siem/public/common/containers/matrix_histogram/index.ts new file mode 100644 index 00000000000000..649ca526c21025 --- /dev/null +++ b/x-pack/plugins/siem/public/common/containers/matrix_histogram/index.ts @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty } from 'lodash/fp'; +import { useEffect, useMemo, useState, useRef } from 'react'; + +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { MatrixHistogramQueryProps } from '../../components/matrix_histogram/types'; +import { errorToToaster, useStateToaster } from '../../components/toasters'; +import { useUiSetting$ } from '../../lib/kibana'; +import { createFilter } from '../helpers'; +import { useApolloClient } from '../../utils/apollo_context'; +import { inputsModel } from '../../store'; +import { MatrixHistogramGqlQuery } from './index.gql_query'; +import { GetMatrixHistogramQuery, MatrixOverTimeHistogramData } from '../../../graphql/types'; + +export const useQuery = ({ + endDate, + errorMessage, + filterQuery, + histogramType, + indexToAdd, + isInspected, + stackByField, + startDate, +}: MatrixHistogramQueryProps) => { + const [configIndex] = useUiSetting$(DEFAULT_INDEX_KEY); + const defaultIndex = useMemo(() => { + if (indexToAdd != null && !isEmpty(indexToAdd)) { + return [...configIndex, ...indexToAdd]; + } + return configIndex; + }, [configIndex, indexToAdd]); + + const [, dispatchToaster] = useStateToaster(); + const refetch = useRef(); + const [loading, setLoading] = useState(false); + const [data, setData] = useState(null); + const [inspect, setInspect] = useState(null); + const [totalCount, setTotalCount] = useState(-1); + const apolloClient = useApolloClient(); + + useEffect(() => { + const matrixHistogramVariables: GetMatrixHistogramQuery.Variables = { + filterQuery: createFilter(filterQuery), + sourceId: 'default', + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + defaultIndex, + inspect: isInspected, + stackByField, + histogramType, + }; + let isSubscribed = true; + const abortCtrl = new AbortController(); + const abortSignal = abortCtrl.signal; + + async function fetchData() { + if (!apolloClient) return null; + setLoading(true); + return apolloClient + .query({ + query: MatrixHistogramGqlQuery, + fetchPolicy: 'network-only', + variables: matrixHistogramVariables, + context: { + fetchOptions: { + abortSignal, + }, + }, + }) + .then( + result => { + if (isSubscribed) { + const source = result?.data?.source?.MatrixHistogram ?? {}; + setData(source?.matrixHistogramData ?? []); + setTotalCount(source?.totalCount ?? -1); + setInspect(source?.inspect ?? null); + setLoading(false); + } + }, + error => { + if (isSubscribed) { + setData(null); + setTotalCount(-1); + setInspect(null); + setLoading(false); + errorToToaster({ title: errorMessage, error, dispatchToaster }); + } + } + ); + } + refetch.current = fetchData; + fetchData(); + return () => { + isSubscribed = false; + abortCtrl.abort(); + }; + }, [ + defaultIndex, + errorMessage, + filterQuery, + histogramType, + indexToAdd, + isInspected, + stackByField, + startDate, + endDate, + data, + ]); + + return { data, loading, inspect, totalCount, refetch: refetch.current }; +}; diff --git a/x-pack/plugins/siem/public/containers/query_template.tsx b/x-pack/plugins/siem/public/common/containers/query_template.tsx similarity index 98% rename from x-pack/plugins/siem/public/containers/query_template.tsx rename to x-pack/plugins/siem/public/common/containers/query_template.tsx index dfb452c24b86e5..fdc95c1dadfe16 100644 --- a/x-pack/plugins/siem/public/containers/query_template.tsx +++ b/x-pack/plugins/siem/public/common/containers/query_template.tsx @@ -8,7 +8,7 @@ import { ApolloQueryResult } from 'apollo-client'; import React from 'react'; import { FetchMoreOptions, FetchMoreQueryOptions, OperationVariables } from 'react-apollo'; -import { ESQuery } from '../../common/typed_json'; +import { ESQuery } from '../../../common/typed_json'; export interface QueryTemplateProps { id?: string; diff --git a/x-pack/plugins/siem/public/containers/query_template_paginated.tsx b/x-pack/plugins/siem/public/common/containers/query_template_paginated.tsx similarity index 98% rename from x-pack/plugins/siem/public/containers/query_template_paginated.tsx rename to x-pack/plugins/siem/public/common/containers/query_template_paginated.tsx index db618f216d83e7..446e1125b2807e 100644 --- a/x-pack/plugins/siem/public/containers/query_template_paginated.tsx +++ b/x-pack/plugins/siem/public/common/containers/query_template_paginated.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { FetchMoreOptions, FetchMoreQueryOptions, OperationVariables } from 'react-apollo'; import deepEqual from 'fast-deep-equal'; -import { ESQuery } from '../../common/typed_json'; +import { ESQuery } from '../../../common/typed_json'; import { inputsModel } from '../store/model'; import { generateTablePaginationOptions } from '../components/paginated_table/helpers'; diff --git a/x-pack/plugins/siem/public/containers/source/index.gql_query.ts b/x-pack/plugins/siem/public/common/containers/source/index.gql_query.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/source/index.gql_query.ts rename to x-pack/plugins/siem/public/common/containers/source/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/source/index.test.tsx b/x-pack/plugins/siem/public/common/containers/source/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/containers/source/index.test.tsx rename to x-pack/plugins/siem/public/common/containers/source/index.test.tsx diff --git a/x-pack/plugins/siem/public/common/containers/source/index.tsx b/x-pack/plugins/siem/public/common/containers/source/index.tsx new file mode 100644 index 00000000000000..8c33c556c67674 --- /dev/null +++ b/x-pack/plugins/siem/public/common/containers/source/index.tsx @@ -0,0 +1,177 @@ +/* + * 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 { isUndefined } from 'lodash'; +import { get, keyBy, pick, set, isEmpty } from 'lodash/fp'; +import { Query } from 'react-apollo'; +import React, { useEffect, useMemo, useState } from 'react'; +import memoizeOne from 'memoize-one'; +import { IIndexPattern } from 'src/plugins/data/public'; + +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { useUiSetting$ } from '../../lib/kibana'; + +import { IndexField, SourceQuery } from '../../../graphql/types'; + +import { sourceQuery } from './index.gql_query'; +import { useApolloClient } from '../../utils/apollo_context'; + +export { sourceQuery }; + +export interface BrowserField { + aggregatable: boolean; + category: string; + description: string | null; + example: string | number | null; + fields: Readonly>>; + format: string; + indexes: string[]; + name: string; + searchable: boolean; + type: string; +} + +export type BrowserFields = Readonly>>; + +export const getAllBrowserFields = (browserFields: BrowserFields): Array> => + Object.values(browserFields).reduce>>( + (acc, namespace) => [ + ...acc, + ...Object.values(namespace.fields != null ? namespace.fields : {}), + ], + [] + ); + +export const getAllFieldsByName = ( + browserFields: BrowserFields +): { [fieldName: string]: Partial } => + keyBy('name', getAllBrowserFields(browserFields)); + +interface WithSourceArgs { + indicesExist: boolean; + browserFields: BrowserFields; + indexPattern: IIndexPattern; +} + +interface WithSourceProps { + children: (args: WithSourceArgs) => React.ReactNode; + indexToAdd?: string[] | null; + sourceId: string; +} + +export const getIndexFields = memoizeOne( + (title: string, fields: IndexField[]): IIndexPattern => + fields && fields.length > 0 + ? { + fields: fields.map(field => pick(['name', 'searchable', 'type', 'aggregatable'], field)), + title, + } + : { fields: [], title } +); + +export const getBrowserFields = memoizeOne( + (title: string, fields: IndexField[]): BrowserFields => + fields && fields.length > 0 + ? fields.reduce( + (accumulator: BrowserFields, field: IndexField) => + set([field.category, 'fields', field.name], field, accumulator), + {} + ) + : {} +); + +export const WithSource = React.memo(({ children, indexToAdd, sourceId }) => { + const [configIndex] = useUiSetting$(DEFAULT_INDEX_KEY); + const defaultIndex = useMemo(() => { + if (indexToAdd != null && !isEmpty(indexToAdd)) { + return [...configIndex, ...indexToAdd]; + } + return configIndex; + }, [configIndex, indexToAdd]); + + return ( + + query={sourceQuery} + fetchPolicy="cache-first" + notifyOnNetworkStatusChange + variables={{ + sourceId, + defaultIndex, + }} + > + {({ data }) => + children({ + indicesExist: get('source.status.indicesExist', data), + browserFields: getBrowserFields( + defaultIndex.join(), + get('source.status.indexFields', data) + ), + indexPattern: getIndexFields(defaultIndex.join(), get('source.status.indexFields', data)), + }) + } + + ); +}); + +WithSource.displayName = 'WithSource'; + +export const indicesExistOrDataTemporarilyUnavailable = (indicesExist: boolean | undefined) => + indicesExist || isUndefined(indicesExist); + +export const useWithSource = (sourceId: string, indices: string[]) => { + const [loading, updateLoading] = useState(false); + const [indicesExist, setIndicesExist] = useState(undefined); + const [browserFields, setBrowserFields] = useState(null); + const [indexPattern, setIndexPattern] = useState(null); + const [errorMessage, updateErrorMessage] = useState(null); + + const apolloClient = useApolloClient(); + async function fetchSource(signal: AbortSignal) { + updateLoading(true); + if (apolloClient) { + apolloClient + .query({ + query: sourceQuery, + fetchPolicy: 'cache-first', + variables: { + sourceId, + defaultIndex: indices, + }, + context: { + fetchOptions: { + signal, + }, + }, + }) + .then( + result => { + updateLoading(false); + updateErrorMessage(null); + setIndicesExist(get('data.source.status.indicesExist', result)); + setBrowserFields( + getBrowserFields(indices.join(), get('data.source.status.indexFields', result)) + ); + setIndexPattern( + getIndexFields(indices.join(), get('data.source.status.indexFields', result)) + ); + }, + error => { + updateLoading(false); + updateErrorMessage(error.message); + } + ); + } + } + + useEffect(() => { + const abortCtrl = new AbortController(); + const signal = abortCtrl.signal; + fetchSource(signal); + return () => abortCtrl.abort(); + }, [apolloClient, sourceId, indices]); + + return { indicesExist, browserFields, indexPattern, loading, errorMessage }; +}; diff --git a/x-pack/plugins/siem/public/common/containers/source/mock.ts b/x-pack/plugins/siem/public/common/containers/source/mock.ts new file mode 100644 index 00000000000000..55e8b6ac02b128 --- /dev/null +++ b/x-pack/plugins/siem/public/common/containers/source/mock.ts @@ -0,0 +1,699 @@ +/* + * 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_INDEX_PATTERN } from '../../../../common/constants'; + +import { BrowserFields } from '.'; +import { sourceQuery } from './index.gql_query'; + +export const mocksSource = [ + { + request: { + query: sourceQuery, + variables: { + sourceId: 'default', + defaultIndex: DEFAULT_INDEX_PATTERN, + }, + }, + result: { + data: { + source: { + id: 'default', + configuration: {}, + status: { + indicesExist: true, + winlogbeatIndices: [ + 'winlogbeat-7.0.0-2019.02.17', + 'winlogbeat-7.0.0-2019.02.18', + 'winlogbeat-7.0.0-2019.02.19', + 'winlogbeat-7.0.0-2019.02.20', + 'winlogbeat-7.0.0-2019.02.21', + 'winlogbeat-7.0.0-2019.02.21-000001', + 'winlogbeat-7.0.0-2019.02.22', + 'winlogbeat-8.0.0-2019.02.19-000001', + ], + auditbeatIndices: [ + 'auditbeat-7.0.0-2019.02.17', + 'auditbeat-7.0.0-2019.02.18', + 'auditbeat-7.0.0-2019.02.19', + 'auditbeat-7.0.0-2019.02.20', + 'auditbeat-7.0.0-2019.02.21', + 'auditbeat-7.0.0-2019.02.21-000001', + 'auditbeat-7.0.0-2019.02.22', + 'auditbeat-8.0.0-2019.02.19-000001', + ], + filebeatIndices: [ + 'filebeat-7.0.0-iot-2019.06', + 'filebeat-7.0.0-iot-2019.07', + 'filebeat-7.0.0-iot-2019.08', + 'filebeat-7.0.0-iot-2019.09', + 'filebeat-7.0.0-iot-2019.10', + 'filebeat-8.0.0-2019.02.19-000001', + ], + indexFields: [ + { + category: 'base', + description: + 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', + example: '2016-05-23T08:05:34.853Z', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: '@timestamp', + searchable: true, + type: 'date', + aggregatable: true, + }, + { + category: 'agent', + description: + 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', + example: '8a4f500f', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.ephemeral_id', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'agent', + description: null, + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.hostname', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'agent', + description: + 'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.', + example: '8a4f500d', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.id', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'agent', + description: + 'Name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.', + example: 'foo', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.name', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a0', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a1', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a2', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'client', + description: + 'Some event client addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.address', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'client', + description: 'Bytes sent from the client to the server.', + example: '184', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.bytes', + searchable: true, + type: 'number', + aggregatable: true, + }, + { + category: 'client', + description: 'Client domain.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.domain', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'client', + description: 'Country ISO code.', + example: 'CA', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.geo.country_iso_code', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'cloud', + description: + 'The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.', + example: '666777888999', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'cloud.account.id', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'cloud', + description: 'Availability zone in which this host is running.', + example: 'us-east-1c', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'cloud.availability_zone', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'container', + description: 'Unique container id.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.id', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'container', + description: 'Name of the image the container was built on.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.image.name', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'container', + description: 'Container image tag.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.image.tag', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'destination', + description: + 'Some event destination addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.address', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + category: 'destination', + description: 'Bytes sent from the destination to the source.', + example: '184', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.bytes', + searchable: true, + type: 'number', + aggregatable: true, + }, + { + category: 'destination', + description: 'Destination domain.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.domain', + searchable: true, + type: 'string', + aggregatable: true, + }, + { + aggregatable: true, + category: 'destination', + description: + 'IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.ip', + searchable: true, + type: 'ip', + }, + { + aggregatable: true, + category: 'destination', + description: 'Port of the destination.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.port', + searchable: true, + type: 'long', + }, + { + aggregatable: true, + category: 'source', + description: + 'IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'source.ip', + searchable: true, + type: 'ip', + }, + { + aggregatable: true, + category: 'source', + description: 'Port of the source.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'source.port', + searchable: true, + type: 'long', + }, + { + aggregatable: true, + category: 'event', + description: + 'event.end contains the date when the event ended or when the activity was last observed.', + example: null, + format: '', + indexes: DEFAULT_INDEX_PATTERN, + name: 'event.end', + searchable: true, + type: 'date', + }, + ], + }, + }, + }, + }, + }, +]; + +export const mockIndexFields = [ + { aggregatable: true, name: '@timestamp', searchable: true, type: 'date' }, + { aggregatable: true, name: 'agent.ephemeral_id', searchable: true, type: 'string' }, + { aggregatable: true, name: 'agent.hostname', searchable: true, type: 'string' }, + { aggregatable: true, name: 'agent.id', searchable: true, type: 'string' }, + { aggregatable: true, name: 'agent.name', searchable: true, type: 'string' }, + { aggregatable: true, name: 'auditd.data.a0', searchable: true, type: 'string' }, + { aggregatable: true, name: 'auditd.data.a1', searchable: true, type: 'string' }, + { aggregatable: true, name: 'auditd.data.a2', searchable: true, type: 'string' }, + { aggregatable: true, name: 'client.address', searchable: true, type: 'string' }, + { aggregatable: true, name: 'client.bytes', searchable: true, type: 'number' }, + { aggregatable: true, name: 'client.domain', searchable: true, type: 'string' }, + { aggregatable: true, name: 'client.geo.country_iso_code', searchable: true, type: 'string' }, + { aggregatable: true, name: 'cloud.account.id', searchable: true, type: 'string' }, + { aggregatable: true, name: 'cloud.availability_zone', searchable: true, type: 'string' }, + { aggregatable: true, name: 'container.id', searchable: true, type: 'string' }, + { aggregatable: true, name: 'container.image.name', searchable: true, type: 'string' }, + { aggregatable: true, name: 'container.image.tag', searchable: true, type: 'string' }, + { aggregatable: true, name: 'destination.address', searchable: true, type: 'string' }, + { aggregatable: true, name: 'destination.bytes', searchable: true, type: 'number' }, + { aggregatable: true, name: 'destination.domain', searchable: true, type: 'string' }, + { aggregatable: true, name: 'destination.ip', searchable: true, type: 'ip' }, + { aggregatable: true, name: 'destination.port', searchable: true, type: 'long' }, + { aggregatable: true, name: 'source.ip', searchable: true, type: 'ip' }, + { aggregatable: true, name: 'source.port', searchable: true, type: 'long' }, + { aggregatable: true, name: 'event.end', searchable: true, type: 'date' }, +]; + +export const mockBrowserFields: BrowserFields = { + agent: { + fields: { + 'agent.ephemeral_id': { + aggregatable: true, + category: 'agent', + description: + 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', + example: '8a4f500f', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.ephemeral_id', + searchable: true, + type: 'string', + }, + 'agent.hostname': { + aggregatable: true, + category: 'agent', + description: null, + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.hostname', + searchable: true, + type: 'string', + }, + 'agent.id': { + aggregatable: true, + category: 'agent', + description: + 'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.', + example: '8a4f500d', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.id', + searchable: true, + type: 'string', + }, + 'agent.name': { + aggregatable: true, + category: 'agent', + description: + 'Name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.', + example: 'foo', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.name', + searchable: true, + type: 'string', + }, + }, + }, + auditd: { + fields: { + 'auditd.data.a0': { + aggregatable: true, + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a0', + searchable: true, + type: 'string', + }, + 'auditd.data.a1': { + aggregatable: true, + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a1', + searchable: true, + type: 'string', + }, + 'auditd.data.a2': { + aggregatable: true, + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a2', + searchable: true, + type: 'string', + }, + }, + }, + base: { + fields: { + '@timestamp': { + aggregatable: true, + category: 'base', + description: + 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', + example: '2016-05-23T08:05:34.853Z', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: '@timestamp', + searchable: true, + type: 'date', + }, + }, + }, + client: { + fields: { + 'client.address': { + aggregatable: true, + category: 'client', + description: + 'Some event client addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.address', + searchable: true, + type: 'string', + }, + 'client.bytes': { + aggregatable: true, + category: 'client', + description: 'Bytes sent from the client to the server.', + example: '184', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.bytes', + searchable: true, + type: 'number', + }, + 'client.domain': { + aggregatable: true, + category: 'client', + description: 'Client domain.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.domain', + searchable: true, + type: 'string', + }, + 'client.geo.country_iso_code': { + aggregatable: true, + category: 'client', + description: 'Country ISO code.', + example: 'CA', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.geo.country_iso_code', + searchable: true, + type: 'string', + }, + }, + }, + cloud: { + fields: { + 'cloud.account.id': { + aggregatable: true, + category: 'cloud', + description: + 'The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.', + example: '666777888999', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'cloud.account.id', + searchable: true, + type: 'string', + }, + 'cloud.availability_zone': { + aggregatable: true, + category: 'cloud', + description: 'Availability zone in which this host is running.', + example: 'us-east-1c', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'cloud.availability_zone', + searchable: true, + type: 'string', + }, + }, + }, + container: { + fields: { + 'container.id': { + aggregatable: true, + category: 'container', + description: 'Unique container id.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.id', + searchable: true, + type: 'string', + }, + 'container.image.name': { + aggregatable: true, + category: 'container', + description: 'Name of the image the container was built on.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.image.name', + searchable: true, + type: 'string', + }, + 'container.image.tag': { + aggregatable: true, + category: 'container', + description: 'Container image tag.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.image.tag', + searchable: true, + type: 'string', + }, + }, + }, + destination: { + fields: { + 'destination.address': { + aggregatable: true, + category: 'destination', + description: + 'Some event destination addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.address', + searchable: true, + type: 'string', + }, + 'destination.bytes': { + aggregatable: true, + category: 'destination', + description: 'Bytes sent from the destination to the source.', + example: '184', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.bytes', + searchable: true, + type: 'number', + }, + 'destination.domain': { + aggregatable: true, + category: 'destination', + description: 'Destination domain.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.domain', + searchable: true, + type: 'string', + }, + 'destination.ip': { + aggregatable: true, + category: 'destination', + description: + 'IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.ip', + searchable: true, + type: 'ip', + }, + 'destination.port': { + aggregatable: true, + category: 'destination', + description: 'Port of the destination.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'destination.port', + searchable: true, + type: 'long', + }, + }, + }, + event: { + fields: { + 'event.end': { + category: 'event', + description: + 'event.end contains the date when the event ended or when the activity was last observed.', + example: null, + format: '', + indexes: DEFAULT_INDEX_PATTERN, + name: 'event.end', + searchable: true, + type: 'date', + aggregatable: true, + }, + }, + }, + source: { + fields: { + 'source.ip': { + aggregatable: true, + category: 'source', + description: 'IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'source.ip', + searchable: true, + type: 'ip', + }, + 'source.port': { + aggregatable: true, + category: 'source', + description: 'Port of the source.', + example: '', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'source.port', + searchable: true, + type: 'long', + }, + }, + }, +}; diff --git a/x-pack/plugins/siem/public/hooks/api/__mock__/api.tsx b/x-pack/plugins/siem/public/common/hooks/api/__mock__/api.tsx similarity index 100% rename from x-pack/plugins/siem/public/hooks/api/__mock__/api.tsx rename to x-pack/plugins/siem/public/common/hooks/api/__mock__/api.tsx diff --git a/x-pack/plugins/siem/public/hooks/api/api.tsx b/x-pack/plugins/siem/public/common/hooks/api/api.tsx similarity index 94% rename from x-pack/plugins/siem/public/hooks/api/api.tsx rename to x-pack/plugins/siem/public/common/hooks/api/api.tsx index 8120e3819d9a81..12863bffcf5150 100644 --- a/x-pack/plugins/siem/public/hooks/api/api.tsx +++ b/x-pack/plugins/siem/public/common/hooks/api/api.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { StartServices } from '../../plugin'; +import { StartServices } from '../../../plugin'; import { IndexPatternSavedObject, IndexPatternSavedObjectAttributes } from '../types'; /** diff --git a/x-pack/plugins/siem/public/hooks/api/helpers.test.tsx b/x-pack/plugins/siem/public/common/hooks/api/helpers.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/hooks/api/helpers.test.tsx rename to x-pack/plugins/siem/public/common/hooks/api/helpers.test.tsx diff --git a/x-pack/plugins/siem/public/hooks/api/helpers.tsx b/x-pack/plugins/siem/public/common/hooks/api/helpers.tsx similarity index 100% rename from x-pack/plugins/siem/public/hooks/api/helpers.tsx rename to x-pack/plugins/siem/public/common/hooks/api/helpers.tsx diff --git a/x-pack/plugins/siem/public/hooks/translations.ts b/x-pack/plugins/siem/public/common/hooks/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/hooks/translations.ts rename to x-pack/plugins/siem/public/common/hooks/translations.ts diff --git a/x-pack/plugins/siem/public/common/hooks/types.ts b/x-pack/plugins/siem/public/common/hooks/types.ts new file mode 100644 index 00000000000000..36b626b0ba9f1d --- /dev/null +++ b/x-pack/plugins/siem/public/common/hooks/types.ts @@ -0,0 +1,16 @@ +/* + * 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. + */ + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { SimpleSavedObject } from '../../../../../../src/core/public'; + +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type IndexPatternSavedObjectAttributes = { title: string }; + +export type IndexPatternSavedObject = Pick< + SimpleSavedObject, + 'type' | 'id' | 'attributes' | '_version' +>; diff --git a/x-pack/plugins/siem/public/hooks/use_add_to_timeline.tsx b/x-pack/plugins/siem/public/common/hooks/use_add_to_timeline.tsx similarity index 95% rename from x-pack/plugins/siem/public/hooks/use_add_to_timeline.tsx rename to x-pack/plugins/siem/public/common/hooks/use_add_to_timeline.tsx index be0ddb153457e9..9d3c1efbe34511 100644 --- a/x-pack/plugins/siem/public/hooks/use_add_to_timeline.tsx +++ b/x-pack/plugins/siem/public/common/hooks/use_add_to_timeline.tsx @@ -9,8 +9,8 @@ import { useCallback } from 'react'; import { DraggableId, FluidDragActions, Position, SensorAPI } from 'react-beautiful-dnd'; import { IS_DRAGGING_CLASS_NAME } from '../components/drag_and_drop/helpers'; -import { HIGHLIGHTED_DROP_TARGET_CLASS_NAME } from '../components/timeline/data_providers/empty'; -import { EMPTY_PROVIDERS_GROUP_CLASS_NAME } from '../components/timeline/data_providers/providers'; +import { HIGHLIGHTED_DROP_TARGET_CLASS_NAME } from '../../timelines/components/timeline/data_providers/empty'; +import { EMPTY_PROVIDERS_GROUP_CLASS_NAME } from '../../timelines/components/timeline/data_providers/providers'; let _sensorApiSingleton: SensorAPI; diff --git a/x-pack/plugins/siem/public/hooks/use_index_patterns.tsx b/x-pack/plugins/siem/public/common/hooks/use_index_patterns.tsx similarity index 100% rename from x-pack/plugins/siem/public/hooks/use_index_patterns.tsx rename to x-pack/plugins/siem/public/common/hooks/use_index_patterns.tsx diff --git a/x-pack/plugins/siem/public/hooks/use_providers_portal.tsx b/x-pack/plugins/siem/public/common/hooks/use_providers_portal.tsx similarity index 100% rename from x-pack/plugins/siem/public/hooks/use_providers_portal.tsx rename to x-pack/plugins/siem/public/common/hooks/use_providers_portal.tsx diff --git a/x-pack/plugins/siem/public/lib/clipboard/clipboard.tsx b/x-pack/plugins/siem/public/common/lib/clipboard/clipboard.tsx similarity index 100% rename from x-pack/plugins/siem/public/lib/clipboard/clipboard.tsx rename to x-pack/plugins/siem/public/common/lib/clipboard/clipboard.tsx diff --git a/x-pack/plugins/siem/public/lib/clipboard/translations.ts b/x-pack/plugins/siem/public/common/lib/clipboard/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/lib/clipboard/translations.ts rename to x-pack/plugins/siem/public/common/lib/clipboard/translations.ts diff --git a/x-pack/plugins/siem/public/lib/clipboard/with_copy_to_clipboard.tsx b/x-pack/plugins/siem/public/common/lib/clipboard/with_copy_to_clipboard.tsx similarity index 100% rename from x-pack/plugins/siem/public/lib/clipboard/with_copy_to_clipboard.tsx rename to x-pack/plugins/siem/public/common/lib/clipboard/with_copy_to_clipboard.tsx diff --git a/x-pack/plugins/siem/public/common/lib/compose/helpers.test.ts b/x-pack/plugins/siem/public/common/lib/compose/helpers.test.ts new file mode 100644 index 00000000000000..4a3d734d0a6d45 --- /dev/null +++ b/x-pack/plugins/siem/public/common/lib/compose/helpers.test.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'; +import { errorLink, reTryOneTimeOnErrorLink } from '../../containers/errors'; +import { getLinks } from './helpers'; +import { withClientState } from 'apollo-link-state'; +import * as apolloLinkHttp from 'apollo-link-http'; +import introspectionQueryResultData from '../../../graphql/introspection.json'; + +jest.mock('apollo-cache-inmemory'); +jest.mock('apollo-link-http'); +jest.mock('apollo-link-state'); +jest.mock('../../containers/errors'); +const mockWithClientState = 'mockWithClientState'; +const mockHttpLink = { mockHttpLink: 'mockHttpLink' }; + +// @ts-ignore +withClientState.mockReturnValue(mockWithClientState); +// @ts-ignore +apolloLinkHttp.createHttpLink.mockImplementation(() => mockHttpLink); + +describe('getLinks helper', () => { + test('It should return links in correct order', () => { + const mockCache = new InMemoryCache({ + dataIdFromObject: () => null, + fragmentMatcher: new IntrospectionFragmentMatcher({ + introspectionQueryResultData, + }), + }); + const links = getLinks(mockCache, 'basePath'); + expect(links[0]).toEqual(errorLink); + expect(links[1]).toEqual(reTryOneTimeOnErrorLink); + expect(links[2]).toEqual(mockWithClientState); + expect(links[3]).toEqual(mockHttpLink); + }); +}); diff --git a/x-pack/plugins/siem/public/lib/compose/helpers.ts b/x-pack/plugins/siem/public/common/lib/compose/helpers.ts similarity index 100% rename from x-pack/plugins/siem/public/lib/compose/helpers.ts rename to x-pack/plugins/siem/public/common/lib/compose/helpers.ts diff --git a/x-pack/plugins/siem/public/lib/compose/kibana_compose.tsx b/x-pack/plugins/siem/public/common/lib/compose/kibana_compose.tsx similarity index 83% rename from x-pack/plugins/siem/public/lib/compose/kibana_compose.tsx rename to x-pack/plugins/siem/public/common/lib/compose/kibana_compose.tsx index fb30c9a5411ed7..f7c7c65318482d 100644 --- a/x-pack/plugins/siem/public/lib/compose/kibana_compose.tsx +++ b/x-pack/plugins/siem/public/common/lib/compose/kibana_compose.tsx @@ -8,8 +8,9 @@ import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemo import ApolloClient from 'apollo-client'; import { ApolloLink } from 'apollo-link'; -import { CoreStart } from '../../../../../../src/core/public'; -import introspectionQueryResultData from '../../graphql/introspection.json'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { CoreStart } from '../../../../../../../src/core/public'; +import introspectionQueryResultData from '../../../graphql/introspection.json'; import { AppFrontendLibs } from '../lib'; import { getLinks } from './helpers'; diff --git a/x-pack/plugins/siem/public/common/lib/connectors/components/connector_flyout/index.tsx b/x-pack/plugins/siem/public/common/lib/connectors/components/connector_flyout/index.tsx new file mode 100644 index 00000000000000..246a7cced37e5f --- /dev/null +++ b/x-pack/plugins/siem/public/common/lib/connectors/components/connector_flyout/index.tsx @@ -0,0 +1,151 @@ +/* + * 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, { useCallback, useEffect } from 'react'; +import { EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSpacer } from '@elastic/eui'; + +import { isEmpty, get } from 'lodash/fp'; + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ActionConnectorFieldsProps } from '../../../../../../../triggers_actions_ui/public/types'; +import { FieldMapping } from '../../../../../cases/components/configure_cases/field_mapping'; + +import { CasesConfigurationMapping } from '../../../../../cases/containers/configure/types'; + +import * as i18n from '../../translations'; +import { ActionConnector, ConnectorFlyoutHOCProps } from '../../types'; +import { createDefaultMapping } from '../../utils'; +import { connectorsConfiguration } from '../../config'; + +export const withConnectorFlyout = ({ + ConnectorFormComponent, + connectorActionTypeId, + secretKeys = [], + configKeys = [], +}: ConnectorFlyoutHOCProps) => { + const ConnectorFlyout: React.FC> = ({ + action, + editActionConfig, + editActionSecrets, + errors, + }) => { + /* We do not provide defaults values to the fields (like empty string for apiUrl) intentionally. + * If we do, errors will be shown the first time the flyout is open even though the user did not + * interact with the form. Also, we would like to show errors for empty fields provided by the user. + /*/ + const { apiUrl, casesConfiguration: { mapping = [] } = {} } = action.config; + const configKeysWithDefault = [...configKeys, 'apiUrl']; + + const isApiUrlInvalid: boolean = errors.apiUrl.length > 0 && apiUrl != null; + + /** + * We need to distinguish between the add flyout and the edit flyout. + * useEffect will run only once on component mount. + * This guarantees that the function below will run only once. + * On the first render of the component the apiUrl can be either undefined or filled. + * If it is filled then we are on the edit flyout. Otherwise we are on the add flyout. + */ + + useEffect(() => { + if (!isEmpty(apiUrl)) { + secretKeys.forEach((key: string) => editActionSecrets(key, '')); + } + }, []); + + if (isEmpty(mapping)) { + editActionConfig('casesConfiguration', { + ...action.config.casesConfiguration, + mapping: createDefaultMapping(connectorsConfiguration[connectorActionTypeId].fields), + }); + } + + const handleOnChangeActionConfig = useCallback( + (key: string, value: string) => editActionConfig(key, value), + [] + ); + + const handleOnBlurActionConfig = useCallback( + (key: string) => { + if (configKeysWithDefault.includes(key) && get(key, action.config) == null) { + editActionConfig(key, ''); + } + }, + [action.config] + ); + + const handleOnChangeSecretConfig = useCallback( + (key: string, value: string) => editActionSecrets(key, value), + [] + ); + + const handleOnBlurSecretConfig = useCallback( + (key: string) => { + if (secretKeys.includes(key) && get(key, action.secrets) == null) { + editActionSecrets(key, ''); + } + }, + [action.secrets] + ); + + const handleOnChangeMappingConfig = useCallback( + (newMapping: CasesConfigurationMapping[]) => + editActionConfig('casesConfiguration', { + ...action.config.casesConfiguration, + mapping: newMapping, + }), + [action.config] + ); + + return ( + <> + + + + handleOnChangeActionConfig('apiUrl', evt.target.value)} + onBlur={handleOnBlurActionConfig.bind(null, 'apiUrl')} + /> + + + + + + + + + + + + + ); + }; + + return ConnectorFlyout; +}; diff --git a/x-pack/plugins/siem/public/lib/connectors/config.ts b/x-pack/plugins/siem/public/common/lib/connectors/config.ts similarity index 100% rename from x-pack/plugins/siem/public/lib/connectors/config.ts rename to x-pack/plugins/siem/public/common/lib/connectors/config.ts diff --git a/x-pack/plugins/siem/public/lib/connectors/index.ts b/x-pack/plugins/siem/public/common/lib/connectors/index.ts similarity index 100% rename from x-pack/plugins/siem/public/lib/connectors/index.ts rename to x-pack/plugins/siem/public/common/lib/connectors/index.ts diff --git a/x-pack/plugins/siem/public/lib/connectors/jira/config.ts b/x-pack/plugins/siem/public/common/lib/connectors/jira/config.ts similarity index 100% rename from x-pack/plugins/siem/public/lib/connectors/jira/config.ts rename to x-pack/plugins/siem/public/common/lib/connectors/jira/config.ts diff --git a/x-pack/plugins/siem/public/lib/connectors/jira/flyout.tsx b/x-pack/plugins/siem/public/common/lib/connectors/jira/flyout.tsx similarity index 100% rename from x-pack/plugins/siem/public/lib/connectors/jira/flyout.tsx rename to x-pack/plugins/siem/public/common/lib/connectors/jira/flyout.tsx diff --git a/x-pack/plugins/siem/public/common/lib/connectors/jira/index.tsx b/x-pack/plugins/siem/public/common/lib/connectors/jira/index.tsx new file mode 100644 index 00000000000000..f7e293d9ad2f8e --- /dev/null +++ b/x-pack/plugins/siem/public/common/lib/connectors/jira/index.tsx @@ -0,0 +1,54 @@ +/* + * 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 { lazy } from 'react'; +import { + ValidationResult, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../triggers_actions_ui/public/types'; + +import { connector } from './config'; +import { createActionType } from '../utils'; +import logo from './logo.svg'; +import { JiraActionConnector } from './types'; +import * as i18n from './translations'; + +interface Errors { + projectKey: string[]; + email: string[]; + apiToken: string[]; +} + +const validateConnector = (action: JiraActionConnector): ValidationResult => { + const errors: Errors = { + projectKey: [], + email: [], + apiToken: [], + }; + + if (!action.config.projectKey) { + errors.projectKey = [...errors.projectKey, i18n.JIRA_PROJECT_KEY_REQUIRED]; + } + + if (!action.secrets.email) { + errors.email = [...errors.email, i18n.EMAIL_REQUIRED]; + } + + if (!action.secrets.apiToken) { + errors.apiToken = [...errors.apiToken, i18n.API_TOKEN_REQUIRED]; + } + + return { errors }; +}; + +export const getActionType = createActionType({ + id: connector.id, + iconClass: logo, + selectMessage: i18n.JIRA_DESC, + actionTypeTitle: connector.name, + validateConnector, + actionConnectorFields: lazy(() => import('./flyout')), +}); diff --git a/x-pack/plugins/siem/public/lib/connectors/jira/logo.svg b/x-pack/plugins/siem/public/common/lib/connectors/jira/logo.svg similarity index 100% rename from x-pack/plugins/siem/public/lib/connectors/jira/logo.svg rename to x-pack/plugins/siem/public/common/lib/connectors/jira/logo.svg diff --git a/x-pack/plugins/siem/public/lib/connectors/jira/translations.ts b/x-pack/plugins/siem/public/common/lib/connectors/jira/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/lib/connectors/jira/translations.ts rename to x-pack/plugins/siem/public/common/lib/connectors/jira/translations.ts diff --git a/x-pack/plugins/siem/public/common/lib/connectors/jira/types.ts b/x-pack/plugins/siem/public/common/lib/connectors/jira/types.ts new file mode 100644 index 00000000000000..fafb4a0d41fb3b --- /dev/null +++ b/x-pack/plugins/siem/public/common/lib/connectors/jira/types.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. + */ + +/* eslint-disable no-restricted-imports */ +/* eslint-disable @kbn/eslint/no-restricted-paths */ + +import { + JiraPublicConfigurationType, + JiraSecretConfigurationType, +} from '../../../../../../actions/server/builtin_action_types/jira/types'; + +export { JiraFieldsType } from '../../../../../../case/common/api/connectors'; + +export * from '../types'; + +export interface JiraActionConnector { + config: JiraPublicConfigurationType; + secrets: JiraSecretConfigurationType; +} diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow/config.ts b/x-pack/plugins/siem/public/common/lib/connectors/servicenow/config.ts similarity index 100% rename from x-pack/plugins/siem/public/lib/connectors/servicenow/config.ts rename to x-pack/plugins/siem/public/common/lib/connectors/servicenow/config.ts diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow/flyout.tsx b/x-pack/plugins/siem/public/common/lib/connectors/servicenow/flyout.tsx similarity index 100% rename from x-pack/plugins/siem/public/lib/connectors/servicenow/flyout.tsx rename to x-pack/plugins/siem/public/common/lib/connectors/servicenow/flyout.tsx diff --git a/x-pack/plugins/siem/public/common/lib/connectors/servicenow/index.tsx b/x-pack/plugins/siem/public/common/lib/connectors/servicenow/index.tsx new file mode 100644 index 00000000000000..c9c5298365e817 --- /dev/null +++ b/x-pack/plugins/siem/public/common/lib/connectors/servicenow/index.tsx @@ -0,0 +1,47 @@ +/* + * 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 { lazy } from 'react'; +import { + ValidationResult, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../triggers_actions_ui/public/types'; +import { connector } from './config'; +import { createActionType } from '../utils'; +import logo from './logo.svg'; +import { ServiceNowActionConnector } from './types'; +import * as i18n from './translations'; + +interface Errors { + username: string[]; + password: string[]; +} + +const validateConnector = (action: ServiceNowActionConnector): ValidationResult => { + const errors: Errors = { + username: [], + password: [], + }; + + if (!action.secrets.username) { + errors.username = [...errors.username, i18n.USERNAME_REQUIRED]; + } + + if (!action.secrets.password) { + errors.password = [...errors.password, i18n.PASSWORD_REQUIRED]; + } + + return { errors }; +}; + +export const getActionType = createActionType({ + id: connector.id, + iconClass: logo, + selectMessage: i18n.SERVICENOW_DESC, + actionTypeTitle: connector.name, + validateConnector, + actionConnectorFields: lazy(() => import('./flyout')), +}); diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow/logo.svg b/x-pack/plugins/siem/public/common/lib/connectors/servicenow/logo.svg similarity index 100% rename from x-pack/plugins/siem/public/lib/connectors/servicenow/logo.svg rename to x-pack/plugins/siem/public/common/lib/connectors/servicenow/logo.svg diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow/translations.ts b/x-pack/plugins/siem/public/common/lib/connectors/servicenow/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/lib/connectors/servicenow/translations.ts rename to x-pack/plugins/siem/public/common/lib/connectors/servicenow/translations.ts diff --git a/x-pack/plugins/siem/public/common/lib/connectors/servicenow/types.ts b/x-pack/plugins/siem/public/common/lib/connectors/servicenow/types.ts new file mode 100644 index 00000000000000..b4a80e28c8d154 --- /dev/null +++ b/x-pack/plugins/siem/public/common/lib/connectors/servicenow/types.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. + */ + +/* eslint-disable no-restricted-imports */ +/* eslint-disable @kbn/eslint/no-restricted-paths */ + +import { + ServiceNowPublicConfigurationType, + ServiceNowSecretConfigurationType, +} from '../../../../../../actions/server/builtin_action_types/servicenow/types'; + +export { ServiceNowFieldsType } from '../../../../../../case/common/api/connectors'; + +export * from '../types'; + +export interface ServiceNowActionConnector { + config: ServiceNowPublicConfigurationType; + secrets: ServiceNowSecretConfigurationType; +} diff --git a/x-pack/plugins/siem/public/lib/connectors/translations.ts b/x-pack/plugins/siem/public/common/lib/connectors/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/lib/connectors/translations.ts rename to x-pack/plugins/siem/public/common/lib/connectors/translations.ts diff --git a/x-pack/plugins/siem/public/common/lib/connectors/types.ts b/x-pack/plugins/siem/public/common/lib/connectors/types.ts new file mode 100644 index 00000000000000..1d688ad9b1d6a9 --- /dev/null +++ b/x-pack/plugins/siem/public/common/lib/connectors/types.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable no-restricted-imports */ +/* eslint-disable @kbn/eslint/no-restricted-paths */ + +import { ActionType } from '../../../../../triggers_actions_ui/public'; +import { IErrorObject } from '../../../../../triggers_actions_ui/public/types'; +import { ExternalIncidentServiceConfiguration } from '../../../../../actions/server/builtin_action_types/case/types'; + +import { + ActionType as ThirdPartySupportedActions, + CaseField, +} from '../../../../../case/common/api'; + +export { ThirdPartyField as AllThirdPartyFields } from '../../../../../case/common/api'; + +export interface ThirdPartyField { + label: string; + validSourceFields: CaseField[]; + defaultSourceField: CaseField; + defaultActionType: ThirdPartySupportedActions; +} + +export interface ConnectorConfiguration extends ActionType { + logo: string; + fields: Record; +} + +export interface ActionConnector { + config: ExternalIncidentServiceConfiguration; + secrets: {}; +} + +export interface ActionConnectorParams { + message: string; +} + +export interface ActionConnectorValidationErrors { + apiUrl: string[]; +} + +export type Optional = Omit & Partial; + +export interface ConnectorFlyoutFormProps { + errors: IErrorObject; + action: T; + onChangeSecret: (key: string, value: string) => void; + onBlurSecret: (key: string) => void; + onChangeConfig: (key: string, value: string) => void; + onBlurConfig: (key: string) => void; +} + +export interface ConnectorFlyoutHOCProps { + ConnectorFormComponent: React.FC>; + connectorActionTypeId: string; + configKeys?: string[]; + secretKeys?: string[]; +} diff --git a/x-pack/plugins/siem/public/common/lib/connectors/utils.ts b/x-pack/plugins/siem/public/common/lib/connectors/utils.ts new file mode 100644 index 00000000000000..b9c90a593b2020 --- /dev/null +++ b/x-pack/plugins/siem/public/common/lib/connectors/utils.ts @@ -0,0 +1,75 @@ +/* + * 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 { + ActionTypeModel, + ValidationResult, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../triggers_actions_ui/public/types'; + +import { + ActionConnector, + ActionConnectorParams, + ActionConnectorValidationErrors, + Optional, + ThirdPartyField, +} from './types'; +import { isUrlInvalid } from './validators'; + +import * as i18n from './translations'; +import { CasesConfigurationMapping } from '../../../cases/containers/configure/types'; + +export const createActionType = ({ + id, + actionTypeTitle, + selectMessage, + iconClass, + validateConnector, + validateParams = connectorParamsValidator, + actionConnectorFields, + actionParamsFields = null, +}: Optional) => (): ActionTypeModel => { + return { + id, + iconClass, + selectMessage, + actionTypeTitle, + validateConnector: (action: ActionConnector): ValidationResult => { + const errors: ActionConnectorValidationErrors = { + apiUrl: [], + }; + + if (!action.config.apiUrl) { + errors.apiUrl = [...errors.apiUrl, i18n.API_URL_REQUIRED]; + } + + if (isUrlInvalid(action.config.apiUrl)) { + errors.apiUrl = [...errors.apiUrl, i18n.API_URL_INVALID]; + } + + return { errors: { ...errors, ...validateConnector(action).errors } }; + }, + validateParams, + actionConnectorFields, + actionParamsFields, + }; +}; + +const connectorParamsValidator = (actionParams: ActionConnectorParams): ValidationResult => { + return { errors: {} }; +}; + +export const createDefaultMapping = ( + fields: Record +): CasesConfigurationMapping[] => + Object.keys(fields).map( + key => + ({ + source: fields[key].defaultSourceField, + target: key, + actionType: fields[key].defaultActionType, + } as CasesConfigurationMapping) + ); diff --git a/x-pack/plugins/siem/public/lib/connectors/validators.ts b/x-pack/plugins/siem/public/common/lib/connectors/validators.ts similarity index 100% rename from x-pack/plugins/siem/public/lib/connectors/validators.ts rename to x-pack/plugins/siem/public/common/lib/connectors/validators.ts diff --git a/x-pack/plugins/siem/public/lib/helpers/index.test.tsx b/x-pack/plugins/siem/public/common/lib/helpers/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/lib/helpers/index.test.tsx rename to x-pack/plugins/siem/public/common/lib/helpers/index.test.tsx diff --git a/x-pack/plugins/siem/public/lib/helpers/index.tsx b/x-pack/plugins/siem/public/common/lib/helpers/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/lib/helpers/index.tsx rename to x-pack/plugins/siem/public/common/lib/helpers/index.tsx diff --git a/x-pack/plugins/siem/public/lib/helpers/scheduler.ts b/x-pack/plugins/siem/public/common/lib/helpers/scheduler.ts similarity index 100% rename from x-pack/plugins/siem/public/lib/helpers/scheduler.ts rename to x-pack/plugins/siem/public/common/lib/helpers/scheduler.ts diff --git a/x-pack/plugins/siem/public/lib/history/index.ts b/x-pack/plugins/siem/public/common/lib/history/index.ts similarity index 100% rename from x-pack/plugins/siem/public/lib/history/index.ts rename to x-pack/plugins/siem/public/common/lib/history/index.ts diff --git a/x-pack/plugins/siem/public/lib/keury/index.test.ts b/x-pack/plugins/siem/public/common/lib/keury/index.test.ts similarity index 100% rename from x-pack/plugins/siem/public/lib/keury/index.test.ts rename to x-pack/plugins/siem/public/common/lib/keury/index.test.ts diff --git a/x-pack/plugins/siem/public/common/lib/keury/index.ts b/x-pack/plugins/siem/public/common/lib/keury/index.ts new file mode 100644 index 00000000000000..53f845de48fb31 --- /dev/null +++ b/x-pack/plugins/siem/public/common/lib/keury/index.ts @@ -0,0 +1,113 @@ +/* + * 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 { isEmpty, isString, flow } from 'lodash/fp'; +import { + EsQueryConfig, + Query, + Filter, + esQuery, + esKuery, + IIndexPattern, +} from '../../../../../../../src/plugins/data/public'; + +import { JsonObject } from '../../../../../../../src/plugins/kibana_utils/public'; + +import { KueryFilterQuery } from '../../store'; + +export const convertKueryToElasticSearchQuery = ( + kueryExpression: string, + indexPattern?: IIndexPattern +) => { + try { + return kueryExpression + ? JSON.stringify( + esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kueryExpression), indexPattern) + ) + : ''; + } catch (err) { + return ''; + } +}; + +export const convertKueryToDslFilter = ( + kueryExpression: string, + indexPattern: IIndexPattern +): JsonObject => { + try { + return kueryExpression + ? esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kueryExpression), indexPattern) + : {}; + } catch (err) { + return {}; + } +}; + +export const escapeQueryValue = (val: number | string = ''): string | number => { + if (isString(val)) { + if (isEmpty(val)) { + return '""'; + } + return `"${escapeKuery(val)}"`; + } + + return val; +}; + +export const isFromKueryExpressionValid = (kqlFilterQuery: KueryFilterQuery | null): boolean => { + if (kqlFilterQuery && kqlFilterQuery.kind === 'kuery') { + try { + esKuery.fromKueryExpression(kqlFilterQuery.expression); + } catch (err) { + return false; + } + } + return true; +}; + +const escapeWhitespace = (val: string) => + val + .replace(/\t/g, '\\t') + .replace(/\r/g, '\\r') + .replace(/\n/g, '\\n'); + +// See the SpecialCharacter rule in kuery.peg +const escapeSpecialCharacters = (val: string) => val.replace(/["]/g, '\\$&'); // $& means the whole matched string + +// See the Keyword rule in kuery.peg +const escapeAndOr = (val: string) => val.replace(/(\s+)(and|or)(\s+)/gi, '$1\\$2$3'); + +const escapeNot = (val: string) => val.replace(/not(\s+)/gi, '\\$&'); + +export const escapeKuery = flow(escapeSpecialCharacters, escapeAndOr, escapeNot, escapeWhitespace); + +export const convertToBuildEsQuery = ({ + config, + indexPattern, + queries, + filters, +}: { + config: EsQueryConfig; + indexPattern: IIndexPattern; + queries: Query[]; + filters: Filter[]; +}) => { + try { + return JSON.stringify( + esQuery.buildEsQuery( + indexPattern, + queries, + filters.filter(f => f.meta.disabled === false), + { + ...config, + dateFormatTZ: undefined, + } + ) + ); + } catch (exp) { + return ''; + } +}; diff --git a/x-pack/plugins/siem/public/lib/kibana/__mocks__/index.ts b/x-pack/plugins/siem/public/common/lib/kibana/__mocks__/index.ts similarity index 100% rename from x-pack/plugins/siem/public/lib/kibana/__mocks__/index.ts rename to x-pack/plugins/siem/public/common/lib/kibana/__mocks__/index.ts diff --git a/x-pack/plugins/siem/public/lib/kibana/hooks.ts b/x-pack/plugins/siem/public/common/lib/kibana/hooks.ts similarity index 95% rename from x-pack/plugins/siem/public/lib/kibana/hooks.ts rename to x-pack/plugins/siem/public/common/lib/kibana/hooks.ts index d62701fe5944ab..ebdefa66b0ef36 100644 --- a/x-pack/plugins/siem/public/lib/kibana/hooks.ts +++ b/x-pack/plugins/siem/public/common/lib/kibana/hooks.ts @@ -8,11 +8,11 @@ import moment from 'moment-timezone'; import { useCallback, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ } from '../../../common/constants'; +import { DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ } from '../../../../common/constants'; import { useUiSetting, useKibana } from './kibana_react'; import { errorToToaster, useStateToaster } from '../../components/toasters'; -import { AuthenticatedUser } from '../../../../security/common/model'; -import { convertToCamelCase } from '../../containers/case/utils'; +import { AuthenticatedUser } from '../../../../../security/common/model'; +import { convertToCamelCase } from '../../../cases/containers/utils'; export const useDateFormat = (): string => useUiSetting(DEFAULT_DATE_FORMAT); diff --git a/x-pack/plugins/siem/public/lib/kibana/index.ts b/x-pack/plugins/siem/public/common/lib/kibana/index.ts similarity index 100% rename from x-pack/plugins/siem/public/lib/kibana/index.ts rename to x-pack/plugins/siem/public/common/lib/kibana/index.ts diff --git a/x-pack/plugins/siem/public/common/lib/kibana/kibana_react.ts b/x-pack/plugins/siem/public/common/lib/kibana/kibana_react.ts new file mode 100644 index 00000000000000..42738c6bbe7d8f --- /dev/null +++ b/x-pack/plugins/siem/public/common/lib/kibana/kibana_react.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 { + KibanaContextProvider, + KibanaReactContextValue, + useKibana, + useUiSetting, + useUiSetting$, + withKibana, +} from '../../../../../../../src/plugins/kibana_react/public'; +import { StartServices } from '../../../plugin'; + +export type KibanaContext = KibanaReactContextValue; +export interface WithKibanaProps { + kibana: KibanaContext; +} + +// eslint-disable-next-line react-hooks/rules-of-hooks +const typedUseKibana = () => useKibana(); + +export { + KibanaContextProvider, + typedUseKibana as useKibana, + useUiSetting, + useUiSetting$, + withKibana, +}; diff --git a/x-pack/plugins/siem/public/lib/kibana/services.ts b/x-pack/plugins/siem/public/common/lib/kibana/services.ts similarity index 89% rename from x-pack/plugins/siem/public/lib/kibana/services.ts rename to x-pack/plugins/siem/public/common/lib/kibana/services.ts index 4ab3e102f56ab5..8a8138691ba173 100644 --- a/x-pack/plugins/siem/public/lib/kibana/services.ts +++ b/x-pack/plugins/siem/public/common/lib/kibana/services.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CoreStart } from '../../../../../../src/core/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { CoreStart } from '../../../../../../../src/core/public'; type GlobalServices = Pick; diff --git a/x-pack/plugins/siem/public/lib/lib.ts b/x-pack/plugins/siem/public/common/lib/lib.ts similarity index 100% rename from x-pack/plugins/siem/public/lib/lib.ts rename to x-pack/plugins/siem/public/common/lib/lib.ts diff --git a/x-pack/plugins/siem/public/lib/note/index.ts b/x-pack/plugins/siem/public/common/lib/note/index.ts similarity index 100% rename from x-pack/plugins/siem/public/lib/note/index.ts rename to x-pack/plugins/siem/public/common/lib/note/index.ts diff --git a/x-pack/plugins/siem/public/common/lib/telemetry/index.ts b/x-pack/plugins/siem/public/common/lib/telemetry/index.ts new file mode 100644 index 00000000000000..0ed524c2ae5483 --- /dev/null +++ b/x-pack/plugins/siem/public/common/lib/telemetry/index.ts @@ -0,0 +1,54 @@ +/* + * 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 { METRIC_TYPE, UiStatsMetricType } from '@kbn/analytics'; + +import { SetupPlugins } from '../../../plugin'; +export { telemetryMiddleware } from './middleware'; + +export { METRIC_TYPE }; + +type TrackFn = (type: UiStatsMetricType, event: string | string[], count?: number) => void; + +const noop = () => {}; + +let _track: TrackFn; + +export const track: TrackFn = (type, event, count) => { + try { + _track(type, event, count); + } catch (error) { + // ignore failed tracking call + } +}; + +export const initTelemetry = (usageCollection: SetupPlugins['usageCollection'], appId: string) => { + _track = usageCollection?.reportUiStats?.bind(null, appId) ?? noop; +}; + +export enum TELEMETRY_EVENT { + // Detections + SIEM_RULE_ENABLED = 'siem_rule_enabled', + SIEM_RULE_DISABLED = 'siem_rule_disabled', + CUSTOM_RULE_ENABLED = 'custom_rule_enabled', + CUSTOM_RULE_DISABLED = 'custom_rule_disabled', + + // ML + SIEM_JOB_ENABLED = 'siem_job_enabled', + SIEM_JOB_DISABLED = 'siem_job_disabled', + CUSTOM_JOB_ENABLED = 'custom_job_enabled', + CUSTOM_JOB_DISABLED = 'custom_job_disabled', + JOB_ENABLE_FAILURE = 'job_enable_failure', + JOB_DISABLE_FAILURE = 'job_disable_failure', + + // Timeline + TIMELINE_OPENED = 'open_timeline', + TIMELINE_SAVED = 'timeline_saved', + TIMELINE_NAMED = 'timeline_named', + + // UI Interactions + TAB_CLICKED = 'tab_', +} diff --git a/x-pack/plugins/siem/public/lib/telemetry/middleware.ts b/x-pack/plugins/siem/public/common/lib/telemetry/middleware.ts similarity index 91% rename from x-pack/plugins/siem/public/lib/telemetry/middleware.ts rename to x-pack/plugins/siem/public/common/lib/telemetry/middleware.ts index ca889e20e695f3..87acdddf87ed7e 100644 --- a/x-pack/plugins/siem/public/lib/telemetry/middleware.ts +++ b/x-pack/plugins/siem/public/common/lib/telemetry/middleware.ts @@ -7,7 +7,7 @@ import { Action, Dispatch, MiddlewareAPI } from 'redux'; import { track, METRIC_TYPE, TELEMETRY_EVENT } from './'; -import * as timelineActions from '../../store/timeline/actions'; +import * as timelineActions from '../../../timelines/store/timeline/actions'; export const telemetryMiddleware = (api: MiddlewareAPI) => (next: Dispatch) => (action: Action) => { if (timelineActions.endTimelineSaving.match(action)) { diff --git a/x-pack/plugins/siem/public/lib/theme/use_eui_theme.tsx b/x-pack/plugins/siem/public/common/lib/theme/use_eui_theme.tsx similarity index 89% rename from x-pack/plugins/siem/public/lib/theme/use_eui_theme.tsx rename to x-pack/plugins/siem/public/common/lib/theme/use_eui_theme.tsx index 1696001203bc87..23dae0d019f300 100644 --- a/x-pack/plugins/siem/public/lib/theme/use_eui_theme.tsx +++ b/x-pack/plugins/siem/public/common/lib/theme/use_eui_theme.tsx @@ -7,7 +7,7 @@ import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; -import { DEFAULT_DARK_MODE } from '../../../common/constants'; +import { DEFAULT_DARK_MODE } from '../../../../common/constants'; import { useUiSetting$ } from '../kibana'; export const useEuiTheme = () => { diff --git a/x-pack/plugins/siem/public/mock/global_state.ts b/x-pack/plugins/siem/public/common/mock/global_state.ts similarity index 95% rename from x-pack/plugins/siem/public/mock/global_state.ts rename to x-pack/plugins/siem/public/common/mock/global_state.ts index d0223b7834db00..e215aa7403ec9e 100644 --- a/x-pack/plugins/siem/public/mock/global_state.ts +++ b/x-pack/plugins/siem/public/common/mock/global_state.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DEFAULT_TIMELINE_WIDTH } from '../components/timeline/body/constants'; +import { DEFAULT_TIMELINE_WIDTH } from '../../timelines/components/timeline/body/constants'; import { Direction, FlowTarget, @@ -13,8 +13,8 @@ import { NetworkTopTablesFields, TlsFields, UsersFields, -} from '../graphql/types'; -import { networkModel, State } from '../store'; +} from '../../graphql/types'; +import { State } from '../store'; import { defaultHeaders } from './header'; import { @@ -22,8 +22,9 @@ import { DEFAULT_TO, DEFAULT_INTERVAL_TYPE, DEFAULT_INTERVAL_VALUE, -} from '../../common/constants'; -import { TimelineType } from '../../common/types/timeline'; +} from '../../../common/constants'; +import { networkModel } from '../../network/store'; +import { TimelineType } from '../../../common/types/timeline'; export const mockGlobalState: State = { app: { diff --git a/x-pack/plugins/siem/public/mock/header.ts b/x-pack/plugins/siem/public/common/mock/header.ts similarity index 94% rename from x-pack/plugins/siem/public/mock/header.ts rename to x-pack/plugins/siem/public/common/mock/header.ts index 61af5a5f098b5d..51636e1efb254a 100644 --- a/x-pack/plugins/siem/public/mock/header.ts +++ b/x-pack/plugins/siem/public/common/mock/header.ts @@ -3,12 +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 { ColumnHeaderOptions } from '../store/timeline/model'; -import { defaultColumnHeaderType } from '../components/timeline/body/column_headers/default_headers'; +import { ColumnHeaderOptions } from '../../timelines/store/timeline/model'; +import { defaultColumnHeaderType } from '../../timelines/components/timeline/body/column_headers/default_headers'; import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH, -} from '../components/timeline/body/constants'; +} from '../../timelines/components/timeline/body/constants'; export const defaultHeaders: ColumnHeaderOptions[] = [ { diff --git a/x-pack/plugins/siem/public/mock/hook_wrapper.tsx b/x-pack/plugins/siem/public/common/mock/hook_wrapper.tsx similarity index 100% rename from x-pack/plugins/siem/public/mock/hook_wrapper.tsx rename to x-pack/plugins/siem/public/common/mock/hook_wrapper.tsx diff --git a/x-pack/plugins/siem/public/mock/index.ts b/x-pack/plugins/siem/public/common/mock/index.ts similarity index 100% rename from x-pack/plugins/siem/public/mock/index.ts rename to x-pack/plugins/siem/public/common/mock/index.ts diff --git a/x-pack/plugins/siem/public/mock/index_pattern.ts b/x-pack/plugins/siem/public/common/mock/index_pattern.ts similarity index 100% rename from x-pack/plugins/siem/public/mock/index_pattern.ts rename to x-pack/plugins/siem/public/common/mock/index_pattern.ts diff --git a/x-pack/plugins/siem/public/common/mock/kibana_core.ts b/x-pack/plugins/siem/public/common/mock/kibana_core.ts new file mode 100644 index 00000000000000..e82c37e3a5b663 --- /dev/null +++ b/x-pack/plugins/siem/public/common/mock/kibana_core.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { coreMock } from '../../../../../../src/core/public/mocks'; +import { dataPluginMock } from '../../../../../../src/plugins/data/public/mocks'; + +export const createKibanaCoreStartMock = () => coreMock.createStart(); +export const createKibanaPluginsStartMock = () => ({ + data: dataPluginMock.createStartContract(), +}); diff --git a/x-pack/plugins/siem/public/common/mock/kibana_react.ts b/x-pack/plugins/siem/public/common/mock/kibana_react.ts new file mode 100644 index 00000000000000..0c51d39257a979 --- /dev/null +++ b/x-pack/plugins/siem/public/common/mock/kibana_react.ts @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable react/display-name */ + +import React from 'react'; +import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; + +import { + DEFAULT_SIEM_TIME_RANGE, + DEFAULT_SIEM_REFRESH_INTERVAL, + DEFAULT_INDEX_KEY, + DEFAULT_DATE_FORMAT, + DEFAULT_DATE_FORMAT_TZ, + DEFAULT_DARK_MODE, + DEFAULT_TIME_RANGE, + DEFAULT_REFRESH_RATE_INTERVAL, + DEFAULT_FROM, + DEFAULT_TO, + DEFAULT_INTERVAL_PAUSE, + DEFAULT_INTERVAL_VALUE, + DEFAULT_BYTES_FORMAT, + DEFAULT_INDEX_PATTERN, +} from '../../../common/constants'; +import { createKibanaCoreStartMock, createKibanaPluginsStartMock } from './kibana_core'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const mockUiSettings: Record = { + [DEFAULT_TIME_RANGE]: { from: 'now-15m', to: 'now', mode: 'quick' }, + [DEFAULT_REFRESH_RATE_INTERVAL]: { pause: false, value: 0 }, + [DEFAULT_SIEM_TIME_RANGE]: { + from: DEFAULT_FROM, + to: DEFAULT_TO, + }, + [DEFAULT_SIEM_REFRESH_INTERVAL]: { + pause: DEFAULT_INTERVAL_PAUSE, + value: DEFAULT_INTERVAL_VALUE, + }, + [DEFAULT_INDEX_KEY]: DEFAULT_INDEX_PATTERN, + [DEFAULT_BYTES_FORMAT]: '0,0.[0]b', + [DEFAULT_DATE_FORMAT_TZ]: 'UTC', + [DEFAULT_DATE_FORMAT]: 'MMM D, YYYY @ HH:mm:ss.SSS', + [DEFAULT_DARK_MODE]: false, +}; + +export const createUseUiSettingMock = () => ( + key: string, + defaultValue?: T +): T => { + const result = mockUiSettings[key]; + + if (typeof result != null) return result; + + if (defaultValue != null) { + return defaultValue; + } + + throw new Error(`Unexpected config key: ${key}`); +}; + +export const createUseUiSetting$Mock = () => { + const useUiSettingMock = createUseUiSettingMock(); + + return ( + key: string, + defaultValue?: T + ): [T, () => void] | undefined => [useUiSettingMock(key, defaultValue), jest.fn()]; +}; + +export const createUseKibanaMock = () => { + const core = createKibanaCoreStartMock(); + const plugins = createKibanaPluginsStartMock(); + const useUiSetting = createUseUiSettingMock(); + + const services = { + ...core, + ...plugins, + uiSettings: { + ...core.uiSettings, + get: useUiSetting, + }, + }; + + return () => ({ services }); +}; + +export const createWithKibanaMock = () => { + const kibana = createUseKibanaMock()(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (Component: any) => (props: any) => { + return React.createElement(Component, { ...props, kibana }); + }; +}; + +export const createKibanaContextProviderMock = () => { + const kibana = createUseKibanaMock()(); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return ({ services, ...rest }: any) => + React.createElement(KibanaContextProvider, { + ...rest, + services: { ...kibana.services, ...services }, + }); +}; diff --git a/x-pack/plugins/siem/public/mock/match_media.ts b/x-pack/plugins/siem/public/common/mock/match_media.ts similarity index 100% rename from x-pack/plugins/siem/public/mock/match_media.ts rename to x-pack/plugins/siem/public/common/mock/match_media.ts diff --git a/x-pack/plugins/siem/public/mock/mock_detail_item.ts b/x-pack/plugins/siem/public/common/mock/mock_detail_item.ts similarity index 98% rename from x-pack/plugins/siem/public/mock/mock_detail_item.ts rename to x-pack/plugins/siem/public/common/mock/mock_detail_item.ts index c25428649d5630..2395010a0ba2e6 100644 --- a/x-pack/plugins/siem/public/mock/mock_detail_item.ts +++ b/x-pack/plugins/siem/public/common/mock/mock_detail_item.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DetailItem } from '../graphql/types'; +import { DetailItem } from '../../graphql/types'; export const mockDetailItemDataId = 'Y-6TfmcB0WOhS6qyMv3s'; diff --git a/x-pack/plugins/siem/public/mock/mock_ecs.ts b/x-pack/plugins/siem/public/common/mock/mock_ecs.ts similarity index 99% rename from x-pack/plugins/siem/public/mock/mock_ecs.ts rename to x-pack/plugins/siem/public/common/mock/mock_ecs.ts index 59e26039e6bffc..7fbbabb29da1b3 100644 --- a/x-pack/plugins/siem/public/mock/mock_ecs.ts +++ b/x-pack/plugins/siem/public/common/mock/mock_ecs.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Ecs } from '../graphql/types'; +import { Ecs } from '../../graphql/types'; export const mockEcsData: Ecs[] = [ { diff --git a/x-pack/plugins/siem/public/mock/mock_endgame_ecs_data.ts b/x-pack/plugins/siem/public/common/mock/mock_endgame_ecs_data.ts similarity index 99% rename from x-pack/plugins/siem/public/mock/mock_endgame_ecs_data.ts rename to x-pack/plugins/siem/public/common/mock/mock_endgame_ecs_data.ts index e6eee3d1c1cb1f..9b2cd14499db43 100644 --- a/x-pack/plugins/siem/public/mock/mock_endgame_ecs_data.ts +++ b/x-pack/plugins/siem/public/common/mock/mock_endgame_ecs_data.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Ecs } from '../graphql/types'; +import { Ecs } from '../../graphql/types'; export const mockEndgameDnsRequest: Ecs = { _id: 'S8jPcG0BOpWiDweSou3g', diff --git a/x-pack/plugins/siem/public/mock/mock_timeline_data.ts b/x-pack/plugins/siem/public/common/mock/mock_timeline_data.ts similarity index 99% rename from x-pack/plugins/siem/public/mock/mock_timeline_data.ts rename to x-pack/plugins/siem/public/common/mock/mock_timeline_data.ts index b300053d5f227d..7503062300d2d8 100644 --- a/x-pack/plugins/siem/public/mock/mock_timeline_data.ts +++ b/x-pack/plugins/siem/public/common/mock/mock_timeline_data.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Ecs, TimelineItem } from '../graphql/types'; +import { Ecs, TimelineItem } from '../../graphql/types'; export const mockTimelineData: TimelineItem[] = [ { diff --git a/x-pack/plugins/siem/public/mock/netflow.ts b/x-pack/plugins/siem/public/common/mock/netflow.ts similarity index 92% rename from x-pack/plugins/siem/public/mock/netflow.ts rename to x-pack/plugins/siem/public/common/mock/netflow.ts index 333188cca4b7e9..4dad794533374a 100644 --- a/x-pack/plugins/siem/public/mock/netflow.ts +++ b/x-pack/plugins/siem/public/common/mock/netflow.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ONE_MILLISECOND_AS_NANOSECONDS } from '../components/formatted_duration/helpers'; -import { Ecs } from '../graphql/types'; +import { ONE_MILLISECOND_AS_NANOSECONDS } from '../../timelines/components/formatted_duration/helpers'; +import { Ecs } from '../../graphql/types'; /** Returns mock data for testing the Netflow component */ export const getMockNetflowData = (): Ecs => ({ diff --git a/x-pack/plugins/siem/public/mock/news.ts b/x-pack/plugins/siem/public/common/mock/news.ts similarity index 100% rename from x-pack/plugins/siem/public/mock/news.ts rename to x-pack/plugins/siem/public/common/mock/news.ts diff --git a/x-pack/plugins/siem/public/mock/raw_news.ts b/x-pack/plugins/siem/public/common/mock/raw_news.ts similarity index 100% rename from x-pack/plugins/siem/public/mock/raw_news.ts rename to x-pack/plugins/siem/public/common/mock/raw_news.ts diff --git a/x-pack/plugins/siem/public/mock/test_providers.tsx b/x-pack/plugins/siem/public/common/mock/test_providers.tsx similarity index 92% rename from x-pack/plugins/siem/public/mock/test_providers.tsx rename to x-pack/plugins/siem/public/common/mock/test_providers.tsx index 59e3874c6d0a1b..679e0bdc14cd5a 100644 --- a/x-pack/plugins/siem/public/mock/test_providers.tsx +++ b/x-pack/plugins/siem/public/common/mock/test_providers.tsx @@ -20,7 +20,8 @@ import { ThemeProvider } from 'styled-components'; import { createStore, State } from '../store'; import { mockGlobalState } from './global_state'; import { createKibanaContextProviderMock } from './kibana_react'; -import { FieldHook, useForm } from '../shared_imports'; +import { FieldHook, useForm } from '../../shared_imports'; +import { SUB_PLUGINS_REDUCER } from './utils'; const state: State = mockGlobalState; @@ -62,7 +63,7 @@ const MockKibanaContextProvider = createKibanaContextProviderMock(); /** A utility for wrapping children in the providers required to run most tests */ const TestProvidersComponent: React.FC = ({ children, - store = createStore(state, apolloClientObservable), + store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable), onDragEnd = jest.fn(), }) => ( @@ -82,7 +83,7 @@ export const TestProviders = React.memo(TestProvidersComponent); const TestProviderWithoutDragAndDropComponent: React.FC = ({ children, - store = createStore(state, apolloClientObservable), + store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable), }) => ( {children} diff --git a/x-pack/plugins/siem/public/mock/timeline_results.ts b/x-pack/plugins/siem/public/common/mock/timeline_results.ts similarity index 99% rename from x-pack/plugins/siem/public/mock/timeline_results.ts rename to x-pack/plugins/siem/public/common/mock/timeline_results.ts index 1af0f533a7ca95..b1a9b65874edc0 100644 --- a/x-pack/plugins/siem/public/mock/timeline_results.ts +++ b/x-pack/plugins/siem/public/common/mock/timeline_results.ts @@ -3,16 +3,16 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { FilterStateStore } from '../../../../../src/plugins/data/common/es_query/filters/meta_filter'; +import { FilterStateStore } from '../../../../../../src/plugins/data/common/es_query/filters/meta_filter'; -import { TimelineType } from '../../common/types/timeline'; +import { TimelineType } from '../../../common/types/timeline'; -import { OpenTimelineResult } from '../components/open_timeline/types'; -import { GetAllTimeline, SortFieldTimeline, TimelineResult, Direction } from '../graphql/types'; -import { allTimelinesQuery } from '../containers/timeline/all/index.gql_query'; -import { CreateTimelineProps } from '../pages/detection_engine/components/signals/types'; -import { TimelineModel } from '../store/timeline/model'; -import { timelineDefaults } from '../store/timeline/defaults'; +import { OpenTimelineResult } from '../../timelines/components/open_timeline/types'; +import { GetAllTimeline, SortFieldTimeline, TimelineResult, Direction } from '../../graphql/types'; +import { allTimelinesQuery } from '../../timelines/containers/all/index.gql_query'; +import { CreateTimelineProps } from '../../alerts/components/signals/types'; +import { TimelineModel } from '../../timelines/store/timeline/model'; +import { timelineDefaults } from '../../timelines/store/timeline/defaults'; export interface MockedProvidedQuery { request: { query: GetAllTimeline.Query; diff --git a/x-pack/plugins/siem/public/common/mock/utils.ts b/x-pack/plugins/siem/public/common/mock/utils.ts new file mode 100644 index 00000000000000..2b54bf83c0a9b7 --- /dev/null +++ b/x-pack/plugins/siem/public/common/mock/utils.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 { hostsReducer } from '../../hosts/store'; +import { networkReducer } from '../../network/store'; +import { timelineReducer } from '../../timelines/store/timeline/reducer'; + +interface Global extends NodeJS.Global { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + window?: any; +} + +export const globalNode: Global = global; + +export const SUB_PLUGINS_REDUCER = { + hosts: hostsReducer, + network: networkReducer, + timeline: timelineReducer, +}; diff --git a/x-pack/plugins/siem/public/common/store/actions.ts b/x-pack/plugins/siem/public/common/store/actions.ts new file mode 100644 index 00000000000000..8a6c292c4893a2 --- /dev/null +++ b/x-pack/plugins/siem/public/common/store/actions.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { appActions } from './app'; +export { dragAndDropActions } from './drag_and_drop'; +export { inputsActions } from './inputs'; diff --git a/x-pack/plugins/siem/public/store/app/actions.ts b/x-pack/plugins/siem/public/common/store/app/actions.ts similarity index 100% rename from x-pack/plugins/siem/public/store/app/actions.ts rename to x-pack/plugins/siem/public/common/store/app/actions.ts diff --git a/x-pack/plugins/siem/public/store/app/index.ts b/x-pack/plugins/siem/public/common/store/app/index.ts similarity index 100% rename from x-pack/plugins/siem/public/store/app/index.ts rename to x-pack/plugins/siem/public/common/store/app/index.ts diff --git a/x-pack/plugins/siem/public/store/app/model.ts b/x-pack/plugins/siem/public/common/store/app/model.ts similarity index 100% rename from x-pack/plugins/siem/public/store/app/model.ts rename to x-pack/plugins/siem/public/common/store/app/model.ts diff --git a/x-pack/plugins/siem/public/store/app/reducer.ts b/x-pack/plugins/siem/public/common/store/app/reducer.ts similarity index 100% rename from x-pack/plugins/siem/public/store/app/reducer.ts rename to x-pack/plugins/siem/public/common/store/app/reducer.ts diff --git a/x-pack/plugins/siem/public/store/app/selectors.ts b/x-pack/plugins/siem/public/common/store/app/selectors.ts similarity index 100% rename from x-pack/plugins/siem/public/store/app/selectors.ts rename to x-pack/plugins/siem/public/common/store/app/selectors.ts diff --git a/x-pack/plugins/siem/public/store/constants.ts b/x-pack/plugins/siem/public/common/store/constants.ts similarity index 100% rename from x-pack/plugins/siem/public/store/constants.ts rename to x-pack/plugins/siem/public/common/store/constants.ts diff --git a/x-pack/plugins/siem/public/common/store/drag_and_drop/actions.ts b/x-pack/plugins/siem/public/common/store/drag_and_drop/actions.ts new file mode 100644 index 00000000000000..82b544641adcb8 --- /dev/null +++ b/x-pack/plugins/siem/public/common/store/drag_and_drop/actions.ts @@ -0,0 +1,17 @@ +/* + * 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 actionCreatorFactory from 'typescript-fsa'; + +import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider'; + +const actionCreator = actionCreatorFactory('x-pack/siem/local/drag_and_drop'); + +export const registerProvider = actionCreator<{ provider: DataProvider }>('REGISTER_PROVIDER'); + +export const unRegisterProvider = actionCreator<{ id: string }>('UNREGISTER_PROVIDER'); + +export const noProviderFound = actionCreator<{ id: string }>('NO_PROVIDER_FOUND'); diff --git a/x-pack/plugins/siem/public/store/drag_and_drop/index.ts b/x-pack/plugins/siem/public/common/store/drag_and_drop/index.ts similarity index 100% rename from x-pack/plugins/siem/public/store/drag_and_drop/index.ts rename to x-pack/plugins/siem/public/common/store/drag_and_drop/index.ts diff --git a/x-pack/plugins/siem/public/common/store/drag_and_drop/model.ts b/x-pack/plugins/siem/public/common/store/drag_and_drop/model.ts new file mode 100644 index 00000000000000..e62bf05c042f8f --- /dev/null +++ b/x-pack/plugins/siem/public/common/store/drag_and_drop/model.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider'; + +export interface IdToDataProvider { + [id: string]: DataProvider; +} + +export interface DragAndDropModel { + dataProviders: IdToDataProvider; +} diff --git a/x-pack/plugins/siem/public/common/store/drag_and_drop/reducer.test.ts b/x-pack/plugins/siem/public/common/store/drag_and_drop/reducer.test.ts new file mode 100644 index 00000000000000..d89f7beb208d52 --- /dev/null +++ b/x-pack/plugins/siem/public/common/store/drag_and_drop/reducer.test.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider'; +import { mockDataProviders } from '../../../timelines/components/timeline/data_providers/mock/mock_data_providers'; + +import { IdToDataProvider } from './model'; +import { registerProviderHandler, unRegisterProviderHandler } from './reducer'; + +const dataProviders: IdToDataProvider = mockDataProviders.reduce( + (acc, provider) => ({ + ...acc, + [provider.id]: provider, + }), + {} +); + +describe('reducer', () => { + describe('#registerProviderHandler', () => { + test('it registers the data provider', () => { + const provider: DataProvider = { + ...mockDataProviders[0], + id: 'abcd', + name: 'Provider abcd', + }; + + expect(registerProviderHandler({ provider, dataProviders })).toEqual({ + ...dataProviders, + [provider.id]: provider, + }); + }); + }); + + describe('#unRegisterProviderHandler', () => { + test('it un-registers the data provider', () => { + const id = mockDataProviders[0].id; + + const expected = unRegisterProviderHandler({ id, dataProviders }); + + expect(Object.keys(expected)).not.toContain(id); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/common/store/drag_and_drop/reducer.ts b/x-pack/plugins/siem/public/common/store/drag_and_drop/reducer.ts new file mode 100644 index 00000000000000..d402da136a5961 --- /dev/null +++ b/x-pack/plugins/siem/public/common/store/drag_and_drop/reducer.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { omit } from 'lodash/fp'; +import { reducerWithInitialState } from 'typescript-fsa-reducers'; + +import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider'; + +import { registerProvider, unRegisterProvider } from './actions'; +import { DragAndDropModel, IdToDataProvider } from './model'; + +export type DragAndDropState = DragAndDropModel; + +export const initialDragAndDropState: DragAndDropState = { dataProviders: {} }; + +interface RegisterProviderHandlerParams { + provider: DataProvider; + dataProviders: IdToDataProvider; +} + +export const registerProviderHandler = ({ + provider, + dataProviders, +}: RegisterProviderHandlerParams): IdToDataProvider => ({ + ...dataProviders, + [provider.id]: provider, +}); + +interface UnRegisterProviderHandlerParams { + id: string; + dataProviders: IdToDataProvider; +} + +export const unRegisterProviderHandler = ({ + id, + dataProviders, +}: UnRegisterProviderHandlerParams): IdToDataProvider => omit(id, dataProviders); + +export const dragAndDropReducer = reducerWithInitialState(initialDragAndDropState) + .case(registerProvider, (state, { provider }) => ({ + ...state, + dataProviders: registerProviderHandler({ provider, dataProviders: state.dataProviders }), + })) + .case(unRegisterProvider, (state, { id }) => ({ + ...state, + dataProviders: unRegisterProviderHandler({ id, dataProviders: state.dataProviders }), + })) + .build(); diff --git a/x-pack/plugins/siem/public/store/drag_and_drop/selectors.ts b/x-pack/plugins/siem/public/common/store/drag_and_drop/selectors.ts similarity index 100% rename from x-pack/plugins/siem/public/store/drag_and_drop/selectors.ts rename to x-pack/plugins/siem/public/common/store/drag_and_drop/selectors.ts diff --git a/x-pack/plugins/siem/public/common/store/epic.ts b/x-pack/plugins/siem/public/common/store/epic.ts new file mode 100644 index 00000000000000..b9e8e7d88c202a --- /dev/null +++ b/x-pack/plugins/siem/public/common/store/epic.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { combineEpics } from 'redux-observable'; +import { createTimelineEpic } from '../../timelines/store/timeline/epic'; +import { createTimelineFavoriteEpic } from '../../timelines/store/timeline/epic_favorite'; +import { createTimelineNoteEpic } from '../../timelines/store/timeline/epic_note'; +import { createTimelinePinnedEventEpic } from '../../timelines/store/timeline/epic_pinned_event'; + +export const createRootEpic = () => + combineEpics( + createTimelineEpic(), + createTimelineFavoriteEpic(), + createTimelineNoteEpic(), + createTimelinePinnedEventEpic() + ); diff --git a/x-pack/plugins/siem/public/store/index.ts b/x-pack/plugins/siem/public/common/store/index.ts similarity index 100% rename from x-pack/plugins/siem/public/store/index.ts rename to x-pack/plugins/siem/public/common/store/index.ts diff --git a/x-pack/plugins/siem/public/common/store/inputs/actions.ts b/x-pack/plugins/siem/public/common/store/inputs/actions.ts new file mode 100644 index 00000000000000..5b26957843f082 --- /dev/null +++ b/x-pack/plugins/siem/public/common/store/inputs/actions.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 actionCreatorFactory from 'typescript-fsa'; + +import { InspectQuery, Refetch, RefetchKql } from './model'; +import { InputsModelId } from './constants'; +import { Filter, SavedQuery } from '../../../../../../../src/plugins/data/public'; + +const actionCreator = actionCreatorFactory('x-pack/siem/local/inputs'); + +export const setAbsoluteRangeDatePicker = actionCreator<{ + id: InputsModelId; + from: number; + to: number; +}>('SET_ABSOLUTE_RANGE_DATE_PICKER'); + +export const setTimelineRangeDatePicker = actionCreator<{ + from: number; + to: number; +}>('SET_TIMELINE_RANGE_DATE_PICKER'); + +export const setRelativeRangeDatePicker = actionCreator<{ + id: InputsModelId; + fromStr: string; + toStr: string; + from: number; + to: number; +}>('SET_RELATIVE_RANGE_DATE_PICKER'); + +export const setDuration = actionCreator<{ id: InputsModelId; duration: number }>('SET_DURATION'); + +export const startAutoReload = actionCreator<{ id: InputsModelId }>('START_KQL_AUTO_RELOAD'); + +export const stopAutoReload = actionCreator<{ id: InputsModelId }>('STOP_KQL_AUTO_RELOAD'); + +export const setQuery = actionCreator<{ + inputId: InputsModelId; + id: string; + loading: boolean; + refetch: Refetch | RefetchKql; + inspect: InspectQuery | null; +}>('SET_QUERY'); + +export const deleteOneQuery = actionCreator<{ + inputId: InputsModelId; + id: string; +}>('DELETE_QUERY'); + +export const setInspectionParameter = actionCreator<{ + id: string; + inputId: InputsModelId; + isInspected: boolean; + selectedInspectIndex: number; +}>('SET_INSPECTION_PARAMETER'); + +export const deleteAllQuery = actionCreator<{ id: InputsModelId }>('DELETE_ALL_QUERY'); + +export const toggleTimelineLinkTo = actionCreator<{ linkToId: InputsModelId }>( + 'TOGGLE_TIMELINE_LINK_TO' +); + +export const removeTimelineLinkTo = actionCreator('REMOVE_TIMELINE_LINK_TO'); +export const addTimelineLinkTo = actionCreator<{ linkToId: InputsModelId }>('ADD_TIMELINE_LINK_TO'); + +export const removeGlobalLinkTo = actionCreator('REMOVE_GLOBAL_LINK_TO'); +export const addGlobalLinkTo = actionCreator<{ linkToId: InputsModelId }>('ADD_GLOBAL_LINK_TO'); + +export const setFilterQuery = actionCreator<{ + id: InputsModelId; + query: string | { [key: string]: unknown }; + language: string; +}>('SET_FILTER_QUERY'); + +export const setSavedQuery = actionCreator<{ + id: InputsModelId; + savedQuery: SavedQuery | undefined; +}>('SET_SAVED_QUERY'); + +export const setSearchBarFilter = actionCreator<{ + id: InputsModelId; + filters: Filter[]; +}>('SET_SEARCH_BAR_FILTER'); diff --git a/x-pack/plugins/siem/public/store/inputs/constants.ts b/x-pack/plugins/siem/public/common/store/inputs/constants.ts similarity index 100% rename from x-pack/plugins/siem/public/store/inputs/constants.ts rename to x-pack/plugins/siem/public/common/store/inputs/constants.ts diff --git a/x-pack/plugins/siem/public/store/inputs/helpers.test.ts b/x-pack/plugins/siem/public/common/store/inputs/helpers.test.ts similarity index 100% rename from x-pack/plugins/siem/public/store/inputs/helpers.test.ts rename to x-pack/plugins/siem/public/common/store/inputs/helpers.test.ts diff --git a/x-pack/plugins/siem/public/store/inputs/helpers.ts b/x-pack/plugins/siem/public/common/store/inputs/helpers.ts similarity index 100% rename from x-pack/plugins/siem/public/store/inputs/helpers.ts rename to x-pack/plugins/siem/public/common/store/inputs/helpers.ts diff --git a/x-pack/plugins/siem/public/store/inputs/index.ts b/x-pack/plugins/siem/public/common/store/inputs/index.ts similarity index 100% rename from x-pack/plugins/siem/public/store/inputs/index.ts rename to x-pack/plugins/siem/public/common/store/inputs/index.ts diff --git a/x-pack/plugins/siem/public/common/store/inputs/model.ts b/x-pack/plugins/siem/public/common/store/inputs/model.ts new file mode 100644 index 00000000000000..e851caf523eb4b --- /dev/null +++ b/x-pack/plugins/siem/public/common/store/inputs/model.ts @@ -0,0 +1,103 @@ +/* + * 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 { Dispatch } from 'redux'; +import { InputsModelId } from './constants'; +import { CONSTANTS } from '../../components/url_state/constants'; +import { Query, Filter, SavedQuery } from '../../../../../../../src/plugins/data/public'; + +export interface AbsoluteTimeRange { + kind: 'absolute'; + fromStr: undefined; + toStr: undefined; + from: number; + to: number; +} + +export interface RelativeTimeRange { + kind: 'relative'; + fromStr: string; + toStr: string; + from: number; + to: number; +} + +export const isRelativeTimeRange = ( + timeRange: RelativeTimeRange | AbsoluteTimeRange | URLTimeRange +): timeRange is RelativeTimeRange => timeRange.kind === 'relative'; + +export const isAbsoluteTimeRange = ( + timeRange: RelativeTimeRange | AbsoluteTimeRange | URLTimeRange +): timeRange is AbsoluteTimeRange => timeRange.kind === 'absolute'; + +export type TimeRange = AbsoluteTimeRange | RelativeTimeRange; + +export type URLTimeRange = Omit & { + from: string | TimeRange['from']; + to: string | TimeRange['to']; +}; + +export interface Policy { + kind: 'manual' | 'interval'; + duration: number; // in ms +} + +interface InspectVariables { + inspect: boolean; +} +export type RefetchWithParams = ({ inspect }: InspectVariables) => void; +export type RefetchKql = (dispatch: Dispatch) => boolean; +export type Refetch = () => void; + +export interface InspectQuery { + dsl: string[]; + response: string[]; +} + +export interface GlobalGenericQuery { + inspect: InspectQuery | null; + isInspected: boolean; + loading: boolean; + selectedInspectIndex: number; +} + +export interface GlobalGraphqlQuery extends GlobalGenericQuery { + id: string; + refetch: null | Refetch | RefetchWithParams; +} +export interface GlobalKqlQuery extends GlobalGenericQuery { + id: 'kql'; + refetch: RefetchKql; +} + +export type GlobalQuery = GlobalGraphqlQuery | GlobalKqlQuery; + +export interface InputsRange { + timerange: TimeRange; + policy: Policy; + queries: GlobalQuery[]; + linkTo: InputsModelId[]; + query: Query; + filters: Filter[]; + savedQuery?: SavedQuery; +} + +export interface LinkTo { + linkTo: InputsModelId[]; +} + +export interface InputsModel { + global: InputsRange; + timeline: InputsRange; +} +export interface UrlInputsModelInputs { + linkTo: InputsModelId[]; + [CONSTANTS.timerange]: TimeRange; +} +export interface UrlInputsModel { + global: UrlInputsModelInputs; + timeline: UrlInputsModelInputs; +} diff --git a/x-pack/plugins/siem/public/store/inputs/reducer.ts b/x-pack/plugins/siem/public/common/store/inputs/reducer.ts similarity index 100% rename from x-pack/plugins/siem/public/store/inputs/reducer.ts rename to x-pack/plugins/siem/public/common/store/inputs/reducer.ts diff --git a/x-pack/plugins/siem/public/store/inputs/selectors.ts b/x-pack/plugins/siem/public/common/store/inputs/selectors.ts similarity index 100% rename from x-pack/plugins/siem/public/store/inputs/selectors.ts rename to x-pack/plugins/siem/public/common/store/inputs/selectors.ts diff --git a/x-pack/plugins/siem/public/common/store/model.ts b/x-pack/plugins/siem/public/common/store/model.ts new file mode 100644 index 00000000000000..0032a95cce321a --- /dev/null +++ b/x-pack/plugins/siem/public/common/store/model.ts @@ -0,0 +1,10 @@ +/* + * 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 { appModel } from './app'; +export { dragAndDropModel } from './drag_and_drop'; +export { inputsModel } from './inputs'; +export * from './types'; diff --git a/x-pack/plugins/siem/public/common/store/reducer.ts b/x-pack/plugins/siem/public/common/store/reducer.ts new file mode 100644 index 00000000000000..da1dcd3ea9e730 --- /dev/null +++ b/x-pack/plugins/siem/public/common/store/reducer.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { combineReducers } from 'redux'; + +import { appReducer, AppState, initialAppState } from './app'; +import { dragAndDropReducer, DragAndDropState, initialDragAndDropState } from './drag_and_drop'; +import { createInitialInputsState, initialInputsState, inputsReducer, InputsState } from './inputs'; + +import { HostsPluginState, HostsPluginReducer } from '../../hosts/store'; +import { NetworkPluginState, NetworkPluginReducer } from '../../network/store'; +import { TimelinePluginState, TimelinePluginReducer } from '../../timelines/store/timeline'; + +export interface State extends HostsPluginState, NetworkPluginState, TimelinePluginState { + app: AppState; + dragAndDrop: DragAndDropState; + inputs: InputsState; +} + +export const initialState: Pick = { + app: initialAppState, + dragAndDrop: initialDragAndDropState, + inputs: initialInputsState, +}; + +type SubPluginsInitState = HostsPluginState & NetworkPluginState & TimelinePluginState; +export type SubPluginsInitReducer = HostsPluginReducer & + NetworkPluginReducer & + TimelinePluginReducer; + +export const createInitialState = (pluginsInitState: SubPluginsInitState): State => ({ + ...initialState, + ...pluginsInitState, + inputs: createInitialInputsState(), +}); + +export const createReducer = (pluginsReducer: SubPluginsInitReducer) => + combineReducers({ + app: appReducer, + dragAndDrop: dragAndDropReducer, + inputs: inputsReducer, + ...pluginsReducer, + }); diff --git a/x-pack/plugins/siem/public/common/store/selectors.ts b/x-pack/plugins/siem/public/common/store/selectors.ts new file mode 100644 index 00000000000000..b938bae39b634b --- /dev/null +++ b/x-pack/plugins/siem/public/common/store/selectors.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { appSelectors } from './app'; +export { dragAndDropSelectors } from './drag_and_drop'; +export { inputsSelectors } from './inputs'; diff --git a/x-pack/plugins/siem/public/store/store.ts b/x-pack/plugins/siem/public/common/store/store.ts similarity index 87% rename from x-pack/plugins/siem/public/store/store.ts rename to x-pack/plugins/siem/public/common/store/store.ts index 2af0f87b4494d9..ea7cb417fb24b2 100644 --- a/x-pack/plugins/siem/public/store/store.ts +++ b/x-pack/plugins/siem/public/common/store/store.ts @@ -9,13 +9,13 @@ import { Action, applyMiddleware, compose, createStore as createReduxStore, Stor import { createEpicMiddleware } from 'redux-observable'; import { Observable } from 'rxjs'; -import { AppApolloClient } from '../lib/lib'; import { telemetryMiddleware } from '../lib/telemetry'; import { appSelectors } from './app'; -import { timelineSelectors } from './timeline'; +import { timelineSelectors } from '../../timelines/store/timeline'; import { inputsSelectors } from './inputs'; -import { State, initialState, reducer } from './reducer'; +import { State, SubPluginsInitReducer, createReducer } from './reducer'; import { createRootEpic } from './epic'; +import { AppApolloClient } from '../lib/lib'; type ComposeType = typeof compose; declare global { @@ -24,8 +24,10 @@ declare global { } } let store: Store | null = null; +export { SubPluginsInitReducer }; export const createStore = ( - state: State = initialState, + state: State, + pluginsReducer: SubPluginsInitReducer, apolloClient: Observable ): Store => { const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; @@ -45,7 +47,7 @@ export const createStore = ( ); store = createReduxStore( - reducer, + createReducer(pluginsReducer), state, composeEnhancers(applyMiddleware(epicMiddleware, telemetryMiddleware)) ); diff --git a/x-pack/plugins/siem/public/store/types.ts b/x-pack/plugins/siem/public/common/store/types.ts similarity index 100% rename from x-pack/plugins/siem/public/store/types.ts rename to x-pack/plugins/siem/public/common/store/types.ts diff --git a/x-pack/plugins/siem/public/pages/common/translations.ts b/x-pack/plugins/siem/public/common/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/common/translations.ts rename to x-pack/plugins/siem/public/common/translations.ts diff --git a/x-pack/plugins/siem/public/utils/api/index.ts b/x-pack/plugins/siem/public/common/utils/api/index.ts similarity index 100% rename from x-pack/plugins/siem/public/utils/api/index.ts rename to x-pack/plugins/siem/public/common/utils/api/index.ts diff --git a/x-pack/plugins/siem/public/utils/apollo_context.ts b/x-pack/plugins/siem/public/common/utils/apollo_context.ts similarity index 100% rename from x-pack/plugins/siem/public/utils/apollo_context.ts rename to x-pack/plugins/siem/public/common/utils/apollo_context.ts diff --git a/x-pack/plugins/siem/public/utils/default_date_settings.test.ts b/x-pack/plugins/siem/public/common/utils/default_date_settings.test.ts similarity index 99% rename from x-pack/plugins/siem/public/utils/default_date_settings.test.ts rename to x-pack/plugins/siem/public/common/utils/default_date_settings.test.ts index 9dc179ba7a6e2d..3ae3ef2326ea2b 100644 --- a/x-pack/plugins/siem/public/utils/default_date_settings.test.ts +++ b/x-pack/plugins/siem/public/common/utils/default_date_settings.test.ts @@ -21,7 +21,7 @@ import { DEFAULT_INTERVAL_PAUSE, DEFAULT_INTERVAL_VALUE, DEFAULT_INTERVAL_TYPE, -} from '../../common/constants'; +} from '../../../common/constants'; import { KibanaServices } from '../lib/kibana'; import { Policy } from '../store/inputs/model'; @@ -30,7 +30,7 @@ import { Policy } from '../store/inputs/model'; // we have to repeat ourselves once const DEFAULT_FROM_DATE = '1983-05-31T13:03:54.234Z'; const DEFAULT_TO_DATE = '1990-05-31T13:03:54.234Z'; -jest.mock('../../common/constants', () => ({ +jest.mock('../../../common/constants', () => ({ DEFAULT_FROM: '1983-05-31T13:03:54.234Z', DEFAULT_TO: '1990-05-31T13:03:54.234Z', DEFAULT_INTERVAL_PAUSE: true, diff --git a/x-pack/plugins/siem/public/utils/default_date_settings.ts b/x-pack/plugins/siem/public/common/utils/default_date_settings.ts similarity index 98% rename from x-pack/plugins/siem/public/utils/default_date_settings.ts rename to x-pack/plugins/siem/public/common/utils/default_date_settings.ts index c4869a4851ae50..3523a02ea44f50 100644 --- a/x-pack/plugins/siem/public/utils/default_date_settings.ts +++ b/x-pack/plugins/siem/public/common/utils/default_date_settings.ts @@ -15,7 +15,7 @@ import { DEFAULT_TO, DEFAULT_INTERVAL_TYPE, DEFAULT_INTERVAL_VALUE, -} from '../../common/constants'; +} from '../../../common/constants'; import { KibanaServices } from '../lib/kibana'; import { Policy } from '../store/inputs/model'; diff --git a/x-pack/plugins/siem/public/utils/kql/use_update_kql.test.tsx b/x-pack/plugins/siem/public/common/utils/kql/use_update_kql.test.tsx similarity index 92% rename from x-pack/plugins/siem/public/utils/kql/use_update_kql.test.tsx rename to x-pack/plugins/siem/public/common/utils/kql/use_update_kql.test.tsx index b70a5432e47f84..9b1a397deb17fb 100644 --- a/x-pack/plugins/siem/public/utils/kql/use_update_kql.test.tsx +++ b/x-pack/plugins/siem/public/common/utils/kql/use_update_kql.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { applyKqlFilterQuery as dispatchApplyTimelineFilterQuery } from '../../store/timeline/actions'; +import { applyKqlFilterQuery as dispatchApplyTimelineFilterQuery } from '../../../timelines/store/timeline/actions'; import { mockIndexPattern } from '../../mock/index_pattern'; import { useUpdateKql } from './use_update_kql'; @@ -14,7 +14,7 @@ mockDispatch.mockImplementation(fn => fn); const applyTimelineKqlMock: jest.Mock = (dispatchApplyTimelineFilterQuery as unknown) as jest.Mock; -jest.mock('../../store/timeline/actions', () => ({ +jest.mock('../../../timelines/store/timeline/actions', () => ({ applyKqlFilterQuery: jest.fn(), })); diff --git a/x-pack/plugins/siem/public/utils/kql/use_update_kql.tsx b/x-pack/plugins/siem/public/common/utils/kql/use_update_kql.tsx similarity index 96% rename from x-pack/plugins/siem/public/utils/kql/use_update_kql.tsx rename to x-pack/plugins/siem/public/common/utils/kql/use_update_kql.tsx index af993588f7e0dc..d1f5b40086ceaa 100644 --- a/x-pack/plugins/siem/public/utils/kql/use_update_kql.tsx +++ b/x-pack/plugins/siem/public/common/utils/kql/use_update_kql.tsx @@ -9,7 +9,7 @@ import { IIndexPattern } from 'src/plugins/data/public'; import deepEqual from 'fast-deep-equal'; import { KueryFilterQuery } from '../../store'; -import { applyKqlFilterQuery as dispatchApplyTimelineFilterQuery } from '../../store/timeline/actions'; +import { applyKqlFilterQuery as dispatchApplyTimelineFilterQuery } from '../../../timelines/store/timeline/actions'; import { convertKueryToElasticSearchQuery } from '../../lib/keury'; import { RefetchKql } from '../../store/inputs/model'; diff --git a/x-pack/plugins/siem/public/utils/logo_endpoint/64_color.svg b/x-pack/plugins/siem/public/common/utils/logo_endpoint/64_color.svg similarity index 100% rename from x-pack/plugins/siem/public/utils/logo_endpoint/64_color.svg rename to x-pack/plugins/siem/public/common/utils/logo_endpoint/64_color.svg diff --git a/x-pack/plugins/siem/public/utils/route/helpers.ts b/x-pack/plugins/siem/public/common/utils/route/helpers.ts similarity index 100% rename from x-pack/plugins/siem/public/utils/route/helpers.ts rename to x-pack/plugins/siem/public/common/utils/route/helpers.ts diff --git a/x-pack/plugins/siem/public/common/utils/route/index.test.tsx b/x-pack/plugins/siem/public/common/utils/route/index.test.tsx new file mode 100644 index 00000000000000..95e40b0f66301a --- /dev/null +++ b/x-pack/plugins/siem/public/common/utils/route/index.test.tsx @@ -0,0 +1,205 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; + +import { HostsTableType } from '../../../hosts/store/model'; +import { RouteSpyState } from './types'; +import { ManageRoutesSpy } from './manage_spy_routes'; +import { SpyRouteComponent } from './spy_routes'; +import { useRouteSpy } from './use_route_spy'; + +type Action = 'PUSH' | 'POP' | 'REPLACE'; +const pop: Action = 'POP'; + +const defaultLocation = { + hash: '', + pathname: '/hosts', + search: '', + state: '', +}; + +export const mockHistory = { + action: pop, + block: jest.fn(), + createHref: jest.fn(), + go: jest.fn(), + goBack: jest.fn(), + goForward: jest.fn(), + length: 2, + listen: jest.fn(), + location: defaultLocation, + push: jest.fn(), + replace: jest.fn(), +}; + +const dispatchMock = jest.fn(); +const mockRoutes: RouteSpyState = { + pageName: '', + detailName: undefined, + tabName: undefined, + search: '', + pathName: '/', + history: mockHistory, +}; + +const mockUseRouteSpy: jest.Mock = useRouteSpy as jest.Mock; +jest.mock('./use_route_spy', () => ({ + useRouteSpy: jest.fn(), +})); + +describe('Spy Routes', () => { + describe('At Initialization of the app', () => { + beforeEach(() => { + dispatchMock.mockReset(); + dispatchMock.mockClear(); + }); + test('Make sure we update search state first', () => { + const pathname = '/'; + mockUseRouteSpy.mockImplementation(() => [mockRoutes, dispatchMock]); + mount( + + + + ); + + expect(dispatchMock.mock.calls[0]).toEqual([ + { + type: 'updateSearch', + search: '?importantQueryString="really"', + }, + ]); + }); + + test('Make sure we update search state first and then update the route but keeping the initial search', () => { + const pathname = '/hosts/allHosts'; + mockUseRouteSpy.mockImplementation(() => [mockRoutes, dispatchMock]); + mount( + + + + ); + + expect(dispatchMock.mock.calls[0]).toEqual([ + { + type: 'updateSearch', + search: '?importantQueryString="really"', + }, + ]); + + expect(dispatchMock.mock.calls[1]).toEqual([ + { + route: { + detailName: undefined, + history: mockHistory, + pageName: 'hosts', + pathName: pathname, + tabName: HostsTableType.hosts, + }, + type: 'updateRouteWithOutSearch', + }, + ]); + }); + }); + + describe('When app is running', () => { + beforeEach(() => { + dispatchMock.mockReset(); + dispatchMock.mockClear(); + }); + test('Update route should be updated when there is changed detected', () => { + const pathname = '/hosts/allHosts'; + const newPathname = `hosts/${HostsTableType.authentications}`; + mockUseRouteSpy.mockImplementation(() => [mockRoutes, dispatchMock]); + const wrapper = mount( + + ); + + dispatchMock.mockReset(); + dispatchMock.mockClear(); + + wrapper.setProps({ + location: { + hash: '', + pathname: newPathname, + search: '?updated="true"', + state: '', + }, + match: { + isExact: false, + path: newPathname, + url: newPathname, + params: { + pageName: 'hosts', + detailName: undefined, + tabName: HostsTableType.authentications, + search: '', + }, + }, + }); + wrapper.update(); + expect(dispatchMock.mock.calls[0]).toEqual([ + { + route: { + detailName: undefined, + history: mockHistory, + pageName: 'hosts', + pathName: newPathname, + tabName: HostsTableType.authentications, + search: '?updated="true"', + }, + type: 'updateRoute', + }, + ]); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/utils/route/manage_spy_routes.tsx b/x-pack/plugins/siem/public/common/utils/route/manage_spy_routes.tsx similarity index 100% rename from x-pack/plugins/siem/public/utils/route/manage_spy_routes.tsx rename to x-pack/plugins/siem/public/common/utils/route/manage_spy_routes.tsx diff --git a/x-pack/plugins/siem/public/utils/route/spy_routes.tsx b/x-pack/plugins/siem/public/common/utils/route/spy_routes.tsx similarity index 100% rename from x-pack/plugins/siem/public/utils/route/spy_routes.tsx rename to x-pack/plugins/siem/public/common/utils/route/spy_routes.tsx diff --git a/x-pack/plugins/siem/public/common/utils/route/types.ts b/x-pack/plugins/siem/public/common/utils/route/types.ts new file mode 100644 index 00000000000000..912da545a66a3c --- /dev/null +++ b/x-pack/plugins/siem/public/common/utils/route/types.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as H from 'history'; +import React from 'react'; +import { RouteComponentProps } from 'react-router-dom'; + +import { TimelineType } from '../../../../common/types/timeline'; + +import { HostsTableType } from '../../../hosts/store/model'; +import { NetworkRouteType } from '../../../network/pages/navigation/types'; +import { FlowTarget } from '../../../graphql/types'; + +export type SiemRouteType = HostsTableType | NetworkRouteType | TimelineType; +export interface RouteSpyState { + pageName: string; + detailName: string | undefined; + tabName: SiemRouteType | undefined; + search: string; + pathName: string; + history?: H.History; + flowTarget?: FlowTarget; + state?: Record; +} + +export interface HostRouteSpyState extends RouteSpyState { + tabName: HostsTableType | undefined; +} + +export interface NetworkRouteSpyState extends RouteSpyState { + tabName: NetworkRouteType | undefined; +} + +export interface TimelineRouteSpyState extends RouteSpyState { + tabName: TimelineType | undefined; +} + +export type RouteSpyAction = + | { + type: 'updateSearch'; + search: string; + } + | { + type: 'updateRouteWithOutSearch'; + route: Pick< + RouteSpyState, + 'pageName' & 'detailName' & 'tabName' & 'pathName' & 'history' & 'state' + >; + } + | { + type: 'updateRoute'; + route: RouteSpyState; + }; + +export interface ManageRoutesSpyProps { + children: React.ReactNode; +} + +export type SpyRouteProps = RouteComponentProps<{ + pageName: string | undefined; + detailName: string | undefined; + tabName: HostsTableType | undefined; + search: string; + flowTarget: FlowTarget | undefined; +}> & { + state?: Record; +}; diff --git a/x-pack/plugins/siem/public/utils/route/use_route_spy.tsx b/x-pack/plugins/siem/public/common/utils/route/use_route_spy.tsx similarity index 100% rename from x-pack/plugins/siem/public/utils/route/use_route_spy.tsx rename to x-pack/plugins/siem/public/common/utils/route/use_route_spy.tsx diff --git a/x-pack/plugins/siem/public/common/utils/saved_query_services/index.tsx b/x-pack/plugins/siem/public/common/utils/saved_query_services/index.tsx new file mode 100644 index 00000000000000..a8ee10ba2b8014 --- /dev/null +++ b/x-pack/plugins/siem/public/common/utils/saved_query_services/index.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useState, useEffect } from 'react'; +import { + SavedQueryService, + createSavedQueryService, +} from '../../../../../../../src/plugins/data/public'; + +import { useKibana } from '../../lib/kibana'; + +export const useSavedQueryServices = () => { + const kibana = useKibana(); + const client = kibana.services.savedObjects.client; + + const [savedQueryService, setSavedQueryService] = useState( + createSavedQueryService(client) + ); + + useEffect(() => { + setSavedQueryService(createSavedQueryService(client)); + }, [client]); + return savedQueryService; +}; diff --git a/x-pack/plugins/siem/public/utils/timeline/use_show_timeline.tsx b/x-pack/plugins/siem/public/common/utils/timeline/use_show_timeline.tsx similarity index 94% rename from x-pack/plugins/siem/public/utils/timeline/use_show_timeline.tsx rename to x-pack/plugins/siem/public/common/utils/timeline/use_show_timeline.tsx index e969330b809ff1..78f22a86c1893f 100644 --- a/x-pack/plugins/siem/public/utils/timeline/use_show_timeline.tsx +++ b/x-pack/plugins/siem/public/common/utils/timeline/use_show_timeline.tsx @@ -7,7 +7,7 @@ import { useLocation } from 'react-router-dom'; import { useState, useEffect } from 'react'; -import { SiemPageName } from '../../pages/home/types'; +import { SiemPageName } from '../../../app/types'; const hideTimelineForRoutes = [`/${SiemPageName.case}/configure`]; diff --git a/x-pack/plugins/siem/public/utils/use_mount_appended.ts b/x-pack/plugins/siem/public/common/utils/use_mount_appended.ts similarity index 100% rename from x-pack/plugins/siem/public/utils/use_mount_appended.ts rename to x-pack/plugins/siem/public/common/utils/use_mount_appended.ts diff --git a/x-pack/plugins/siem/public/utils/validators/index.ts b/x-pack/plugins/siem/public/common/utils/validators/index.ts similarity index 100% rename from x-pack/plugins/siem/public/utils/validators/index.ts rename to x-pack/plugins/siem/public/common/utils/validators/index.ts diff --git a/x-pack/plugins/siem/public/components/alerts_viewer/histogram_configs.ts b/x-pack/plugins/siem/public/components/alerts_viewer/histogram_configs.ts deleted file mode 100644 index fbcf4c6ed039b6..00000000000000 --- a/x-pack/plugins/siem/public/components/alerts_viewer/histogram_configs.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 * as i18n from './translations'; -import { MatrixHistogramOption, MatrixHisrogramConfigs } from '../matrix_histogram/types'; -import { HistogramType } from '../../graphql/types'; - -export const alertsStackByOptions: MatrixHistogramOption[] = [ - { - text: 'event.category', - value: 'event.category', - }, - { - text: 'event.module', - value: 'event.module', - }, -]; - -const DEFAULT_STACK_BY = 'event.module'; - -export const histogramConfigs: MatrixHisrogramConfigs = { - defaultStackByOption: - alertsStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? alertsStackByOptions[1], - errorMessage: i18n.ERROR_FETCHING_ALERTS_DATA, - histogramType: HistogramType.alerts, - stackByOptions: alertsStackByOptions, - subtitle: undefined, - title: i18n.ALERTS_GRAPH_TITLE, -}; diff --git a/x-pack/plugins/siem/public/components/alerts_viewer/index.tsx b/x-pack/plugins/siem/public/components/alerts_viewer/index.tsx deleted file mode 100644 index 957feb6244792a..00000000000000 --- a/x-pack/plugins/siem/public/components/alerts_viewer/index.tsx +++ /dev/null @@ -1,67 +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, { useEffect, useCallback, useMemo } from 'react'; -import numeral from '@elastic/numeral'; - -import { DEFAULT_NUMBER_FORMAT } from '../../../common/constants'; -import { AlertsComponentsQueryProps } from './types'; -import { AlertsTable } from './alerts_table'; -import * as i18n from './translations'; -import { useUiSetting$ } from '../../lib/kibana'; -import { MatrixHistogramContainer } from '../matrix_histogram'; -import { histogramConfigs } from './histogram_configs'; -import { MatrixHisrogramConfigs } from '../matrix_histogram/types'; -const ID = 'alertsOverTimeQuery'; - -export const AlertsView = ({ - deleteQuery, - endDate, - filterQuery, - pageFilters, - setQuery, - startDate, - type, -}: AlertsComponentsQueryProps) => { - const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); - const getSubtitle = useCallback( - (totalCount: number) => - `${i18n.SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${i18n.UNIT( - totalCount - )}`, - [] - ); - const alertsHistogramConfigs: MatrixHisrogramConfigs = useMemo( - () => ({ - ...histogramConfigs, - subtitle: getSubtitle, - }), - [getSubtitle] - ); - useEffect(() => { - return () => { - if (deleteQuery) { - deleteQuery({ id: ID }); - } - }; - }, [deleteQuery]); - - return ( - <> - - - - ); -}; -AlertsView.displayName = 'AlertsView'; diff --git a/x-pack/plugins/siem/public/components/alerts_viewer/types.ts b/x-pack/plugins/siem/public/components/alerts_viewer/types.ts deleted file mode 100644 index 321f7214c8fef9..00000000000000 --- a/x-pack/plugins/siem/public/components/alerts_viewer/types.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 { Filter } from '../../../../../../src/plugins/data/public'; -import { HostsComponentsQueryProps } from '../../pages/hosts/navigation/types'; -import { NetworkComponentQueryProps } from '../../pages/network/navigation/types'; -import { MatrixHistogramOption } from '../matrix_histogram/types'; - -type CommonQueryProps = HostsComponentsQueryProps | NetworkComponentQueryProps; -export interface AlertsComponentsQueryProps - extends Pick< - CommonQueryProps, - 'deleteQuery' | 'endDate' | 'filterQuery' | 'skip' | 'setQuery' | 'startDate' | 'type' - > { - pageFilters: Filter[]; - stackByOptions?: MatrixHistogramOption[]; - defaultFilters?: Filter[]; - defaultStackByOption?: MatrixHistogramOption; -} diff --git a/x-pack/plugins/siem/public/components/arrows/index.test.tsx b/x-pack/plugins/siem/public/components/arrows/index.test.tsx deleted file mode 100644 index 5404a1ac43844f..00000000000000 --- a/x-pack/plugins/siem/public/components/arrows/index.test.tsx +++ /dev/null @@ -1,42 +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 { mount } from 'enzyme'; -import React from 'react'; - -import { TestProviders } from '../../mock'; - -import { ArrowBody, ArrowHead } from '.'; - -describe('arrows', () => { - describe('ArrowBody', () => { - test('renders correctly against snapshot', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find('ArrowBody')).toMatchSnapshot(); - }); - }); - - describe('ArrowHead', () => { - test('it renders an arrow head icon', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="arrow-icon"]') - .first() - .exists() - ).toBe(true); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/autocomplete_field/index.test.tsx b/x-pack/plugins/siem/public/components/autocomplete_field/index.test.tsx deleted file mode 100644 index 72236d799f995c..00000000000000 --- a/x-pack/plugins/siem/public/components/autocomplete_field/index.test.tsx +++ /dev/null @@ -1,385 +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 { EuiFieldSearch } from '@elastic/eui'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import { mount, shallow } from 'enzyme'; -import { noop } from 'lodash/fp'; -import React from 'react'; -import { ThemeProvider } from 'styled-components'; -import { QuerySuggestion, QuerySuggestionTypes } from '../../../../../../src/plugins/data/public'; - -import { TestProviders } from '../../mock'; - -import { AutocompleteField } from '.'; - -const mockAutoCompleteData: QuerySuggestion[] = [ - { - type: QuerySuggestionTypes.Field, - text: 'agent.ephemeral_id ', - description: - '

Filter results that contain agent.ephemeral_id

', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.hostname ', - description: - '

Filter results that contain agent.hostname

', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.id ', - description: - '

Filter results that contain agent.id

', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.name ', - description: - '

Filter results that contain agent.name

', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.type ', - description: - '

Filter results that contain agent.type

', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.version ', - description: - '

Filter results that contain agent.version

', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.test1 ', - description: - '

Filter results that contain agent.test1

', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.test2 ', - description: - '

Filter results that contain agent.test2

', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.test3 ', - description: - '

Filter results that contain agent.test3

', - start: 0, - end: 1, - }, - { - type: QuerySuggestionTypes.Field, - text: 'agent.test4 ', - description: - '

Filter results that contain agent.test4

', - start: 0, - end: 1, - }, -]; - -describe('Autocomplete', () => { - describe('rendering', () => { - test('it renders against snapshot', () => { - const placeholder = 'myPlaceholder'; - - const wrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); - }); - - test('it is rendering with placeholder', () => { - const placeholder = 'myPlaceholder'; - - const wrapper = mount( - - ); - const input = wrapper.find('input[type="search"]'); - expect(input.find('[placeholder]').props().placeholder).toEqual(placeholder); - }); - - test('Rendering suggested items', () => { - const wrapper = mount( - ({ eui: euiDarkVars, darkMode: true })}> - - - ); - const wrapperAutocompleteField = wrapper.find(AutocompleteField); - wrapperAutocompleteField.setState({ areSuggestionsVisible: true }); - wrapper.update(); - - expect(wrapper.find('.euiPanel div[data-test-subj="suggestion-item"]').length).toEqual(10); - }); - - test('Should Not render suggested items if loading new suggestions', () => { - const wrapper = mount( - ({ eui: euiDarkVars, darkMode: true })}> - - - ); - const wrapperAutocompleteField = wrapper.find(AutocompleteField); - wrapperAutocompleteField.setState({ areSuggestionsVisible: true }); - wrapper.update(); - - expect(wrapper.find('.euiPanel div[data-test-subj="suggestion-item"]').length).toEqual(0); - }); - }); - - describe('events', () => { - test('OnChange should have been called', () => { - const onChange = jest.fn((value: string) => value); - - const wrapper = mount( - - ); - const wrapperFixedEuiFieldSearch = wrapper.find('input'); - wrapperFixedEuiFieldSearch.simulate('change', { target: { value: 'test' } }); - expect(onChange).toHaveBeenCalled(); - }); - }); - - test('OnSubmit should have been called by keying enter on the search input', () => { - const onSubmit = jest.fn((value: string) => value); - - const wrapper = mount( - - ); - const wrapperAutocompleteField = wrapper.find(AutocompleteField); - wrapperAutocompleteField.setState({ selectedIndex: null }); - const wrapperFixedEuiFieldSearch = wrapper.find('input'); - wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Enter', preventDefault: noop }); - expect(onSubmit).toHaveBeenCalled(); - }); - - test('OnSubmit should have been called by onSearch event on the input', () => { - const onSubmit = jest.fn((value: string) => value); - - const wrapper = mount( - - ); - const wrapperAutocompleteField = wrapper.find(AutocompleteField); - wrapperAutocompleteField.setState({ selectedIndex: null }); - const wrapperFixedEuiFieldSearch = wrapper.find(EuiFieldSearch); - // TODO: FixedEuiFieldSearch fails to import - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (wrapperFixedEuiFieldSearch as any).props().onSearch(); - expect(onSubmit).toHaveBeenCalled(); - }); - - test('OnChange should have been called if keying enter on a suggested item selected', () => { - const onChange = jest.fn((value: string) => value); - - const wrapper = mount( - - ); - const wrapperAutocompleteField = wrapper.find(AutocompleteField); - wrapperAutocompleteField.setState({ selectedIndex: 1 }); - const wrapperFixedEuiFieldSearch = wrapper.find('input'); - wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Enter', preventDefault: noop }); - expect(onChange).toHaveBeenCalled(); - }); - - test('OnChange should be called if tab is pressed when a suggested item is selected', () => { - const onChange = jest.fn((value: string) => value); - - const wrapper = mount( - - ); - const wrapperAutocompleteField = wrapper.find(AutocompleteField); - wrapperAutocompleteField.setState({ selectedIndex: 1 }); - const wrapperFixedEuiFieldSearch = wrapper.find('input'); - wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Tab', preventDefault: noop }); - expect(onChange).toHaveBeenCalled(); - }); - - test('OnChange should NOT be called if tab is pressed when more than one item is suggested, and no selection has been made', () => { - const onChange = jest.fn((value: string) => value); - - const wrapper = mount( - - ); - - const wrapperFixedEuiFieldSearch = wrapper.find('input'); - wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Tab', preventDefault: noop }); - expect(onChange).not.toHaveBeenCalled(); - }); - - test('OnChange should be called if tab is pressed when only one item is suggested, even though that item is NOT selected', () => { - const onChange = jest.fn((value: string) => value); - const onlyOneSuggestion = [mockAutoCompleteData[0]]; - - const wrapper = mount( - - - - ); - - const wrapperAutocompleteField = wrapper.find(AutocompleteField); - wrapperAutocompleteField.setState({ areSuggestionsVisible: true }); - const wrapperFixedEuiFieldSearch = wrapper.find('input'); - wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Tab', preventDefault: noop }); - expect(onChange).toHaveBeenCalled(); - }); - - test('OnChange should NOT be called if tab is pressed when 0 items are suggested', () => { - const onChange = jest.fn((value: string) => value); - - const wrapper = mount( - - ); - - const wrapperFixedEuiFieldSearch = wrapper.find('input'); - wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'Tab', preventDefault: noop }); - expect(onChange).not.toHaveBeenCalled(); - }); - - test('Load more suggestions when arrowdown on the search bar', () => { - const loadSuggestions = jest.fn(noop); - - const wrapper = mount( - - ); - const wrapperFixedEuiFieldSearch = wrapper.find('input'); - wrapperFixedEuiFieldSearch.simulate('keydown', { key: 'ArrowDown', preventDefault: noop }); - expect(loadSuggestions).toHaveBeenCalled(); - }); -}); diff --git a/x-pack/plugins/siem/public/components/autocomplete_field/index.tsx b/x-pack/plugins/siem/public/components/autocomplete_field/index.tsx deleted file mode 100644 index 9821bb6048b513..00000000000000 --- a/x-pack/plugins/siem/public/components/autocomplete_field/index.tsx +++ /dev/null @@ -1,333 +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 { - EuiFieldSearch, - EuiFieldSearchProps, - EuiOutsideClickDetector, - EuiPanel, -} from '@elastic/eui'; -import React from 'react'; -import { QuerySuggestion } from '../../../../../../src/plugins/data/public'; - -import euiStyled from '../../../../../legacy/common/eui_styled_components'; - -import { SuggestionItem } from './suggestion_item'; - -interface AutocompleteFieldProps { - 'data-test-subj'?: string; - isLoadingSuggestions: boolean; - isValid: boolean; - loadSuggestions: (value: string, cursorPosition: number, maxCount?: number) => void; - onSubmit?: (value: string) => void; - onChange?: (value: string) => void; - placeholder?: string; - suggestions: QuerySuggestion[]; - value: string; -} - -interface AutocompleteFieldState { - areSuggestionsVisible: boolean; - isFocused: boolean; - selectedIndex: number | null; -} - -export class AutocompleteField extends React.PureComponent< - AutocompleteFieldProps, - AutocompleteFieldState -> { - public readonly state: AutocompleteFieldState = { - areSuggestionsVisible: false, - isFocused: false, - selectedIndex: null, - }; - - private inputElement: HTMLInputElement | null = null; - - public render() { - const { - 'data-test-subj': dataTestSubj, - suggestions, - isLoadingSuggestions, - isValid, - placeholder, - value, - } = this.props; - const { areSuggestionsVisible, selectedIndex } = this.state; - return ( - - - - {areSuggestionsVisible && !isLoadingSuggestions && suggestions.length > 0 ? ( - - {suggestions.map((suggestion, suggestionIndex) => ( - - ))} - - ) : null} - - - ); - } - - public componentDidUpdate(prevProps: AutocompleteFieldProps, prevState: AutocompleteFieldState) { - const hasNewValue = prevProps.value !== this.props.value; - const hasNewSuggestions = prevProps.suggestions !== this.props.suggestions; - - if (hasNewValue) { - this.updateSuggestions(); - } - - if (hasNewSuggestions && this.state.isFocused) { - this.showSuggestions(); - } - } - - private handleChangeInputRef = (element: HTMLInputElement | null) => { - this.inputElement = element; - }; - - private handleChange = (evt: React.ChangeEvent) => { - this.changeValue(evt.currentTarget.value); - }; - - private handleKeyDown = (evt: React.KeyboardEvent) => { - const { suggestions } = this.props; - switch (evt.key) { - case 'ArrowUp': - evt.preventDefault(); - if (suggestions.length > 0) { - this.setState( - composeStateUpdaters(withSuggestionsVisible, withPreviousSuggestionSelected) - ); - } - break; - case 'ArrowDown': - evt.preventDefault(); - if (suggestions.length > 0) { - this.setState(composeStateUpdaters(withSuggestionsVisible, withNextSuggestionSelected)); - } else { - this.updateSuggestions(); - } - break; - case 'Enter': - evt.preventDefault(); - if (this.state.selectedIndex !== null) { - this.applySelectedSuggestion(); - } else { - this.submit(); - } - break; - case 'Tab': - evt.preventDefault(); - if (this.state.areSuggestionsVisible && this.props.suggestions.length === 1) { - this.applySuggestionAt(0)(); - } else if (this.state.selectedIndex !== null) { - this.applySelectedSuggestion(); - } - break; - case 'Escape': - evt.preventDefault(); - evt.stopPropagation(); - this.setState(withSuggestionsHidden); - break; - } - }; - - private handleKeyUp = (evt: React.KeyboardEvent) => { - switch (evt.key) { - case 'ArrowLeft': - case 'ArrowRight': - case 'Home': - case 'End': - this.updateSuggestions(); - break; - } - }; - - private handleFocus = () => { - this.setState(composeStateUpdaters(withSuggestionsVisible, withFocused)); - }; - - private handleBlur = () => { - this.setState(composeStateUpdaters(withSuggestionsHidden, withUnfocused)); - }; - - private selectSuggestionAt = (index: number) => () => { - this.setState(withSuggestionAtIndexSelected(index)); - }; - - private applySelectedSuggestion = () => { - if (this.state.selectedIndex !== null) { - this.applySuggestionAt(this.state.selectedIndex)(); - } - }; - - private applySuggestionAt = (index: number) => () => { - const { value, suggestions } = this.props; - const selectedSuggestion = suggestions[index]; - - if (!selectedSuggestion) { - return; - } - - const newValue = - value.substr(0, selectedSuggestion.start) + - selectedSuggestion.text + - value.substr(selectedSuggestion.end); - - this.setState(withSuggestionsHidden); - this.changeValue(newValue); - this.focusInputElement(); - }; - - private changeValue = (value: string) => { - const { onChange } = this.props; - - if (onChange) { - onChange(value); - } - }; - - private focusInputElement = () => { - if (this.inputElement) { - this.inputElement.focus(); - } - }; - - private showSuggestions = () => { - this.setState(withSuggestionsVisible); - }; - - private submit = () => { - const { isValid, onSubmit, value } = this.props; - - if (isValid && onSubmit) { - onSubmit(value); - } - - this.setState(withSuggestionsHidden); - }; - - private updateSuggestions = () => { - const inputCursorPosition = this.inputElement ? this.inputElement.selectionStart || 0 : 0; - this.props.loadSuggestions(this.props.value, inputCursorPosition, 10); - }; -} - -type StateUpdater = ( - prevState: Readonly, - prevProps: Readonly -) => State | null; - -function composeStateUpdaters(...updaters: Array>) { - return (state: State, props: Props) => - updaters.reduce((currentState, updater) => updater(currentState, props) || currentState, state); -} - -const withPreviousSuggestionSelected = ( - state: AutocompleteFieldState, - props: AutocompleteFieldProps -): AutocompleteFieldState => ({ - ...state, - selectedIndex: - props.suggestions.length === 0 - ? null - : state.selectedIndex !== null - ? (state.selectedIndex + props.suggestions.length - 1) % props.suggestions.length - : Math.max(props.suggestions.length - 1, 0), -}); - -const withNextSuggestionSelected = ( - state: AutocompleteFieldState, - props: AutocompleteFieldProps -): AutocompleteFieldState => ({ - ...state, - selectedIndex: - props.suggestions.length === 0 - ? null - : state.selectedIndex !== null - ? (state.selectedIndex + 1) % props.suggestions.length - : 0, -}); - -const withSuggestionAtIndexSelected = (suggestionIndex: number) => ( - state: AutocompleteFieldState, - props: AutocompleteFieldProps -): AutocompleteFieldState => ({ - ...state, - selectedIndex: - props.suggestions.length === 0 - ? null - : suggestionIndex >= 0 && suggestionIndex < props.suggestions.length - ? suggestionIndex - : 0, -}); - -const withSuggestionsVisible = (state: AutocompleteFieldState) => ({ - ...state, - areSuggestionsVisible: true, -}); - -const withSuggestionsHidden = (state: AutocompleteFieldState) => ({ - ...state, - areSuggestionsVisible: false, - selectedIndex: null, -}); - -const withFocused = (state: AutocompleteFieldState) => ({ - ...state, - isFocused: true, -}); - -const withUnfocused = (state: AutocompleteFieldState) => ({ - ...state, - isFocused: false, -}); - -export const FixedEuiFieldSearch: React.FC & - EuiFieldSearchProps & { - inputRef?: (element: HTMLInputElement | null) => void; - onSearch: (value: string) => void; - }> = EuiFieldSearch as any; // eslint-disable-line @typescript-eslint/no-explicit-any - -const AutocompleteContainer = euiStyled.div` - position: relative; -`; - -AutocompleteContainer.displayName = 'AutocompleteContainer'; - -const SuggestionsPanel = euiStyled(EuiPanel).attrs(() => ({ - paddingSize: 'none', - hasShadow: true, -}))` - position: absolute; - width: 100%; - margin-top: 2px; - overflow: hidden; - z-index: ${props => props.theme.eui.euiZLevel1}; -`; - -SuggestionsPanel.displayName = 'SuggestionsPanel'; diff --git a/x-pack/plugins/siem/public/components/bytes/index.test.tsx b/x-pack/plugins/siem/public/components/bytes/index.test.tsx deleted file mode 100644 index d99a909efad102..00000000000000 --- a/x-pack/plugins/siem/public/components/bytes/index.test.tsx +++ /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 React from 'react'; - -import { TestProviders } from '../../mock'; -import { PreferenceFormattedBytes } from '../formatted_bytes'; -import { useMountAppended } from '../../utils/use_mount_appended'; - -import { Bytes } from '.'; - -describe('Bytes', () => { - const mount = useMountAppended(); - - test('it renders the expected formatted bytes', () => { - const wrapper = mount( - - - - ); - expect( - wrapper - .find(PreferenceFormattedBytes) - .first() - .text() - ).toEqual('1.2MB'); - }); -}); diff --git a/x-pack/plugins/siem/public/components/bytes/index.tsx b/x-pack/plugins/siem/public/components/bytes/index.tsx deleted file mode 100644 index 94c6ecba68be52..00000000000000 --- a/x-pack/plugins/siem/public/components/bytes/index.tsx +++ /dev/null @@ -1,35 +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 { DefaultDraggable } from '../draggables'; -import { PreferenceFormattedBytes } from '../formatted_bytes'; - -export const BYTES_FORMAT = 'bytes'; - -/** - * Renders draggable text containing the value of a field representing a - * duration of time, (e.g. `event.duration`) - */ -export const Bytes = React.memo<{ - contextId: string; - eventId: string; - fieldName: string; - value?: string | null; -}>(({ contextId, eventId, fieldName, value }) => ( - - - -)); - -Bytes.displayName = 'Bytes'; diff --git a/x-pack/plugins/siem/public/components/certificate_fingerprint/index.test.tsx b/x-pack/plugins/siem/public/components/certificate_fingerprint/index.test.tsx deleted file mode 100644 index 9cd0af062c54a6..00000000000000 --- a/x-pack/plugins/siem/public/components/certificate_fingerprint/index.test.tsx +++ /dev/null @@ -1,78 +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 { TestProviders } from '../../mock'; -import { useMountAppended } from '../../utils/use_mount_appended'; - -import { CertificateFingerprint } from '.'; - -describe('CertificateFingerprint', () => { - const mount = useMountAppended(); - test('renders the expected label', () => { - const wrapper = mount( - - - - ); - expect( - wrapper - .find('[data-test-subj="fingerprint-label"]') - .first() - .text() - ).toEqual('client cert'); - }); - - test('renders the fingerprint as text', () => { - const wrapper = mount( - - - - ); - expect( - wrapper - .find('[data-test-subj="certificate-fingerprint-link"]') - .first() - .text() - ).toEqual('3f4c57934e089f02ae7511200aee2d7e7aabd272'); - }); - - test('it renders a hyperlink to an external site to compare the fingerprint against a known set of signatures', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="certificate-fingerprint-link"]') - .first() - .props().href - ).toEqual( - 'https://sslbl.abuse.ch/ssl-certificates/sha1/3f4c57934e089f02ae7511200aee2d7e7aabd272' - ); - }); -}); diff --git a/x-pack/plugins/siem/public/components/certificate_fingerprint/index.tsx b/x-pack/plugins/siem/public/components/certificate_fingerprint/index.tsx deleted file mode 100644 index 181d92dce06f99..00000000000000 --- a/x-pack/plugins/siem/public/components/certificate_fingerprint/index.tsx +++ /dev/null @@ -1,68 +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 { EuiText } from '@elastic/eui'; -import React from 'react'; -import styled from 'styled-components'; - -import { DraggableBadge } from '../draggables'; -import { ExternalLinkIcon } from '../external_link_icon'; -import { CertificateFingerprintLink } from '../links'; - -import * as i18n from './translations'; - -export type CertificateType = 'client' | 'server'; - -export const TLS_CLIENT_CERTIFICATE_FINGERPRINT_SHA1_FIELD_NAME = - 'tls.client_certificate.fingerprint.sha1'; -export const TLS_SERVER_CERTIFICATE_FINGERPRINT_SHA1_FIELD_NAME = - 'tls.server_certificate.fingerprint.sha1'; - -const FingerprintLabel = styled.span` - margin-right: 5px; -`; - -FingerprintLabel.displayName = 'FingerprintLabel'; - -/** - * Represents a field containing a certificate fingerprint (e.g. a sha1), with - * a link to an external site, which in-turn compares the fingerprint against a - * set of known fingerprints - * Examples: - * 'tls.client_certificate.fingerprint.sha1' - * 'tls.server_certificate.fingerprint.sha1' - */ -export const CertificateFingerprint = React.memo<{ - eventId: string; - certificateType: CertificateType; - contextId: string; - fieldName: string; - value?: string | null; -}>(({ eventId, certificateType, contextId, fieldName, value }) => { - return ( - - {fieldName} - - } - value={value} - > - - {certificateType === 'client' ? i18n.CLIENT_CERT : i18n.SERVER_CERT} - - - - - ); -}); - -CertificateFingerprint.displayName = 'CertificateFingerprint'; diff --git a/x-pack/plugins/siem/public/components/direction/index.tsx b/x-pack/plugins/siem/public/components/direction/index.tsx deleted file mode 100644 index ad1e63dbd7e6a1..00000000000000 --- a/x-pack/plugins/siem/public/components/direction/index.tsx +++ /dev/null @@ -1,73 +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 { NetworkDirectionEcs } from '../../graphql/types'; -import { DraggableBadge } from '../draggables'; -import { NETWORK_DIRECTION_FIELD_NAME } from '../source_destination/field_names'; - -export const INBOUND = 'inbound'; -export const OUTBOUND = 'outbound'; - -export const EXTERNAL = 'external'; -export const INTERNAL = 'internal'; - -export const INCOMING = 'incoming'; -export const OUTGOING = 'outgoing'; - -export const LISTENING = 'listening'; -export const UNKNOWN = 'unknown'; - -export const DEFAULT_ICON = 'questionInCircle'; - -/** Returns an icon representing the value of `network.direction` */ -export const getDirectionIcon = ( - networkDirection?: string | null -): 'arrowUp' | 'arrowDown' | 'globe' | 'bullseye' | 'questionInCircle' => { - if (networkDirection == null) { - return DEFAULT_ICON; - } - - const direction = `${networkDirection}`.toLowerCase(); - - switch (direction) { - case NetworkDirectionEcs.outbound: - case NetworkDirectionEcs.outgoing: - return 'arrowUp'; - case NetworkDirectionEcs.inbound: - case NetworkDirectionEcs.incoming: - case NetworkDirectionEcs.listening: - return 'arrowDown'; - case NetworkDirectionEcs.external: - return 'globe'; - case NetworkDirectionEcs.internal: - return 'bullseye'; - case NetworkDirectionEcs.unknown: - default: - return DEFAULT_ICON; - } -}; - -/** - * Renders a badge containing the value of `network.direction` - */ -export const DirectionBadge = React.memo<{ - contextId: string; - direction?: string | null; - eventId: string; -}>(({ contextId, eventId, direction }) => ( - -)); - -DirectionBadge.displayName = 'DirectionBadge'; diff --git a/x-pack/plugins/siem/public/components/drag_and_drop/helpers.ts b/x-pack/plugins/siem/public/components/drag_and_drop/helpers.ts deleted file mode 100644 index 9b37387ce076be..00000000000000 --- a/x-pack/plugins/siem/public/components/drag_and_drop/helpers.ts +++ /dev/null @@ -1,333 +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 { isString } from 'lodash/fp'; -import { DropResult } from 'react-beautiful-dnd'; -import { Dispatch } from 'redux'; -import { ActionCreator } from 'typescript-fsa'; - -import { BrowserField, BrowserFields, getAllFieldsByName } from '../../containers/source'; -import { dragAndDropActions, timelineActions } from '../../store/actions'; -import { IdToDataProvider } from '../../store/drag_and_drop/model'; -import { ColumnHeaderOptions } from '../../store/timeline/model'; -import { DEFAULT_COLUMN_MIN_WIDTH } from '../timeline/body/constants'; -import { DataProvider } from '../timeline/data_providers/data_provider'; -import { addContentToTimeline } from '../timeline/data_providers/helpers'; - -export const draggableIdPrefix = 'draggableId'; - -export const droppableIdPrefix = 'droppableId'; - -export const draggableContentPrefix = `${draggableIdPrefix}.content.`; - -export const draggableTimelineProvidersPrefix = `${draggableIdPrefix}.timelineProviders.`; - -export const draggableFieldPrefix = `${draggableIdPrefix}.field.`; - -export const droppableContentPrefix = `${droppableIdPrefix}.content.`; - -export const droppableFieldPrefix = `${droppableIdPrefix}.field.`; - -export const droppableTimelineProvidersPrefix = `${droppableIdPrefix}.timelineProviders.`; - -export const droppableTimelineColumnsPrefix = `${droppableIdPrefix}.timelineColumns.`; - -export const droppableTimelineFlyoutButtonPrefix = `${droppableIdPrefix}.flyoutButton.`; - -export const getDraggableId = (dataProviderId: string): string => - `${draggableContentPrefix}${dataProviderId}`; - -export const getDraggableFieldId = ({ - contextId, - fieldId, -}: { - contextId: string; - fieldId: string; -}): string => `${draggableFieldPrefix}${escapeContextId(contextId)}.${escapeFieldId(fieldId)}`; - -export const getTimelineProviderDroppableId = ({ - groupIndex, - timelineId, -}: { - groupIndex: number; - timelineId: string; -}): string => `${droppableTimelineProvidersPrefix}${timelineId}.group.${groupIndex}`; - -export const getTimelineProviderDraggableId = ({ - dataProviderId, - groupIndex, - timelineId, -}: { - dataProviderId: string; - groupIndex: number; - timelineId: string; -}): string => - `${draggableTimelineProvidersPrefix}${timelineId}.group.${groupIndex}.${dataProviderId}`; - -export const getDroppableId = (visualizationPlaceholderId: string): string => - `${droppableContentPrefix}${visualizationPlaceholderId}`; - -export const sourceIsContent = (result: DropResult): boolean => - result.source.droppableId.startsWith(droppableContentPrefix); - -export const sourceAndDestinationAreSameTimelineProviders = (result: DropResult): boolean => { - const regex = /^droppableId\.timelineProviders\.(\S+)\./; - const sourceMatches = result.source.droppableId.match(regex) ?? []; - const destinationMatches = result.destination?.droppableId.match(regex) ?? []; - - return ( - sourceMatches.length >= 2 && - destinationMatches.length >= 2 && - sourceMatches[1] === destinationMatches[1] - ); -}; - -export const draggableIsContent = (result: DropResult | { draggableId: string }): boolean => - result.draggableId.startsWith(draggableContentPrefix); - -export const draggableIsField = (result: DropResult | { draggableId: string }): boolean => - result.draggableId.startsWith(draggableFieldPrefix); - -export const reasonIsDrop = (result: DropResult): boolean => result.reason === 'DROP'; - -export const destinationIsTimelineProviders = (result: DropResult): boolean => - result.destination != null && - result.destination.droppableId.startsWith(droppableTimelineProvidersPrefix); - -export const destinationIsTimelineColumns = (result: DropResult): boolean => - result.destination != null && - result.destination.droppableId.startsWith(droppableTimelineColumnsPrefix); - -export const destinationIsTimelineButton = (result: DropResult): boolean => - result.destination != null && - result.destination.droppableId.startsWith(droppableTimelineFlyoutButtonPrefix); - -export const getProviderIdFromDraggable = (result: DropResult): string => - result.draggableId.substring(result.draggableId.lastIndexOf('.') + 1); - -export const getFieldIdFromDraggable = (result: DropResult): string => - unEscapeFieldId(result.draggableId.substring(result.draggableId.lastIndexOf('.') + 1)); - -export const escapeDataProviderId = (path: string) => path.replace(/\./g, '_'); - -export const escapeContextId = (path: string) => path.replace(/\./g, '_'); - -export const escapeFieldId = (path: string) => path.replace(/\./g, '!!!DOT!!!'); - -export const unEscapeFieldId = (path: string) => path.replace(/!!!DOT!!!/g, '.'); - -export const providerWasDroppedOnTimeline = (result: DropResult): boolean => - reasonIsDrop(result) && - draggableIsContent(result) && - sourceIsContent(result) && - destinationIsTimelineProviders(result); - -export const userIsReArrangingProviders = (result: DropResult): boolean => - reasonIsDrop(result) && sourceAndDestinationAreSameTimelineProviders(result); - -export const fieldWasDroppedOnTimelineColumns = (result: DropResult): boolean => - reasonIsDrop(result) && draggableIsField(result) && destinationIsTimelineColumns(result); - -interface AddProviderToTimelineParams { - activeTimelineDataProviders: DataProvider[]; - dataProviders: IdToDataProvider; - dispatch: Dispatch; - noProviderFound?: ActionCreator<{ - id: string; - }>; - onAddedToTimeline: (fieldOrValue: string) => void; - result: DropResult; - timelineId: string; -} - -interface AddFieldToTimelineColumnsParams { - upsertColumn?: ActionCreator<{ - column: ColumnHeaderOptions; - id: string; - index: number; - }>; - browserFields: BrowserFields; - dispatch: Dispatch; - result: DropResult; - timelineId: string; -} - -export const addProviderToTimeline = ({ - activeTimelineDataProviders, - dataProviders, - dispatch, - result, - timelineId, - noProviderFound = dragAndDropActions.noProviderFound, - onAddedToTimeline, -}: AddProviderToTimelineParams): void => { - const providerId = getProviderIdFromDraggable(result); - const providerToAdd = dataProviders[providerId]; - - if (providerToAdd) { - addContentToTimeline({ - dataProviders: activeTimelineDataProviders, - destination: result.destination, - dispatch, - onAddedToTimeline, - providerToAdd, - timelineId, - }); - } else { - dispatch(noProviderFound({ id: providerId })); - } -}; - -export const addFieldToTimelineColumns = ({ - upsertColumn = timelineActions.upsertColumn, - browserFields, - dispatch, - result, - timelineId, -}: AddFieldToTimelineColumnsParams): void => { - const fieldId = getFieldIdFromDraggable(result); - const allColumns = getAllFieldsByName(browserFields); - const column = allColumns[fieldId]; - - if (column != null) { - dispatch( - upsertColumn({ - column: { - category: column.category, - columnHeaderType: 'not-filtered', - description: isString(column.description) ? column.description : undefined, - example: isString(column.example) ? column.example : undefined, - id: fieldId, - type: column.type, - aggregatable: column.aggregatable, - width: DEFAULT_COLUMN_MIN_WIDTH, - }, - id: timelineId, - index: result.destination != null ? result.destination.index : 0, - }) - ); - } else { - // create a column definition, because it doesn't exist in the browserFields: - dispatch( - upsertColumn({ - column: { - columnHeaderType: 'not-filtered', - id: fieldId, - width: DEFAULT_COLUMN_MIN_WIDTH, - }, - id: timelineId, - index: result.destination != null ? result.destination.index : 0, - }) - ); - } -}; - -/** - * Prevents fields from being dragged or dropped to any area other than column - * header drop zone in the timeline - */ -export const DRAG_TYPE_FIELD = 'drag-type-field'; - -/** This class is added to the document body while dragging */ -export const IS_DRAGGING_CLASS_NAME = 'is-dragging'; - -/** This class is added to the document body while timeline field dragging */ -export const IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME = 'is-timeline-field-dragging'; - -export const allowTopN = ({ - browserField, - fieldName, -}: { - browserField: Partial | undefined; - fieldName: string; -}): boolean => { - const isAggregatable = browserField?.aggregatable ?? false; - const fieldType = browserField?.type ?? ''; - const isAllowedType = [ - 'boolean', - 'geo-point', - 'geo-shape', - 'ip', - 'keyword', - 'number', - 'numeric', - 'string', - ].includes(fieldType); - - // TODO: remove this explicit whitelist when the ECS documentation includes signals - const isWhitelistedNonBrowserField = [ - 'signal.ancestors.depth', - 'signal.ancestors.id', - 'signal.ancestors.rule', - 'signal.ancestors.type', - 'signal.original_event.action', - 'signal.original_event.category', - 'signal.original_event.code', - 'signal.original_event.created', - 'signal.original_event.dataset', - 'signal.original_event.duration', - 'signal.original_event.end', - 'signal.original_event.hash', - 'signal.original_event.id', - 'signal.original_event.kind', - 'signal.original_event.module', - 'signal.original_event.original', - 'signal.original_event.outcome', - 'signal.original_event.provider', - 'signal.original_event.risk_score', - 'signal.original_event.risk_score_norm', - 'signal.original_event.sequence', - 'signal.original_event.severity', - 'signal.original_event.start', - 'signal.original_event.timezone', - 'signal.original_event.type', - 'signal.original_time', - 'signal.parent.depth', - 'signal.parent.id', - 'signal.parent.index', - 'signal.parent.rule', - 'signal.parent.type', - 'signal.rule.created_by', - 'signal.rule.description', - 'signal.rule.enabled', - 'signal.rule.false_positives', - 'signal.rule.filters', - 'signal.rule.from', - 'signal.rule.id', - 'signal.rule.immutable', - 'signal.rule.index', - 'signal.rule.interval', - 'signal.rule.language', - 'signal.rule.max_signals', - 'signal.rule.name', - 'signal.rule.note', - 'signal.rule.output_index', - 'signal.rule.query', - 'signal.rule.references', - 'signal.rule.risk_score', - 'signal.rule.rule_id', - 'signal.rule.saved_id', - 'signal.rule.severity', - 'signal.rule.size', - 'signal.rule.tags', - 'signal.rule.threat', - 'signal.rule.threat.tactic.id', - 'signal.rule.threat.tactic.name', - 'signal.rule.threat.tactic.reference', - 'signal.rule.threat.technique.id', - 'signal.rule.threat.technique.name', - 'signal.rule.threat.technique.reference', - 'signal.rule.timeline_id', - 'signal.rule.timeline_title', - 'signal.rule.to', - 'signal.rule.type', - 'signal.rule.updated_by', - 'signal.rule.version', - 'signal.status', - ].includes(fieldName); - - return isWhitelistedNonBrowserField || (isAggregatable && isAllowedType); -}; diff --git a/x-pack/plugins/siem/public/components/draggables/index.tsx b/x-pack/plugins/siem/public/components/draggables/index.tsx deleted file mode 100644 index cea900f7bccf96..00000000000000 --- a/x-pack/plugins/siem/public/components/draggables/index.tsx +++ /dev/null @@ -1,176 +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 { EuiBadge, EuiToolTip, IconType } from '@elastic/eui'; -import React from 'react'; -import styled from 'styled-components'; - -import { DragEffects, DraggableWrapper } from '../drag_and_drop/draggable_wrapper'; -import { escapeDataProviderId } from '../drag_and_drop/helpers'; -import { getEmptyStringTag } from '../empty_value'; -import { IS_OPERATOR } from '../timeline/data_providers/data_provider'; -import { Provider } from '../timeline/data_providers/provider'; - -export interface DefaultDraggableType { - id: string; - field: string; - value?: string | null; - name?: string | null; - queryValue?: string | null; - children?: React.ReactNode; - tooltipContent?: React.ReactNode; -} - -/** - * Only returns true if the specified tooltipContent is exactly `null`. - * Example input / output: - * `bob -> false` - * `undefined -> false` - * `thing -> false` - * `null -> true` - */ -export const tooltipContentIsExplicitlyNull = (tooltipContent?: React.ReactNode): boolean => - tooltipContent === null; // an explicit / exact null check - -/** - * Derives the tooltip content from the field name if no tooltip was specified - */ -export const getDefaultWhenTooltipIsUnspecified = ({ - field, - tooltipContent, -}: { - field: string; - tooltipContent?: React.ReactNode; -}): React.ReactNode => (tooltipContent != null ? tooltipContent : field); - -/** - * Renders the content of the draggable, wrapped in a tooltip - */ -const Content = React.memo<{ - children?: React.ReactNode; - field: string; - tooltipContent?: React.ReactNode; - value?: string | null; -}>(({ children, field, tooltipContent, value }) => - !tooltipContentIsExplicitlyNull(tooltipContent) ? ( - - <>{children ? children : value} - - ) : ( - <>{children ? children : value} - ) -); - -Content.displayName = 'Content'; - -/** - * Draggable text (or an arbitrary visualization specified by `children`) - * that's only displayed when the specified value is non-`null`. - * - * @param id - a unique draggable id, which typically follows the format `${contextId}-${eventId}-${field}-${value}` - * @param field - the name of the field, e.g. `network.transport` - * @param value - value of the field e.g. `tcp` - * @param name - defaulting to `field`, this optional human readable name is used by the `DataProvider` that represents the data - * @param children - defaults to displaying `value`, this allows an arbitrary visualization to be displayed in lieu of the default behavior - * @param tooltipContent - defaults to displaying `field`, pass `null` to - * prevent a tooltip from being displayed, or pass arbitrary content - * @param queryValue - defaults to `value`, this query overrides the `queryMatch.value` used by the `DataProvider` that represents the data - */ -export const DefaultDraggable = React.memo( - ({ id, field, value, name, children, tooltipContent, queryValue }) => - value != null ? ( - - snapshot.isDragging ? ( - - - - ) : ( - - {children} - - ) - } - /> - ) : null -); - -DefaultDraggable.displayName = 'DefaultDraggable'; - -export const Badge = styled(EuiBadge)` - vertical-align: top; -` as any; // eslint-disable-line @typescript-eslint/no-explicit-any - -Badge.displayName = 'Badge'; - -export type BadgeDraggableType = Omit & { - contextId: string; - eventId: string; - iconType?: IconType; - color?: string; -}; - -/** - * A draggable badge that's only displayed when the specified value is non-`null`. - * - * @param contextId - used as part of the formula to derive a unique draggable id, this describes the context e.g. `event-fields-browser` in which the badge is displayed - * @param eventId - uniquely identifies an event, as specified in the `_id` field of the document - * @param field - the name of the field, e.g. `network.transport` - * @param value - value of the field e.g. `tcp` - * @param iconType -the (optional) type of icon e.g. `snowflake` to display on the badge - * @param name - defaulting to `field`, this optional human readable name is used by the `DataProvider` that represents the data - * @param color - defaults to `hollow`, optionally overwrite the color of the badge icon - * @param children - defaults to displaying `value`, this allows an arbitrary visualization to be displayed in lieu of the default behavior - * @param tooltipContent - defaults to displaying `field`, pass `null` to - * prevent a tooltip from being displayed, or pass arbitrary content - * @param queryValue - defaults to `value`, this query overrides the `queryMatch.value` used by the `DataProvider` that represents the data - */ -export const DraggableBadge = React.memo( - ({ - contextId, - eventId, - field, - value, - iconType, - name, - color = 'hollow', - children, - tooltipContent, - queryValue, - }) => - value != null ? ( - - - {children ? children : value !== '' ? value : getEmptyStringTag()} - - - ) : null -); - -DraggableBadge.displayName = 'DraggableBadge'; diff --git a/x-pack/plugins/siem/public/components/duration/index.test.tsx b/x-pack/plugins/siem/public/components/duration/index.test.tsx deleted file mode 100644 index 0dbc60ad9ae523..00000000000000 --- a/x-pack/plugins/siem/public/components/duration/index.test.tsx +++ /dev/null @@ -1,36 +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 { TestProviders } from '../../mock'; -import { ONE_MILLISECOND_AS_NANOSECONDS } from '../formatted_duration/helpers'; -import { useMountAppended } from '../../utils/use_mount_appended'; - -import { Duration } from '.'; - -describe('Duration', () => { - const mount = useMountAppended(); - - test('it renders the expected formatted duration', () => { - const wrapper = mount( - - - - ); - expect( - wrapper - .find('[data-test-subj="formatted-duration"]') - .first() - .text() - ).toEqual('1ms'); - }); -}); diff --git a/x-pack/plugins/siem/public/components/duration/index.tsx b/x-pack/plugins/siem/public/components/duration/index.tsx deleted file mode 100644 index 76712b789ffbe5..00000000000000 --- a/x-pack/plugins/siem/public/components/duration/index.tsx +++ /dev/null @@ -1,35 +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 { DefaultDraggable } from '../draggables'; -import { FormattedDuration } from '../formatted_duration'; - -export const EVENT_DURATION_FIELD_NAME = 'event.duration'; - -/** - * Renders draggable text containing the value of a field representing a - * duration of time, (e.g. `event.duration`) - */ -export const Duration = React.memo<{ - contextId: string; - eventId: string; - fieldName: string; - value?: string | null; -}>(({ contextId, eventId, fieldName, value }) => ( - - - -)); - -Duration.displayName = 'Duration'; diff --git a/x-pack/plugins/siem/public/components/edit_data_provider/helpers.test.tsx b/x-pack/plugins/siem/public/components/edit_data_provider/helpers.test.tsx deleted file mode 100644 index 74430873064280..00000000000000 --- a/x-pack/plugins/siem/public/components/edit_data_provider/helpers.test.tsx +++ /dev/null @@ -1,295 +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 { mockBrowserFields } from '../../containers/source/mock'; -import { EXISTS_OPERATOR, IS_OPERATOR } from '../timeline/data_providers/data_provider'; - -import { - getCategorizedFieldNames, - getExcludedFromSelection, - getFieldNames, - getQueryOperatorFromSelection, - selectionsAreValid, -} from './helpers'; - -import * as i18n from './translations'; - -describe('helpers', () => { - describe('getFieldNames', () => { - test('it should return the expected field names in a category', () => { - expect(getFieldNames(mockBrowserFields.auditd)).toEqual([ - 'auditd.data.a0', - 'auditd.data.a1', - 'auditd.data.a2', - ]); - }); - }); - - describe('getCategorizedFieldNames', () => { - test('it should return the expected field names grouped by category', () => { - expect(getCategorizedFieldNames(mockBrowserFields)).toEqual([ - { - label: 'agent', - options: [ - { label: 'agent.ephemeral_id' }, - { label: 'agent.hostname' }, - { label: 'agent.id' }, - { label: 'agent.name' }, - ], - }, - { - label: 'auditd', - options: [ - { label: 'auditd.data.a0' }, - { label: 'auditd.data.a1' }, - { label: 'auditd.data.a2' }, - ], - }, - { label: 'base', options: [{ label: '@timestamp' }] }, - { - label: 'client', - options: [ - { label: 'client.address' }, - { label: 'client.bytes' }, - { label: 'client.domain' }, - { label: 'client.geo.country_iso_code' }, - ], - }, - { - label: 'cloud', - options: [{ label: 'cloud.account.id' }, { label: 'cloud.availability_zone' }], - }, - { - label: 'container', - options: [ - { label: 'container.id' }, - { label: 'container.image.name' }, - { label: 'container.image.tag' }, - ], - }, - { - label: 'destination', - options: [ - { label: 'destination.address' }, - { label: 'destination.bytes' }, - { label: 'destination.domain' }, - { label: 'destination.ip' }, - { label: 'destination.port' }, - ], - }, - { label: 'event', options: [{ label: 'event.end' }] }, - { label: 'source', options: [{ label: 'source.ip' }, { label: 'source.port' }] }, - ]); - }); - }); - - describe('selectionsAreValid', () => { - test('it should return true when the selected field and operator are valid', () => { - expect( - selectionsAreValid({ - browserFields: mockBrowserFields, - selectedField: [ - { - label: 'destination.bytes', - }, - ], - selectedOperator: [ - { - label: 'is', - }, - ], - }) - ).toBe(true); - }); - - test('it should return false when the selected field is empty', () => { - expect( - selectionsAreValid({ - browserFields: mockBrowserFields, - selectedField: [ - { - label: '', - }, - ], - selectedOperator: [ - { - label: 'is', - }, - ], - }) - ).toBe(false); - }); - - test('it should return false when the selected field is unknown', () => { - expect( - selectionsAreValid({ - browserFields: mockBrowserFields, - selectedField: [ - { - label: 'invalid-field', - }, - ], - selectedOperator: [ - { - label: 'is', - }, - ], - }) - ).toBe(false); - }); - - test('it should return false when the selected operator is empty', () => { - expect( - selectionsAreValid({ - browserFields: mockBrowserFields, - selectedField: [ - { - label: 'destination.bytes', - }, - ], - selectedOperator: [ - { - label: '', - }, - ], - }) - ).toBe(false); - }); - - test('it should return false when the selected operator is unknown', () => { - expect( - selectionsAreValid({ - browserFields: mockBrowserFields, - selectedField: [ - { - label: 'destination.bytes', - }, - ], - selectedOperator: [ - { - label: 'invalid-operator', - }, - ], - }) - ).toBe(false); - }); - }); - - describe('getQueryOperatorFromSelection', () => { - const validSelections = [ - { - operator: i18n.IS, - expected: IS_OPERATOR, - }, - { - operator: i18n.IS_NOT, - expected: IS_OPERATOR, - }, - { - operator: i18n.EXISTS, - expected: EXISTS_OPERATOR, - }, - { - operator: i18n.DOES_NOT_EXIST, - expected: EXISTS_OPERATOR, - }, - ]; - - validSelections.forEach(({ operator, expected }) => { - test(`it should the expected operator given "${operator}", a valid selection`, () => { - expect( - getQueryOperatorFromSelection([ - { - label: operator, - }, - ]) - ).toEqual(expected); - }); - }); - - test('it should default to the "is" operator given an empty selection', () => { - expect( - getQueryOperatorFromSelection([ - { - label: '', - }, - ]) - ).toEqual(IS_OPERATOR); - }); - - test('it should default to the "is" operator given an invalid selection', () => { - expect( - getQueryOperatorFromSelection([ - { - label: 'invalid', - }, - ]) - ).toEqual(IS_OPERATOR); - }); - }); - - describe('getExcludedFromSelection', () => { - test('it returns false when the selected operator is empty', () => { - expect( - getExcludedFromSelection([ - { - label: '', - }, - ]) - ).toBe(false); - }); - - test('it returns false when the "is" operator is selected', () => { - expect( - getExcludedFromSelection([ - { - label: i18n.IS, - }, - ]) - ).toBe(false); - }); - - test('it returns false when the "exists" operator is selected', () => { - expect( - getExcludedFromSelection([ - { - label: i18n.EXISTS, - }, - ]) - ).toBe(false); - }); - - test('it returns false when an unknown selection is made', () => { - expect( - getExcludedFromSelection([ - { - label: 'an unknown selection', - }, - ]) - ).toBe(false); - }); - - test('it returns true when "is not" is selected', () => { - expect( - getExcludedFromSelection([ - { - label: i18n.IS_NOT, - }, - ]) - ).toBe(true); - }); - - test('it returns true when "does not exist" is selected', () => { - expect( - getExcludedFromSelection([ - { - label: i18n.DOES_NOT_EXIST, - }, - ]) - ).toBe(true); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/edit_data_provider/helpers.tsx b/x-pack/plugins/siem/public/components/edit_data_provider/helpers.tsx deleted file mode 100644 index e6afc86a7ee678..00000000000000 --- a/x-pack/plugins/siem/public/components/edit_data_provider/helpers.tsx +++ /dev/null @@ -1,101 +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 { findIndex } from 'lodash/fp'; -import { EuiComboBoxOptionOption } from '@elastic/eui'; - -import { BrowserField, BrowserFields, getAllFieldsByName } from '../../containers/source'; -import { - QueryOperator, - EXISTS_OPERATOR, - IS_OPERATOR, -} from '../timeline/data_providers/data_provider'; - -import * as i18n from './translations'; - -/** The list of operators to display in the `Operator` select */ -export const operatorLabels: EuiComboBoxOptionOption[] = [ - { - label: i18n.IS, - }, - { - label: i18n.IS_NOT, - }, - { - label: i18n.EXISTS, - }, - { - label: i18n.DOES_NOT_EXIST, - }, -]; - -/** Returns the names of fields in a category */ -export const getFieldNames = (category: Partial): string[] => - category.fields != null && Object.keys(category.fields).length > 0 - ? Object.keys(category.fields) - : []; - -/** Returns all field names by category, for display in an `EuiComboBox` */ -export const getCategorizedFieldNames = (browserFields: BrowserFields): EuiComboBoxOptionOption[] => - Object.keys(browserFields) - .sort() - .map(categoryId => ({ - label: categoryId, - options: getFieldNames(browserFields[categoryId]).map(fieldId => ({ - label: fieldId, - })), - })); - -/** Returns true if the specified field name is valid */ -export const selectionsAreValid = ({ - browserFields, - selectedField, - selectedOperator, -}: { - browserFields: BrowserFields; - selectedField: EuiComboBoxOptionOption[]; - selectedOperator: EuiComboBoxOptionOption[]; -}): boolean => { - const fieldId = selectedField.length > 0 ? selectedField[0].label : ''; - const operator = selectedOperator.length > 0 ? selectedOperator[0].label : ''; - - const fieldIsValid = getAllFieldsByName(browserFields)[fieldId] != null; - const operatorIsValid = findIndex(o => o.label === operator, operatorLabels) !== -1; - - return fieldIsValid && operatorIsValid; -}; - -/** Returns a `QueryOperator` based on the user's Operator selection */ -export const getQueryOperatorFromSelection = ( - selectedOperator: EuiComboBoxOptionOption[] -): QueryOperator => { - const selection = selectedOperator.length > 0 ? selectedOperator[0].label : ''; - - switch (selection) { - case i18n.IS: // fall through - case i18n.IS_NOT: - return IS_OPERATOR; - case i18n.EXISTS: // fall through - case i18n.DOES_NOT_EXIST: - return EXISTS_OPERATOR; - default: - return IS_OPERATOR; - } -}; - -/** - * Returns `true` when the search excludes results that match the specified data provider - */ -export const getExcludedFromSelection = (selectedOperator: EuiComboBoxOptionOption[]): boolean => { - const selection = selectedOperator.length > 0 ? selectedOperator[0].label : ''; - - switch (selection) { - case i18n.IS_NOT: // fall through - case i18n.DOES_NOT_EXIST: - return true; - default: - return false; - } -}; diff --git a/x-pack/plugins/siem/public/components/edit_data_provider/index.test.tsx b/x-pack/plugins/siem/public/components/edit_data_provider/index.test.tsx deleted file mode 100644 index 1786905a4bb48a..00000000000000 --- a/x-pack/plugins/siem/public/components/edit_data_provider/index.test.tsx +++ /dev/null @@ -1,428 +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 { mount } from 'enzyme'; -import React from 'react'; - -import { mockBrowserFields } from '../../containers/source/mock'; -import { TestProviders } from '../../mock'; -import { IS_OPERATOR, EXISTS_OPERATOR } from '../timeline/data_providers/data_provider'; - -import { StatefulEditDataProvider } from '.'; - -interface HasIsDisabled { - isDisabled: boolean; -} - -describe('StatefulEditDataProvider', () => { - const field = 'client.address'; - const timelineId = 'test'; - const value = 'test-host'; - - test('it renders the current field', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="field"]') - .first() - .text() - ).toEqual(field); - }); - - test('it renders the expected placeholder for the current field when field is empty', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="field"]') - .first() - .props().placeholder - ).toEqual('Select a field'); - }); - - test('it renders the "is" operator in a humanized format', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="operator"]') - .first() - .text() - ).toEqual('is'); - }); - - test('it renders the negated "is" operator in a humanized format when isExcluded is true', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="operator"]') - .first() - .text() - ).toEqual('is not'); - }); - - test('it renders the "exists" operator in human-readable format', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="operator"]') - .first() - .text() - ).toEqual('exists'); - }); - - test('it renders the negated "exists" operator in a humanized format when isExcluded is true', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="operator"]') - .first() - .text() - ).toEqual('does not exist'); - }); - - test('it renders the current value when the operator is "is"', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="value"]') - .first() - .props().value - ).toEqual(value); - }); - - test('it renders the current value when the type of value is an array', () => { - const reallyAnArray = ([value] as unknown) as string; - - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="value"]') - .first() - .props().value - ).toEqual(value); - }); - - test('it does NOT render the current value when the operator is "is not" (isExcluded is true)', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="value"]') - .first() - .props().value - ).toEqual(value); - }); - - test('it renders the expected placeholder when value is empty', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="value"]') - .first() - .props().placeholder - ).toEqual('value'); - }); - - test('it does NOT render value when the operator is "exists"', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="value"]').exists()).toBe(false); - }); - - test('it does NOT render value when the operator is "not exists" (isExcluded is true)', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="value"]').exists()).toBe(false); - }); - - test('it does NOT disable the save button when field is valid', () => { - const wrapper = mount( - - - - ); - - const props = wrapper - .find('[data-test-subj="save"]') - .first() - .props() as HasIsDisabled; - - expect(props.isDisabled).toBe(false); - }); - - test('it disables the save button when field is invalid because it is empty', () => { - const wrapper = mount( - - - - ); - - const props = wrapper - .find('[data-test-subj="save"]') - .first() - .props() as HasIsDisabled; - - expect(props.isDisabled).toBe(true); - }); - - test('it disables the save button when field is invalid because it is not contained in the browser fields', () => { - const wrapper = mount( - - - - ); - - const props = wrapper - .find('[data-test-subj="save"]') - .first() - .props() as HasIsDisabled; - - expect(props.isDisabled).toBe(true); - }); - - test('it invokes onDataProviderEdited with the expected values when the user clicks the save button', () => { - const onDataProviderEdited = jest.fn(); - - const wrapper = mount( - - - - ); - - wrapper - .find('[data-test-subj="save"]') - .first() - .simulate('click'); - - wrapper.update(); - - expect(onDataProviderEdited).toBeCalledWith({ - andProviderId: undefined, - excluded: false, - field: 'client.address', - id: 'test', - operator: ':', - providerId: 'hosts-table-hostName-test-host', - value: 'test-host', - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/edit_data_provider/index.tsx b/x-pack/plugins/siem/public/components/edit_data_provider/index.tsx deleted file mode 100644 index 5ecc96187532d4..00000000000000 --- a/x-pack/plugins/siem/public/components/edit_data_provider/index.tsx +++ /dev/null @@ -1,253 +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 { noop } from 'lodash/fp'; -import { - EuiButton, - EuiComboBox, - EuiComboBoxOptionOption, - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiPanel, - EuiSpacer, - EuiToolTip, -} from '@elastic/eui'; -import React, { useEffect, useState, useCallback } from 'react'; -import styled from 'styled-components'; - -import { BrowserFields } from '../../containers/source'; -import { OnDataProviderEdited } from '../timeline/events'; -import { QueryOperator } from '../timeline/data_providers/data_provider'; - -import { - getCategorizedFieldNames, - getExcludedFromSelection, - getQueryOperatorFromSelection, - operatorLabels, - selectionsAreValid, -} from './helpers'; - -import * as i18n from './translations'; - -const EDIT_DATA_PROVIDER_WIDTH = 400; -const FIELD_COMBO_BOX_WIDTH = 195; -const OPERATOR_COMBO_BOX_WIDTH = 160; -const SAVE_CLASS_NAME = 'edit-data-provider-save'; -const VALUE_INPUT_CLASS_NAME = 'edit-data-provider-value'; - -export const HeaderContainer = styled.div` - width: ${EDIT_DATA_PROVIDER_WIDTH}; -`; - -HeaderContainer.displayName = 'HeaderContainer'; - -interface Props { - andProviderId?: string; - browserFields: BrowserFields; - field: string; - isExcluded: boolean; - onDataProviderEdited: OnDataProviderEdited; - operator: QueryOperator; - providerId: string; - timelineId: string; - value: string | number; -} - -const sanatizeValue = (value: string | number): string => - Array.isArray(value) ? `${value[0]}` : `${value}`; // fun fact: value should never be an array - -export const getInitialOperatorLabel = ( - isExcluded: boolean, - operator: QueryOperator -): EuiComboBoxOptionOption[] => { - if (operator === ':') { - return isExcluded ? [{ label: i18n.IS_NOT }] : [{ label: i18n.IS }]; - } else { - return isExcluded ? [{ label: i18n.DOES_NOT_EXIST }] : [{ label: i18n.EXISTS }]; - } -}; - -export const StatefulEditDataProvider = React.memo( - ({ - andProviderId, - browserFields, - field, - isExcluded, - onDataProviderEdited, - operator, - providerId, - timelineId, - value, - }) => { - const [updatedField, setUpdatedField] = useState([{ label: field }]); - const [updatedOperator, setUpdatedOperator] = useState( - getInitialOperatorLabel(isExcluded, operator) - ); - const [updatedValue, setUpdatedValue] = useState(value); - - /** Focuses the Value input if it is visible, falling back to the Save button if it's not */ - const focusInput = () => { - const elements = document.getElementsByClassName(VALUE_INPUT_CLASS_NAME); - - if (elements.length > 0) { - (elements[0] as HTMLElement).focus(); // this cast is required because focus() does not exist on every `Element` returned by `getElementsByClassName` - } else { - const saveElements = document.getElementsByClassName(SAVE_CLASS_NAME); - - if (saveElements.length > 0) { - (saveElements[0] as HTMLElement).focus(); - } - } - }; - - const onFieldSelected = useCallback((selectedField: EuiComboBoxOptionOption[]) => { - setUpdatedField(selectedField); - - focusInput(); - }, []); - - const onOperatorSelected = useCallback((operatorSelected: EuiComboBoxOptionOption[]) => { - setUpdatedOperator(operatorSelected); - - focusInput(); - }, []); - - const onValueChange = useCallback((e: React.ChangeEvent) => { - setUpdatedValue(e.target.value); - }, []); - - const disableScrolling = () => { - const x = - window.pageXOffset !== undefined - ? window.pageXOffset - : (document.documentElement || document.body.parentNode || document.body).scrollLeft; - - const y = - window.pageYOffset !== undefined - ? window.pageYOffset - : (document.documentElement || document.body.parentNode || document.body).scrollTop; - - window.onscroll = () => window.scrollTo(x, y); - }; - - const enableScrolling = () => { - window.onscroll = () => noop; - }; - - useEffect(() => { - disableScrolling(); - focusInput(); - return () => { - enableScrolling(); - }; - }, []); - - return ( - - - - - - - 0 ? updatedField[0].label : null}> - - - - - - - - - - - - - - - - - - {updatedOperator.length > 0 && - updatedOperator[0].label !== i18n.EXISTS && - updatedOperator[0].label !== i18n.DOES_NOT_EXIST ? ( - - - - - - ) : null} - - - - - - - - - { - onDataProviderEdited({ - andProviderId, - excluded: getExcludedFromSelection(updatedOperator), - field: updatedField.length > 0 ? updatedField[0].label : '', - id: timelineId, - operator: getQueryOperatorFromSelection(updatedOperator), - providerId, - value: updatedValue, - }); - }} - size="s" - > - {i18n.SAVE} - - - - - - - ); - } -); - -StatefulEditDataProvider.displayName = 'StatefulEditDataProvider'; diff --git a/x-pack/plugins/siem/public/components/embeddables/__mocks__/mock.ts b/x-pack/plugins/siem/public/components/embeddables/__mocks__/mock.ts deleted file mode 100644 index 19ad0d452feb17..00000000000000 --- a/x-pack/plugins/siem/public/components/embeddables/__mocks__/mock.ts +++ /dev/null @@ -1,477 +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 { IndexPatternMapping } from '../types'; -import { IndexPatternSavedObject } from '../../../hooks/types'; - -export const mockIndexPatternIds: IndexPatternMapping[] = [ - { title: 'filebeat-*', id: '8c7323ac-97ad-4b53-ac0a-40f8f691a918' }, -]; - -export const mockAPMIndexPatternIds: IndexPatternMapping[] = [ - { title: 'apm-*', id: '8c7323ac-97ad-4b53-ac0a-40f8f691a918' }, -]; - -export const mockSourceLayer = { - sourceDescriptor: { - id: 'uuid.v4()', - type: 'ES_SEARCH', - applyGlobalQuery: true, - geoField: 'source.geo.location', - filterByMapBounds: false, - tooltipProperties: [ - 'host.name', - 'source.ip', - 'source.domain', - 'source.geo.country_iso_code', - 'source.as.organization.name', - ], - useTopHits: false, - topHitsTimeField: '@timestamp', - topHitsSize: 1, - indexPatternId: '8c7323ac-97ad-4b53-ac0a-40f8f691a918', - }, - style: { - type: 'VECTOR', - properties: { - fillColor: { - type: 'STATIC', - options: { color: '#6092C0' }, - }, - lineColor: { - type: 'STATIC', - options: { color: '#FFFFFF' }, - }, - lineWidth: { type: 'STATIC', options: { size: 2 } }, - iconSize: { type: 'STATIC', options: { size: 8 } }, - iconOrientation: { - type: 'STATIC', - options: { orientation: 0 }, - }, - symbolizeAs: { - options: { value: 'icon' }, - }, - icon: { - type: 'STATIC', - options: { value: 'home' }, - }, - }, - }, - id: 'uuid.v4()', - label: `filebeat-* | Source Point`, - minZoom: 0, - maxZoom: 24, - alpha: 1, - visible: true, - type: 'VECTOR', - query: { query: '', language: 'kuery' }, - joins: [], -}; - -export const mockDestinationLayer = { - sourceDescriptor: { - id: 'uuid.v4()', - type: 'ES_SEARCH', - applyGlobalQuery: true, - geoField: 'destination.geo.location', - filterByMapBounds: true, - tooltipProperties: [ - 'host.name', - 'destination.ip', - 'destination.domain', - 'destination.geo.country_iso_code', - 'destination.as.organization.name', - ], - useTopHits: false, - topHitsTimeField: '@timestamp', - topHitsSize: 1, - indexPatternId: '8c7323ac-97ad-4b53-ac0a-40f8f691a918', - }, - style: { - type: 'VECTOR', - properties: { - fillColor: { - type: 'STATIC', - options: { color: '#D36086' }, - }, - lineColor: { - type: 'STATIC', - options: { color: '#FFFFFF' }, - }, - lineWidth: { type: 'STATIC', options: { size: 2 } }, - iconSize: { type: 'STATIC', options: { size: 8 } }, - iconOrientation: { - type: 'STATIC', - options: { orientation: 0 }, - }, - symbolizeAs: { - options: { value: 'icon' }, - }, - icon: { - type: 'STATIC', - options: { value: 'marker' }, - }, - }, - }, - id: 'uuid.v4()', - label: `filebeat-* | Destination Point`, - minZoom: 0, - maxZoom: 24, - alpha: 1, - visible: true, - type: 'VECTOR', - query: { query: '', language: 'kuery' }, -}; - -export const mockClientLayer = { - sourceDescriptor: { - id: 'uuid.v4()', - type: 'ES_SEARCH', - applyGlobalQuery: true, - geoField: 'client.geo.location', - filterByMapBounds: false, - tooltipProperties: [ - 'host.name', - 'client.ip', - 'client.domain', - 'client.geo.country_iso_code', - 'client.as.organization.name', - ], - useTopHits: false, - topHitsTimeField: '@timestamp', - topHitsSize: 1, - indexPatternId: '8c7323ac-97ad-4b53-ac0a-40f8f691a918', - }, - style: { - type: 'VECTOR', - properties: { - fillColor: { - type: 'STATIC', - options: { color: '#6092C0' }, - }, - lineColor: { - type: 'STATIC', - options: { color: '#FFFFFF' }, - }, - lineWidth: { type: 'STATIC', options: { size: 2 } }, - iconSize: { type: 'STATIC', options: { size: 8 } }, - iconOrientation: { - type: 'STATIC', - options: { orientation: 0 }, - }, - symbolizeAs: { - options: { value: 'icon' }, - }, - icon: { - type: 'STATIC', - options: { value: 'home' }, - }, - }, - }, - id: 'uuid.v4()', - label: `apm-* | Client Point`, - minZoom: 0, - maxZoom: 24, - alpha: 1, - visible: true, - type: 'VECTOR', - query: { query: '', language: 'kuery' }, - joins: [], -}; - -export const mockServerLayer = { - sourceDescriptor: { - id: 'uuid.v4()', - type: 'ES_SEARCH', - applyGlobalQuery: true, - geoField: 'server.geo.location', - filterByMapBounds: true, - tooltipProperties: [ - 'host.name', - 'server.ip', - 'server.domain', - 'server.geo.country_iso_code', - 'server.as.organization.name', - ], - useTopHits: false, - topHitsTimeField: '@timestamp', - topHitsSize: 1, - indexPatternId: '8c7323ac-97ad-4b53-ac0a-40f8f691a918', - }, - style: { - type: 'VECTOR', - properties: { - fillColor: { - type: 'STATIC', - options: { color: '#D36086' }, - }, - lineColor: { - type: 'STATIC', - options: { color: '#FFFFFF' }, - }, - lineWidth: { type: 'STATIC', options: { size: 2 } }, - iconSize: { type: 'STATIC', options: { size: 8 } }, - iconOrientation: { - type: 'STATIC', - options: { orientation: 0 }, - }, - symbolizeAs: { - options: { value: 'icon' }, - }, - icon: { - type: 'STATIC', - options: { value: 'marker' }, - }, - }, - }, - id: 'uuid.v4()', - label: `apm-* | Server Point`, - minZoom: 0, - maxZoom: 24, - alpha: 1, - visible: true, - type: 'VECTOR', - query: { query: '', language: 'kuery' }, -}; - -export const mockLineLayer = { - sourceDescriptor: { - type: 'ES_PEW_PEW', - applyGlobalQuery: true, - id: 'uuid.v4()', - indexPatternId: '8c7323ac-97ad-4b53-ac0a-40f8f691a918', - sourceGeoField: 'source.geo.location', - destGeoField: 'destination.geo.location', - metrics: [ - { type: 'sum', field: 'source.bytes', label: 'source.bytes' }, - { type: 'sum', field: 'destination.bytes', label: 'destination.bytes' }, - ], - }, - style: { - type: 'VECTOR', - properties: { - fillColor: { - type: 'STATIC', - options: { color: '#1EA593' }, - }, - lineColor: { - type: 'STATIC', - options: { color: '#6092C0' }, - }, - lineWidth: { - type: 'DYNAMIC', - options: { - field: { - label: 'count', - name: 'doc_count', - origin: 'source', - }, - minSize: 1, - maxSize: 8, - fieldMetaOptions: { - isEnabled: true, - sigma: 3, - }, - }, - }, - iconSize: { type: 'STATIC', options: { size: 10 } }, - iconOrientation: { - type: 'STATIC', - options: { orientation: 0 }, - }, - symbolizeAs: { - options: { value: 'icon' }, - }, - icon: { - type: 'STATIC', - options: { value: 'airfield' }, - }, - }, - }, - id: 'uuid.v4()', - label: `filebeat-* | Line`, - minZoom: 0, - maxZoom: 24, - alpha: 0.5, - visible: true, - type: 'VECTOR', - query: { query: '', language: 'kuery' }, -}; - -export const mockClientServerLineLayer = { - sourceDescriptor: { - type: 'ES_PEW_PEW', - applyGlobalQuery: true, - id: 'uuid.v4()', - indexPatternId: '8c7323ac-97ad-4b53-ac0a-40f8f691a918', - sourceGeoField: 'client.geo.location', - destGeoField: 'server.geo.location', - metrics: [ - { type: 'sum', field: 'client.bytes', label: 'client.bytes' }, - { type: 'sum', field: 'server.bytes', label: 'server.bytes' }, - ], - }, - style: { - type: 'VECTOR', - properties: { - fillColor: { - type: 'STATIC', - options: { color: '#1EA593' }, - }, - lineColor: { - type: 'STATIC', - options: { color: '#6092C0' }, - }, - lineWidth: { - type: 'DYNAMIC', - options: { - field: { - label: 'count', - name: 'doc_count', - origin: 'source', - }, - minSize: 1, - maxSize: 8, - fieldMetaOptions: { - isEnabled: true, - sigma: 3, - }, - }, - }, - iconSize: { type: 'STATIC', options: { size: 10 } }, - iconOrientation: { - type: 'STATIC', - options: { orientation: 0 }, - }, - symbolizeAs: { - options: { value: 'icon' }, - }, - icon: { - type: 'STATIC', - options: { value: 'airfield' }, - }, - }, - }, - id: 'uuid.v4()', - label: `apm-* | Line`, - minZoom: 0, - maxZoom: 24, - alpha: 0.5, - visible: true, - type: 'VECTOR', - query: { query: '', language: 'kuery' }, -}; - -export const mockLayerList = [ - { - sourceDescriptor: { type: 'EMS_TMS', isAutoSelect: true }, - id: 'uuid.v4()', - label: null, - minZoom: 0, - maxZoom: 24, - alpha: 1, - visible: true, - style: null, - type: 'VECTOR_TILE', - }, - mockLineLayer, - mockDestinationLayer, - mockSourceLayer, -]; - -export const mockLayerListDouble = [ - { - sourceDescriptor: { type: 'EMS_TMS', isAutoSelect: true }, - id: 'uuid.v4()', - label: null, - minZoom: 0, - maxZoom: 24, - alpha: 1, - visible: true, - style: null, - type: 'VECTOR_TILE', - }, - mockLineLayer, - mockDestinationLayer, - mockSourceLayer, - mockLineLayer, - mockDestinationLayer, - mockSourceLayer, -]; - -export const mockLayerListMixed = [ - { - sourceDescriptor: { type: 'EMS_TMS', isAutoSelect: true }, - id: 'uuid.v4()', - label: null, - minZoom: 0, - maxZoom: 24, - alpha: 1, - visible: true, - style: null, - type: 'VECTOR_TILE', - }, - mockLineLayer, - mockDestinationLayer, - mockSourceLayer, - mockClientServerLineLayer, - mockServerLayer, - mockClientLayer, -]; - -export const mockAPMIndexPattern: IndexPatternSavedObject = { - id: 'apm-*', - type: 'index-pattern', - _version: 'abc', - attributes: { - title: 'apm-*', - }, -}; - -export const mockAPMRegexIndexPattern: IndexPatternSavedObject = { - id: 'apm-7.*', - type: 'index-pattern', - _version: 'abc', - attributes: { - title: 'apm-7.*', - }, -}; - -export const mockFilebeatIndexPattern: IndexPatternSavedObject = { - id: 'filebeat-*', - type: 'index-pattern', - _version: 'abc', - attributes: { - title: 'filebeat-*', - }, -}; - -export const mockAuditbeatIndexPattern: IndexPatternSavedObject = { - id: 'auditbeat-*', - type: 'index-pattern', - _version: 'abc', - attributes: { - title: 'auditbeat-*', - }, -}; - -export const mockAPMTransactionIndexPattern: IndexPatternSavedObject = { - id: 'apm-*-transaction*', - type: 'index-pattern', - _version: 'abc', - attributes: { - title: 'apm-*-transaction*', - }, -}; - -export const mockGlobIndexPattern: IndexPatternSavedObject = { - id: '*', - type: 'index-pattern', - _version: 'abc', - attributes: { - title: '*', - }, -}; diff --git a/x-pack/plugins/siem/public/components/embeddables/types.ts b/x-pack/plugins/siem/public/components/embeddables/types.ts deleted file mode 100644 index d8e20c7f47b4ed..00000000000000 --- a/x-pack/plugins/siem/public/components/embeddables/types.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { RenderTooltipContentParams } from '../../../../../legacy/plugins/maps/public'; -import { inputsModel } from '../../store/inputs'; - -export interface IndexPatternMapping { - title: string; - id: string; -} - -export interface LayerMappingDetails { - metricField: string; - geoField: string; - tooltipProperties: string[]; - label: string; -} - -export interface LayerMapping { - source: LayerMappingDetails; - destination: LayerMappingDetails; -} - -export interface LayerMappingCollection { - [indexPatternTitle: string]: LayerMapping; -} - -export type SetQuery = (params: { - id: string; - inspect: inputsModel.InspectQuery | null; - loading: boolean; - refetch: inputsModel.Refetch; -}) => void; - -export interface MapFeature { - id: number; - layerId: string; -} - -export interface LoadFeatureProps { - layerId: string; - featureId: number; -} - -export interface FeatureProperty { - _propertyKey: string; - _rawValue: string | string[]; -} - -export interface FeatureGeometry { - coordinates: [number]; - type: string; -} - -export type MapToolTipProps = Partial; diff --git a/x-pack/plugins/siem/public/components/error_toast_dispatcher/index.test.tsx b/x-pack/plugins/siem/public/components/error_toast_dispatcher/index.test.tsx deleted file mode 100644 index 6b90d9ccd08c48..00000000000000 --- a/x-pack/plugins/siem/public/components/error_toast_dispatcher/index.test.tsx +++ /dev/null @@ -1,35 +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 { shallow } from 'enzyme'; -import React from 'react'; -import { Provider } from 'react-redux'; - -import { apolloClientObservable, mockGlobalState } from '../../mock'; -import { createStore } from '../../store/store'; - -import { ErrorToastDispatcher } from '.'; -import { State } from '../../store/reducer'; - -describe('Error Toast Dispatcher', () => { - const state: State = mockGlobalState; - let store = createStore(state, apolloClientObservable); - - beforeEach(() => { - store = createStore(state, apolloClientObservable); - }); - - describe('rendering', () => { - test('it renders', () => { - const wrapper = shallow( - - - - ); - expect(wrapper.find('Connect(ErrorToastDispatcherComponent)')).toMatchSnapshot(); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/event_details/columns.tsx b/x-pack/plugins/siem/public/components/event_details/columns.tsx deleted file mode 100644 index 131a3a63bae309..00000000000000 --- a/x-pack/plugins/siem/public/components/event_details/columns.tsx +++ /dev/null @@ -1,210 +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. - */ - -/* eslint-disable react/display-name */ - -import { - EuiCheckbox, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiPanel, - EuiText, - EuiToolTip, -} from '@elastic/eui'; -import React from 'react'; -import { Draggable } from 'react-beautiful-dnd'; -import styled from 'styled-components'; - -import { BrowserFields } from '../../containers/source'; -import { ToStringArray } from '../../graphql/types'; -import { ColumnHeaderOptions } from '../../store/timeline/model'; -import { DragEffects } from '../drag_and_drop/draggable_wrapper'; -import { DroppableWrapper } from '../drag_and_drop/droppable_wrapper'; -import { getDroppableId, getDraggableFieldId, DRAG_TYPE_FIELD } from '../drag_and_drop/helpers'; -import { DraggableFieldBadge } from '../draggables/field_badge'; -import { FieldName } from '../fields_browser/field_name'; -import { SelectableText } from '../selectable_text'; -import { OverflowField } from '../tables/helpers'; -import { defaultColumnHeaderType } from '../timeline/body/column_headers/default_headers'; -import { DEFAULT_COLUMN_MIN_WIDTH } from '../timeline/body/constants'; -import { MESSAGE_FIELD_NAME } from '../timeline/body/renderers/constants'; -import { FormattedFieldValue } from '../timeline/body/renderers/formatted_field'; -import { OnUpdateColumns } from '../timeline/events'; -import { getIconFromType, getExampleText, getColumnsWithTimestamp } from './helpers'; -import * as i18n from './translations'; -import { EventFieldsData } from './types'; - -const HoverActionsContainer = styled(EuiPanel)` - align-items: center; - display: flex; - flex-direction: row; - height: 25px; - justify-content: center; - left: 5px; - position: absolute; - top: -10px; - width: 30px; -`; - -HoverActionsContainer.displayName = 'HoverActionsContainer'; - -export const getColumns = ({ - browserFields, - columnHeaders, - eventId, - onUpdateColumns, - contextId, - toggleColumn, -}: { - browserFields: BrowserFields; - columnHeaders: ColumnHeaderOptions[]; - eventId: string; - onUpdateColumns: OnUpdateColumns; - contextId: string; - toggleColumn: (column: ColumnHeaderOptions) => void; -}) => [ - { - field: 'field', - name: '', - sortable: false, - truncateText: false, - width: '30px', - render: (field: string) => ( - - c.id === field) !== -1} - data-test-subj={`toggle-field-${field}`} - id={field} - onChange={() => - toggleColumn({ - columnHeaderType: defaultColumnHeaderType, - id: field, - width: DEFAULT_COLUMN_MIN_WIDTH, - }) - } - /> - - ), - }, - { - field: 'field', - name: i18n.FIELD, - sortable: true, - truncateText: false, - render: (field: string, data: EventFieldsData) => ( - - - - - - - - - ( -
- - - -
- )} - > - - {provided => ( -
- -
- )} -
-
-
-
- ), - }, - { - field: 'values', - name: i18n.VALUE, - sortable: true, - truncateText: false, - render: (values: ToStringArray | null | undefined, data: EventFieldsData) => ( - - {values != null && - values.map((value, i) => ( - - {data.field === MESSAGE_FIELD_NAME ? ( - - ) : ( - - )} - - ))} - - ), - }, - { - field: 'description', - name: i18n.DESCRIPTION, - render: (description: string | null | undefined, data: EventFieldsData) => ( - - {`${description || ''} ${getExampleText(data.example)}`} - - ), - sortable: true, - truncateText: true, - width: '50%', - }, - { - field: 'valuesConcatenated', - name: i18n.BLANK, - render: () => null, - sortable: false, - truncateText: true, - width: '1px', - }, -]; diff --git a/x-pack/plugins/siem/public/components/event_details/helpers.tsx b/x-pack/plugins/siem/public/components/event_details/helpers.tsx deleted file mode 100644 index 5d9c9d82490bbb..00000000000000 --- a/x-pack/plugins/siem/public/components/event_details/helpers.tsx +++ /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 { get, getOr, isEmpty, uniqBy } from 'lodash/fp'; - -import { BrowserField, BrowserFields } from '../../containers/source'; -import { ColumnHeaderOptions } from '../../store/timeline/model'; -import { - DEFAULT_DATE_COLUMN_MIN_WIDTH, - DEFAULT_COLUMN_MIN_WIDTH, -} from '../timeline/body/constants'; -import { ToStringArray } from '../../graphql/types'; - -import * as i18n from './translations'; - -/** - * Defines the behavior of the search input that appears above the table of data - */ -export const search = { - box: { - incremental: true, - placeholder: i18n.PLACEHOLDER, - schema: true, - }, -}; - -export interface ItemValues { - value: JSX.Element; - valueAsString: string; -} - -/** - * An item rendered in the table - */ -export interface Item { - description: string; - field: JSX.Element; - fieldId: string; - type: string; - values: ToStringArray; -} - -export const getColumnHeaderFromBrowserField = ({ - browserField, - width = DEFAULT_COLUMN_MIN_WIDTH, -}: { - browserField: Partial; - width?: number; -}): ColumnHeaderOptions => ({ - category: browserField.category, - columnHeaderType: 'not-filtered', - description: browserField.description != null ? browserField.description : undefined, - example: browserField.example != null ? `${browserField.example}` : undefined, - id: browserField.name || '', - type: browserField.type, - aggregatable: browserField.aggregatable, - width, -}); - -/** - * Returns a collection of columns, where the first column in the collection - * is a timestamp, and the remaining columns are all the columns in the - * specified category - */ -export const getColumnsWithTimestamp = ({ - browserFields, - category, -}: { - browserFields: BrowserFields; - category: string; -}): ColumnHeaderOptions[] => { - const emptyFields: Record> = {}; - const timestamp = get('base.fields.@timestamp', browserFields); - const categoryFields: Array> = [ - ...Object.values(getOr(emptyFields, `${category}.fields`, browserFields)), - ]; - - return timestamp != null && categoryFields.length - ? uniqBy('id', [ - getColumnHeaderFromBrowserField({ - browserField: timestamp, - width: DEFAULT_DATE_COLUMN_MIN_WIDTH, - }), - ...categoryFields.map(f => getColumnHeaderFromBrowserField({ browserField: f })), - ]) - : []; -}; - -/** Returns example text, or an empty string if the field does not have an example */ -export const getExampleText = (example: string | number | null | undefined): string => - !isEmpty(example) ? `Example: ${example}` : ''; - -export const getIconFromType = (type: string | null) => { - switch (type) { - case 'string': // fall through - case 'keyword': - return 'string'; - case 'number': // fall through - case 'long': - return 'number'; - case 'date': - return 'clock'; - case 'ip': - return 'globe'; - case 'object': - return 'questionInCircle'; - case 'float': - return 'number'; - default: - return 'questionInCircle'; - } -}; diff --git a/x-pack/plugins/siem/public/components/event_details/types.ts b/x-pack/plugins/siem/public/components/event_details/types.ts deleted file mode 100644 index 4e351fcdf98e4c..00000000000000 --- a/x-pack/plugins/siem/public/components/event_details/types.ts +++ /dev/null @@ -1,10 +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 { BrowserField } from '../../containers/source'; -import { DetailItem } from '../../graphql/types'; - -export type EventFieldsData = BrowserField & DetailItem; diff --git a/x-pack/plugins/siem/public/components/events_viewer/default_model.tsx b/x-pack/plugins/siem/public/components/events_viewer/default_model.tsx deleted file mode 100644 index 59a9f6d061c8d4..00000000000000 --- a/x-pack/plugins/siem/public/components/events_viewer/default_model.tsx +++ /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. - */ - -import { defaultHeaders } from './default_headers'; -import { SubsetTimelineModel } from '../../store/timeline/model'; -import { timelineDefaults } from '../../store/timeline/defaults'; - -export const eventsDefaultModel: SubsetTimelineModel = { - ...timelineDefaults, - columns: defaultHeaders, -}; diff --git a/x-pack/plugins/siem/public/components/events_viewer/index.test.tsx b/x-pack/plugins/siem/public/components/events_viewer/index.test.tsx deleted file mode 100644 index 6f614c1e32f651..00000000000000 --- a/x-pack/plugins/siem/public/components/events_viewer/index.test.tsx +++ /dev/null @@ -1,85 +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 { MockedProvider } from 'react-apollo/test-utils'; -import useResizeObserver from 'use-resize-observer/polyfilled'; - -import { wait } from '../../lib/helpers'; -import { mockIndexPattern, TestProviders } from '../../mock'; -import { useMountAppended } from '../../utils/use_mount_appended'; - -import { mockEventViewerResponse } from './mock'; -import { StatefulEventsViewer } from '.'; -import { useFetchIndexPatterns } from '../../containers/detection_engine/rules/fetch_index_patterns'; -import { mockBrowserFields } from '../../containers/source/mock'; -import { eventsDefaultModel } from './default_model'; - -const mockUseFetchIndexPatterns: jest.Mock = useFetchIndexPatterns as jest.Mock; -jest.mock('../../containers/detection_engine/rules/fetch_index_patterns'); -mockUseFetchIndexPatterns.mockImplementation(() => [ - { - browserFields: mockBrowserFields, - indexPatterns: mockIndexPattern, - }, -]); - -const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; -jest.mock('use-resize-observer/polyfilled'); -mockUseResizeObserver.mockImplementation(() => ({})); - -const from = 1566943856794; -const to = 1566857456791; - -describe('StatefulEventsViewer', () => { - const mount = useMountAppended(); - - test('it renders the events viewer', async () => { - const wrapper = mount( - - - - - - ); - - await wait(); - wrapper.update(); - - expect( - wrapper - .find('[data-test-subj="events-viewer-panel"]') - .first() - .exists() - ).toBe(true); - }); - - // InspectButtonContainer controls displaying InspectButton components - test('it renders InspectButtonContainer', async () => { - const wrapper = mount( - - - - - - ); - - await wait(); - wrapper.update(); - - expect(wrapper.find(`InspectButtonContainer`).exists()).toBe(true); - }); -}); diff --git a/x-pack/plugins/siem/public/components/events_viewer/index.tsx b/x-pack/plugins/siem/public/components/events_viewer/index.tsx deleted file mode 100644 index bc6a1b3b77bfa9..00000000000000 --- a/x-pack/plugins/siem/public/components/events_viewer/index.tsx +++ /dev/null @@ -1,218 +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, { useCallback, useMemo, useEffect } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import deepEqual from 'fast-deep-equal'; - -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; -import { inputsModel, inputsSelectors, State, timelineSelectors } from '../../store'; -import { inputsActions, timelineActions } from '../../store/actions'; -import { - ColumnHeaderOptions, - SubsetTimelineModel, - TimelineModel, -} from '../../store/timeline/model'; -import { OnChangeItemsPerPage } from '../timeline/events'; -import { Filter } from '../../../../../../src/plugins/data/public'; -import { useUiSetting } from '../../lib/kibana'; -import { EventsViewer } from './events_viewer'; -import { useFetchIndexPatterns } from '../../containers/detection_engine/rules/fetch_index_patterns'; -import { TimelineTypeContextProps } from '../timeline/timeline_context'; -import { InspectButtonContainer } from '../inspect'; -import * as i18n from './translations'; - -export interface OwnProps { - defaultIndices?: string[]; - defaultModel: SubsetTimelineModel; - end: number; - id: string; - start: number; - headerFilterGroup?: React.ReactNode; - pageFilters?: Filter[]; - timelineTypeContext?: TimelineTypeContextProps; - utilityBar?: (refetch: inputsModel.Refetch, totalCount: number) => React.ReactNode; -} - -type Props = OwnProps & PropsFromRedux; - -const defaultTimelineTypeContext = { - loadingText: i18n.LOADING_EVENTS, -}; - -const StatefulEventsViewerComponent: React.FC = ({ - createTimeline, - columns, - dataProviders, - deletedEventIds, - defaultIndices, - deleteEventQuery, - end, - filters, - headerFilterGroup, - id, - isLive, - itemsPerPage, - itemsPerPageOptions, - kqlMode, - pageFilters, - query, - removeColumn, - start, - showCheckboxes, - showRowRenderers, - sort, - timelineTypeContext = defaultTimelineTypeContext, - updateItemsPerPage, - upsertColumn, - utilityBar, -}) => { - const [{ browserFields, indexPatterns }] = useFetchIndexPatterns( - defaultIndices ?? useUiSetting(DEFAULT_INDEX_KEY) - ); - - useEffect(() => { - if (createTimeline != null) { - createTimeline({ id, columns, sort, itemsPerPage, showCheckboxes, showRowRenderers }); - } - return () => { - deleteEventQuery({ id, inputId: 'global' }); - }; - }, []); - - const onChangeItemsPerPage: OnChangeItemsPerPage = useCallback( - itemsChangedPerPage => updateItemsPerPage({ id, itemsPerPage: itemsChangedPerPage }), - [id, updateItemsPerPage] - ); - - const toggleColumn = useCallback( - (column: ColumnHeaderOptions) => { - const exists = columns.findIndex(c => c.id === column.id) !== -1; - - if (!exists && upsertColumn != null) { - upsertColumn({ - column, - id, - index: 1, - }); - } - - if (exists && removeColumn != null) { - removeColumn({ - columnId: column.id, - id, - }); - } - }, - [columns, id, upsertColumn, removeColumn] - ); - - const globalFilters = useMemo(() => [...filters, ...(pageFilters ?? [])], [filters, pageFilters]); - - return ( - - - - ); -}; - -const makeMapStateToProps = () => { - const getInputsTimeline = inputsSelectors.getTimelineSelector(); - const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); - const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); - const getEvents = timelineSelectors.getEventsByIdSelector(); - const mapStateToProps = (state: State, { id, defaultModel }: OwnProps) => { - const input: inputsModel.InputsRange = getInputsTimeline(state); - const events: TimelineModel = getEvents(state, id) ?? defaultModel; - const { - columns, - dataProviders, - deletedEventIds, - itemsPerPage, - itemsPerPageOptions, - kqlMode, - sort, - showCheckboxes, - showRowRenderers, - } = events; - - return { - columns, - dataProviders, - deletedEventIds, - filters: getGlobalFiltersQuerySelector(state), - id, - isLive: input.policy.kind === 'interval', - itemsPerPage, - itemsPerPageOptions, - kqlMode, - query: getGlobalQuerySelector(state), - sort, - showCheckboxes, - showRowRenderers, - }; - }; - return mapStateToProps; -}; - -const mapDispatchToProps = { - createTimeline: timelineActions.createTimeline, - deleteEventQuery: inputsActions.deleteOneQuery, - updateItemsPerPage: timelineActions.updateItemsPerPage, - removeColumn: timelineActions.removeColumn, - upsertColumn: timelineActions.upsertColumn, -}; - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const StatefulEventsViewer = connector( - React.memo( - StatefulEventsViewerComponent, - (prevProps, nextProps) => - prevProps.id === nextProps.id && - deepEqual(prevProps.columns, nextProps.columns) && - deepEqual(prevProps.dataProviders, nextProps.dataProviders) && - prevProps.deletedEventIds === nextProps.deletedEventIds && - prevProps.end === nextProps.end && - deepEqual(prevProps.filters, nextProps.filters) && - prevProps.isLive === nextProps.isLive && - prevProps.itemsPerPage === nextProps.itemsPerPage && - deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) && - prevProps.kqlMode === nextProps.kqlMode && - deepEqual(prevProps.query, nextProps.query) && - deepEqual(prevProps.sort, nextProps.sort) && - prevProps.start === nextProps.start && - deepEqual(prevProps.pageFilters, nextProps.pageFilters) && - prevProps.showCheckboxes === nextProps.showCheckboxes && - prevProps.showRowRenderers === nextProps.showRowRenderers && - prevProps.start === nextProps.start && - deepEqual(prevProps.timelineTypeContext, nextProps.timelineTypeContext) && - prevProps.utilityBar === nextProps.utilityBar - ) -); diff --git a/x-pack/plugins/siem/public/components/events_viewer/mock.ts b/x-pack/plugins/siem/public/components/events_viewer/mock.ts deleted file mode 100644 index 352b0b95c6dd4d..00000000000000 --- a/x-pack/plugins/siem/public/components/events_viewer/mock.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 { noop } from 'lodash/fp'; -import { timelineQuery } from '../../containers/timeline/index.gql_query'; - -export const mockEventViewerResponse = [ - { - request: { - query: timelineQuery, - fetchPolicy: 'network-only', - notifyOnNetworkStatusChange: true, - variables: { - fieldRequested: [ - '@timestamp', - 'message', - 'host.name', - 'event.module', - 'event.dataset', - 'event.action', - 'user.name', - 'source.ip', - 'destination.ip', - ], - filterQuery: - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1566943856794}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1566857456791}}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}', - sourceId: 'default', - pagination: { limit: 25, cursor: null, tiebreaker: null }, - sortField: { sortFieldId: '@timestamp', direction: 'desc' }, - defaultIndex: ['filebeat-*', 'auditbeat-*', 'packetbeat-*'], - inspect: false, - }, - }, - result: { - loading: false, - fetchMore: noop, - refetch: noop, - data: { - source: { - id: 'default', - Timeline: { - totalCount: 12, - pageInfo: { - endCursor: null, - hasNextPage: true, - __typename: 'PageInfo', - }, - edges: [], - __typename: 'TimelineData', - }, - __typename: 'Source', - }, - }, - }, - }, -]; diff --git a/x-pack/plugins/siem/public/components/fields_browser/helpers.test.tsx b/x-pack/plugins/siem/public/components/fields_browser/helpers.test.tsx deleted file mode 100644 index db9daacb21fa8f..00000000000000 --- a/x-pack/plugins/siem/public/components/fields_browser/helpers.test.tsx +++ /dev/null @@ -1,376 +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 { mockBrowserFields } from '../../containers/source/mock'; - -import { - categoryHasFields, - createVirtualCategory, - getCategoryPaneCategoryClassName, - getFieldBrowserCategoryTitleClassName, - getFieldBrowserSearchInputClassName, - getFieldCount, - filterBrowserFieldsByFieldName, -} from './helpers'; -import { BrowserFields } from '../../containers/source'; - -const timelineId = 'test'; - -describe('helpers', () => { - describe('getCategoryPaneCategoryClassName', () => { - test('it returns the expected class name', () => { - const categoryId = 'auditd'; - - expect(getCategoryPaneCategoryClassName({ categoryId, timelineId })).toEqual( - 'field-browser-category-pane-auditd-test' - ); - }); - }); - - describe('getFieldBrowserCategoryTitleClassName', () => { - test('it returns the expected class name', () => { - const categoryId = 'auditd'; - - expect(getFieldBrowserCategoryTitleClassName({ categoryId, timelineId })).toEqual( - 'field-browser-category-title-auditd-test' - ); - }); - }); - - describe('getFieldBrowserSearchInputClassName', () => { - test('it returns the expected class name', () => { - expect(getFieldBrowserSearchInputClassName(timelineId)).toEqual( - 'field-browser-search-input-test' - ); - }); - }); - - describe('categoryHasFields', () => { - test('it returns false if the category fields property is undefined', () => { - expect(categoryHasFields({})).toBe(false); - }); - - test('it returns false if the category fields property is empty', () => { - expect(categoryHasFields({ fields: {} })).toBe(false); - }); - - test('it returns true if the category has one field', () => { - expect( - categoryHasFields({ - fields: { - 'auditd.data.a0': { - aggregatable: true, - category: 'auditd', - description: null, - example: null, - format: '', - indexes: ['auditbeat'], - name: 'auditd.data.a0', - searchable: true, - type: 'string', - }, - }, - }) - ).toBe(true); - }); - - test('it returns true if the category has multiple fields', () => { - expect( - categoryHasFields({ - fields: { - 'agent.ephemeral_id': { - aggregatable: true, - category: 'agent', - description: - 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', - example: '8a4f500f', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.ephemeral_id', - searchable: true, - type: 'string', - }, - 'agent.hostname': { - aggregatable: true, - category: 'agent', - description: null, - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.hostname', - searchable: true, - type: 'string', - }, - }, - }) - ).toBe(true); - }); - }); - - describe('getFieldCount', () => { - test('it returns 0 if the category fields property is undefined', () => { - expect(getFieldCount({})).toEqual(0); - }); - - test('it returns 0 if the category fields property is empty', () => { - expect(getFieldCount({ fields: {} })).toEqual(0); - }); - - test('it returns 1 if the category has one field', () => { - expect( - getFieldCount({ - fields: { - 'auditd.data.a0': { - aggregatable: true, - category: 'auditd', - description: null, - example: null, - format: '', - indexes: ['auditbeat'], - name: 'auditd.data.a0', - searchable: true, - type: 'string', - }, - }, - }) - ).toEqual(1); - }); - - test('it returns the correct count when category has multiple fields', () => { - expect( - getFieldCount({ - fields: { - 'agent.ephemeral_id': { - aggregatable: true, - category: 'agent', - description: - 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', - example: '8a4f500f', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.ephemeral_id', - searchable: true, - type: 'string', - }, - 'agent.hostname': { - aggregatable: true, - category: 'agent', - description: null, - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.hostname', - searchable: true, - type: 'string', - }, - }, - }) - ).toEqual(2); - }); - }); - - describe('filterBrowserFieldsByFieldName', () => { - test('it returns an empty collection when browserFields is empty', () => { - expect(filterBrowserFieldsByFieldName({ browserFields: {}, substring: '' })).toEqual({}); - }); - - test('it returns an empty collection when browserFields is empty and substring is non empty', () => { - expect( - filterBrowserFieldsByFieldName({ browserFields: {}, substring: 'nothing to match' }) - ).toEqual({}); - }); - - test('it returns an empty collection when browserFields is NOT empty and substring does not match any fields', () => { - expect( - filterBrowserFieldsByFieldName({ - browserFields: mockBrowserFields, - substring: 'nothing to match', - }) - ).toEqual({}); - }); - - test('it returns the original collection when browserFields is NOT empty and substring is empty', () => { - expect( - filterBrowserFieldsByFieldName({ - browserFields: mockBrowserFields, - substring: '', - }) - ).toEqual(mockBrowserFields); - }); - - test('it returns (only) non-empty categories, where each category contains only the fields matching the substring', () => { - const filtered: BrowserFields = { - agent: { - fields: { - 'agent.ephemeral_id': { - aggregatable: true, - category: 'agent', - description: - 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', - example: '8a4f500f', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.ephemeral_id', - searchable: true, - type: 'string', - }, - 'agent.id': { - aggregatable: true, - category: 'agent', - description: - 'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.', - example: '8a4f500d', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.id', - searchable: true, - type: 'string', - }, - }, - }, - cloud: { - fields: { - 'cloud.account.id': { - aggregatable: true, - category: 'cloud', - description: - 'The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.', - example: '666777888999', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'cloud.account.id', - searchable: true, - type: 'string', - }, - }, - }, - container: { - fields: { - 'container.id': { - aggregatable: true, - category: 'container', - description: 'Unique container id.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'container.id', - searchable: true, - type: 'string', - }, - }, - }, - }; - - expect( - filterBrowserFieldsByFieldName({ - browserFields: mockBrowserFields, - substring: 'id', - }) - ).toEqual(filtered); - }); - }); - - describe('createVirtualCategory', () => { - test('it combines the specified fields into a virtual category when the input ONLY contains field names that contain dots (e.g. agent.hostname)', () => { - const expectedMatchingFields = { - fields: { - 'agent.hostname': { - aggregatable: true, - category: 'agent', - description: null, - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.hostname', - searchable: true, - type: 'string', - }, - 'client.domain': { - aggregatable: true, - category: 'client', - description: 'Client domain.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.domain', - searchable: true, - type: 'string', - }, - 'client.geo.country_iso_code': { - aggregatable: true, - category: 'client', - description: 'Country ISO code.', - example: 'CA', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.geo.country_iso_code', - searchable: true, - type: 'string', - }, - }, - }; - - const fieldIds = ['agent.hostname', 'client.domain', 'client.geo.country_iso_code']; - - expect( - createVirtualCategory({ - browserFields: mockBrowserFields, - fieldIds, - }) - ).toEqual(expectedMatchingFields); - }); - - test('it combines the specified fields into a virtual category when the input includes field names from the base category that do NOT contain dots (e.g. @timestamp)', () => { - const expectedMatchingFields = { - fields: { - 'agent.hostname': { - aggregatable: true, - category: 'agent', - description: null, - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.hostname', - searchable: true, - type: 'string', - }, - '@timestamp': { - aggregatable: true, - category: 'base', - description: - 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', - example: '2016-05-23T08:05:34.853Z', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: '@timestamp', - searchable: true, - type: 'date', - }, - 'client.domain': { - aggregatable: true, - category: 'client', - description: 'Client domain.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.domain', - searchable: true, - type: 'string', - }, - }, - }; - - const fieldIds = ['agent.hostname', '@timestamp', 'client.domain']; - - expect( - createVirtualCategory({ - browserFields: mockBrowserFields, - fieldIds, - }) - ).toEqual(expectedMatchingFields); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/fields_browser/helpers.tsx b/x-pack/plugins/siem/public/components/fields_browser/helpers.tsx deleted file mode 100644 index e198d802d8a2ef..00000000000000 --- a/x-pack/plugins/siem/public/components/fields_browser/helpers.tsx +++ /dev/null @@ -1,143 +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 { EuiLoadingSpinner } from '@elastic/eui'; -import { filter, get, pickBy } from 'lodash/fp'; -import styled from 'styled-components'; - -import { BrowserField, BrowserFields } from '../../containers/source'; -import { - DEFAULT_CATEGORY_NAME, - defaultHeaders, -} from '../timeline/body/column_headers/default_headers'; - -export const LoadingSpinner = styled(EuiLoadingSpinner)` - cursor: pointer; - position: relative; - top: 3px; -`; - -LoadingSpinner.displayName = 'LoadingSpinner'; - -export const CATEGORY_PANE_WIDTH = 200; -export const DESCRIPTION_COLUMN_WIDTH = 300; -export const FIELD_COLUMN_WIDTH = 200; -export const FIELD_BROWSER_WIDTH = 900; -export const FIELD_BROWSER_HEIGHT = 300; -export const FIELDS_PANE_WIDTH = 670; -export const HEADER_HEIGHT = 40; -export const PANES_FLEX_GROUP_WIDTH = CATEGORY_PANE_WIDTH + FIELDS_PANE_WIDTH + 10; -export const SEARCH_INPUT_WIDTH = 850; -export const TABLE_HEIGHT = 260; -export const TYPE_COLUMN_WIDTH = 50; - -/** - * Returns the CSS class name for the title of a category shown in the left - * side field browser - */ -export const getCategoryPaneCategoryClassName = ({ - categoryId, - timelineId, -}: { - categoryId: string; - timelineId: string; -}): string => `field-browser-category-pane-${categoryId}-${timelineId}`; - -/** - * Returns the CSS class name for the title of a category shown in the right - * side of field browser - */ -export const getFieldBrowserCategoryTitleClassName = ({ - categoryId, - timelineId, -}: { - categoryId: string; - timelineId: string; -}): string => `field-browser-category-title-${categoryId}-${timelineId}`; - -/** Returns the class name for a field browser search input */ -export const getFieldBrowserSearchInputClassName = (timelineId: string): string => - `field-browser-search-input-${timelineId}`; - -/** Returns true if the specified category has at least one field */ -export const categoryHasFields = (category: Partial): boolean => - category.fields != null && Object.keys(category.fields).length > 0; - -/** Returns the count of fields in the specified category */ -export const getFieldCount = (category: Partial | undefined): number => - category != null && category.fields != null ? Object.keys(category.fields).length : 0; - -/** - * Filters the specified `BrowserFields` to return a new collection where every - * category contains at least one field name that matches the specified substring. - */ -export const filterBrowserFieldsByFieldName = ({ - browserFields, - substring, -}: { - browserFields: BrowserFields; - substring: string; -}): BrowserFields => { - const trimmedSubstring = substring.trim(); - - // filter each category such that it only contains fields with field names - // that contain the specified substring: - const filteredBrowserFields: BrowserFields = Object.keys(browserFields).reduce( - (filteredCategories, categoryId) => ({ - ...filteredCategories, - [categoryId]: { - ...browserFields[categoryId], - fields: filter( - f => f.name != null && f.name.includes(trimmedSubstring), - browserFields[categoryId].fields - ).reduce((filtered, field) => ({ ...filtered, [field.name!]: field }), {}), - }, - }), - {} - ); - - // only pick non-empty categories from the filtered browser fields - const nonEmptyCategories: BrowserFields = pickBy( - category => categoryHasFields(category), - filteredBrowserFields - ); - - return nonEmptyCategories; -}; - -/** - * Returns a "virtual" category (e.g. default ECS) from the specified fieldIds - */ -export const createVirtualCategory = ({ - browserFields, - fieldIds, -}: { - browserFields: BrowserFields; - fieldIds: string[]; -}): Partial => ({ - fields: fieldIds.reduce>>>((fields, fieldId) => { - const splitId = fieldId.split('.'); // source.geo.city_name -> [source, geo, city_name] - - return { - ...fields, - [fieldId]: { - ...get([splitId.length > 1 ? splitId[0] : 'base', 'fields', fieldId], browserFields), - name: fieldId, - }, - }; - }, {}), -}); - -/** Merges the specified browser fields with the default category (i.e. `default ECS`) */ -export const mergeBrowserFieldsWithDefaultCategory = ( - browserFields: BrowserFields -): BrowserFields => ({ - ...browserFields, - [DEFAULT_CATEGORY_NAME]: createVirtualCategory({ - browserFields, - fieldIds: defaultHeaders.map(header => header.id), - }), -}); diff --git a/x-pack/plugins/siem/public/components/fields_browser/index.test.tsx b/x-pack/plugins/siem/public/components/fields_browser/index.test.tsx deleted file mode 100644 index 9e513b890e722a..00000000000000 --- a/x-pack/plugins/siem/public/components/fields_browser/index.test.tsx +++ /dev/null @@ -1,275 +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 { mount } from 'enzyme'; -import React from 'react'; -import { ActionCreator } from 'typescript-fsa'; - -import { mockBrowserFields } from '../../containers/source/mock'; -import { TestProviders } from '../../mock'; -import { ColumnHeaderOptions } from '../../store/timeline/model'; - -import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from './helpers'; - -import { StatefulFieldsBrowserComponent } from '.'; - -// Suppress warnings about "react-beautiful-dnd" until we migrate to @testing-library/react -/* eslint-disable no-console */ -const originalError = console.error; -const originalWarn = console.warn; -beforeAll(() => { - console.warn = jest.fn(); - console.error = jest.fn(); -}); -afterAll(() => { - console.error = originalError; - console.warn = originalWarn; -}); - -const removeColumnMock = (jest.fn() as unknown) as ActionCreator<{ - id: string; - columnId: string; -}>; - -const upsertColumnMock = (jest.fn() as unknown) as ActionCreator<{ - column: ColumnHeaderOptions; - id: string; - index: number; -}>; - -describe('StatefulFieldsBrowser', () => { - const timelineId = 'test'; - - test('it renders the Fields button, which displays the fields browser on click', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="show-field-browser"]') - .first() - .text() - ).toEqual('Columns'); - }); - - describe('toggleShow', () => { - test('it does NOT render the fields browser until the Fields button is clicked', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="fields-browser-container"]').exists()).toBe(false); - }); - - test('it renders the fields browser when the Fields button is clicked', () => { - const wrapper = mount( - - - - ); - - wrapper - .find('[data-test-subj="show-field-browser"]') - .first() - .simulate('click'); - - expect(wrapper.find('[data-test-subj="fields-browser-container"]').exists()).toBe(true); - }); - }); - - describe('updateSelectedCategoryId', () => { - beforeEach(() => { - jest.useFakeTimers(); - }); - test('it updates the selectedCategoryId state, which makes the category bold, when the user clicks a category name in the left hand side of the field browser', () => { - const wrapper = mount( - - - - ); - - wrapper - .find('[data-test-subj="show-field-browser"]') - .first() - .simulate('click'); - - wrapper - .find(`.field-browser-category-pane-auditd-${timelineId}`) - .first() - .simulate('click'); - - wrapper.update(); - expect( - wrapper.find(`.field-browser-category-pane-auditd-${timelineId}`).first() - ).toHaveStyleRule('font-weight', 'bold', { modifier: '.euiText' }); - }); - - test('it updates the selectedCategoryId state according to most fields returned', () => { - const wrapper = mount( - - - - ); - - wrapper - .find('[data-test-subj="show-field-browser"]') - .first() - .simulate('click'); - expect( - wrapper.find(`.field-browser-category-pane-cloud-${timelineId}`).first() - ).toHaveStyleRule('font-weight', 'normal', { modifier: '.euiText' }); - wrapper - .find('[data-test-subj="field-search"]') - .last() - .simulate('change', { target: { value: 'cloud' } }); - - jest.runOnlyPendingTimers(); - wrapper.update(); - expect( - wrapper.find(`.field-browser-category-pane-cloud-${timelineId}`).first() - ).toHaveStyleRule('font-weight', 'bold', { modifier: '.euiText' }); - }); - }); - - test('it renders the Fields Browser button as a settings gear when the isEventViewer prop is true', () => { - const isEventViewer = true; - - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="show-field-browser-gear"]') - .first() - .exists() - ).toBe(true); - }); - - test('it does NOT render the Fields Browser button as a settings gear when the isEventViewer prop is false', () => { - const isEventViewer = false; - - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="show-field-browser-gear"]') - .first() - .exists() - ).toBe(false); - }); - - test('it does NOT render the default Fields Browser button when the isEventViewer prop is true', () => { - const isEventViewer = true; - - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="show-field-browser"]') - .first() - .exists() - ).toBe(false); - }); -}); diff --git a/x-pack/plugins/siem/public/components/fields_browser/index.tsx b/x-pack/plugins/siem/public/components/fields_browser/index.tsx deleted file mode 100644 index 3e19ba383b4ecb..00000000000000 --- a/x-pack/plugins/siem/public/components/fields_browser/index.tsx +++ /dev/null @@ -1,211 +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 { EuiButtonEmpty, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import { noop } from 'lodash/fp'; -import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import styled from 'styled-components'; - -import { BrowserFields } from '../../containers/source'; -import { timelineActions } from '../../store/actions'; -import { ColumnHeaderOptions } from '../../store/timeline/model'; -import { DEFAULT_CATEGORY_NAME } from '../timeline/body/column_headers/default_headers'; -import { FieldsBrowser } from './field_browser'; -import { filterBrowserFieldsByFieldName, mergeBrowserFieldsWithDefaultCategory } from './helpers'; -import * as i18n from './translations'; -import { FieldBrowserProps } from './types'; - -const fieldsButtonClassName = 'fields-button'; - -/** wait this many ms after the user completes typing before applying the filter input */ -export const INPUT_TIMEOUT = 250; - -const FieldsBrowserButtonContainer = styled.div` - position: relative; -`; - -FieldsBrowserButtonContainer.displayName = 'FieldsBrowserButtonContainer'; - -/** - * Manages the state of the field browser - */ -export const StatefulFieldsBrowserComponent = React.memo( - ({ - columnHeaders, - browserFields, - height, - isEventViewer = false, - onFieldSelected, - onUpdateColumns, - timelineId, - toggleColumn, - width, - }) => { - /** tracks the latest timeout id from `setTimeout`*/ - const inputTimeoutId = useRef(0); - - /** all field names shown in the field browser must contain this string (when specified) */ - const [filterInput, setFilterInput] = useState(''); - /** all fields in this collection have field names that match the filterInput */ - const [filteredBrowserFields, setFilteredBrowserFields] = useState(null); - /** when true, show a spinner in the input to indicate the field browser is searching for matching field names */ - const [isSearching, setIsSearching] = useState(false); - /** this category will be displayed in the right-hand pane of the field browser */ - const [selectedCategoryId, setSelectedCategoryId] = useState(DEFAULT_CATEGORY_NAME); - /** show the field browser */ - const [show, setShow] = useState(false); - useEffect(() => { - return () => { - if (inputTimeoutId.current !== 0) { - // ⚠️ mutation: cancel any remaining timers and zero-out the timer id: - clearTimeout(inputTimeoutId.current); - inputTimeoutId.current = 0; - } - }; - }, []); - - /** Shows / hides the field browser */ - const toggleShow = useCallback(() => { - setShow(!show); - }, [show]); - - /** Invoked when the user types in the filter input */ - const updateFilter = useCallback( - (newFilterInput: string) => { - setFilterInput(newFilterInput); - setIsSearching(true); - if (inputTimeoutId.current !== 0) { - clearTimeout(inputTimeoutId.current); // ⚠️ mutation: cancel any previous timers - } - // ⚠️ mutation: schedule a new timer that will apply the filter when it fires: - inputTimeoutId.current = window.setTimeout(() => { - const newFilteredBrowserFields = filterBrowserFieldsByFieldName({ - browserFields: mergeBrowserFieldsWithDefaultCategory(browserFields), - substring: newFilterInput, - }); - setFilteredBrowserFields(newFilteredBrowserFields); - setIsSearching(false); - - const newSelectedCategoryId = - newFilterInput === '' || Object.keys(newFilteredBrowserFields).length === 0 - ? DEFAULT_CATEGORY_NAME - : Object.keys(newFilteredBrowserFields) - .sort() - .reduce( - (selected, category) => - newFilteredBrowserFields[category].fields != null && - newFilteredBrowserFields[selected].fields != null && - Object.keys(newFilteredBrowserFields[category].fields!).length > - Object.keys(newFilteredBrowserFields[selected].fields!).length - ? category - : selected, - Object.keys(newFilteredBrowserFields)[0] - ); - setSelectedCategoryId(newSelectedCategoryId); - }, INPUT_TIMEOUT); - }, - [browserFields, filterInput, inputTimeoutId.current] - ); - - /** - * Invoked when the user clicks a category name in the left-hand side of - * the field browser - */ - const updateSelectedCategoryId = useCallback((categoryId: string) => { - setSelectedCategoryId(categoryId); - }, []); - - /** - * Invoked when the user clicks on the context menu to view a category's - * columns in the timeline, this function dispatches the action that - * causes the timeline display those columns. - */ - const updateColumnsAndSelectCategoryId = useCallback((columns: ColumnHeaderOptions[]) => { - onUpdateColumns(columns); // show the category columns in the timeline - }, []); - - /** Invoked when the field browser should be hidden */ - const hideFieldBrowser = useCallback(() => { - setFilterInput(''); - setFilterInput(''); - setFilteredBrowserFields(null); - setIsSearching(false); - setSelectedCategoryId(DEFAULT_CATEGORY_NAME); - setShow(false); - }, []); - // only merge in the default category if the field browser is visible - const browserFieldsWithDefaultCategory = useMemo( - () => (show ? mergeBrowserFieldsWithDefaultCategory(browserFields) : {}), - [show, browserFields] - ); - - return ( - <> - - - {isEventViewer ? ( - - ) : ( - - {i18n.FIELDS} - - )} - - - {show && ( - - )} - - - ); - } -); - -const mapDispatchToProps = { - removeColumn: timelineActions.removeColumn, - upsertColumn: timelineActions.upsertColumn, -}; - -const connector = connect(null, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const StatefulFieldsBrowser = connector(React.memo(StatefulFieldsBrowserComponent)); diff --git a/x-pack/plugins/siem/public/components/fields_browser/types.ts b/x-pack/plugins/siem/public/components/fields_browser/types.ts deleted file mode 100644 index d6b1936fcc52f8..00000000000000 --- a/x-pack/plugins/siem/public/components/fields_browser/types.ts +++ /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 { BrowserFields } from '../../containers/source'; -import { ColumnHeaderOptions } from '../../store/timeline/model'; -import { OnUpdateColumns } from '../timeline/events'; - -export type OnFieldSelected = (fieldId: string) => void; -export type OnHideFieldBrowser = () => void; - -export interface FieldBrowserProps { - /** The timeline's current column headers */ - columnHeaders: ColumnHeaderOptions[]; - /** A map of categoryId -> metadata about the fields in that category */ - browserFields: BrowserFields; - /** The height of the field browser */ - height: number; - /** When true, this Fields Browser is being used as an "events viewer" */ - isEventViewer?: boolean; - /** - * Overrides the default behavior of the `FieldBrowser` to enable - * "selection" mode, where a field is selected by clicking a button - * instead of dragging it to the timeline - */ - onFieldSelected?: OnFieldSelected; - /** Invoked when a user chooses to view a new set of columns in the timeline */ - onUpdateColumns: OnUpdateColumns; - /** The timeline associated with this field browser */ - timelineId: string; - /** Adds or removes a column to / from the timeline */ - toggleColumn: (column: ColumnHeaderOptions) => void; - /** The width of the field browser */ - width: number; -} diff --git a/x-pack/plugins/siem/public/components/flyout/button/index.tsx b/x-pack/plugins/siem/public/components/flyout/button/index.tsx deleted file mode 100644 index d0debbca4dec39..00000000000000 --- a/x-pack/plugins/siem/public/components/flyout/button/index.tsx +++ /dev/null @@ -1,149 +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 { noop } from 'lodash/fp'; -import { EuiButton, EuiNotificationBadge, EuiPanel } from '@elastic/eui'; -import { rgba } from 'polished'; -import React, { useMemo } from 'react'; -import styled from 'styled-components'; - -import { WithSource } from '../../../containers/source'; -import { IS_DRAGGING_CLASS_NAME } from '../../drag_and_drop/helpers'; -import { DataProviders } from '../../timeline/data_providers'; -import { DataProvider } from '../../timeline/data_providers/data_provider'; -import { flattenIntoAndGroups } from '../../timeline/data_providers/helpers'; - -import * as i18n from './translations'; - -export const FLYOUT_BUTTON_CLASS_NAME = 'timeline-flyout-button'; - -export const getBadgeCount = (dataProviders: DataProvider[]): number => - flattenIntoAndGroups(dataProviders).reduce((total, group) => total + group.length, 0); - -const SHOW_HIDE_TRANSLATE_X = 497; // px - -const Container = styled.div` - padding-top: 8px; - position: fixed; - right: 0px; - top: 40%; - transform: translateX(${SHOW_HIDE_TRANSLATE_X}px); - user-select: none; - width: 500px; - z-index: ${({ theme }) => theme.eui.euiZLevel9}; - - .${IS_DRAGGING_CLASS_NAME} & { - transform: none; - } - - .${FLYOUT_BUTTON_CLASS_NAME} { - border-radius: 4px 4px 0 0; - box-shadow: none; - height: 46px; - } - - .${IS_DRAGGING_CLASS_NAME} & .${FLYOUT_BUTTON_CLASS_NAME} { - color: ${({ theme }) => theme.eui.euiColorSuccess}; - background: ${({ theme }) => rgba(theme.eui.euiColorSuccess, 0.2)} !important; - border: 1px solid ${({ theme }) => theme.eui.euiColorSuccess}; - border-bottom: none; - text-decoration: none; - } -`; - -Container.displayName = 'Container'; - -const BadgeButtonContainer = styled.div` - align-items: flex-start; - display: flex; - flex-direction: row; - left: -87px; - position: absolute; - top: 34px; - transform: rotate(-90deg); -`; - -BadgeButtonContainer.displayName = 'BadgeButtonContainer'; - -const DataProvidersPanel = styled(EuiPanel)` - border-radius: 0; - padding: 0 4px 0 4px; - user-select: none; - z-index: ${({ theme }) => theme.eui.euiZLevel9}; -`; - -interface FlyoutButtonProps { - dataProviders: DataProvider[]; - onOpen: () => void; - show: boolean; - timelineId: string; -} - -export const FlyoutButton = React.memo( - ({ onOpen, show, dataProviders, timelineId }) => { - const badgeCount = useMemo(() => getBadgeCount(dataProviders), [dataProviders]); - - if (!show) { - return null; - } - - return ( - - - - {i18n.FLYOUT_BUTTON} - - - {badgeCount} - - - - - {({ browserFields }) => ( - - )} - - - - ); - }, - (prevProps, nextProps) => - prevProps.show === nextProps.show && - prevProps.dataProviders === nextProps.dataProviders && - prevProps.timelineId === nextProps.timelineId -); - -FlyoutButton.displayName = 'FlyoutButton'; diff --git a/x-pack/plugins/siem/public/components/flyout/header/index.tsx b/x-pack/plugins/siem/public/components/flyout/header/index.tsx deleted file mode 100644 index 27a8a83a0850af..00000000000000 --- a/x-pack/plugins/siem/public/components/flyout/header/index.tsx +++ /dev/null @@ -1,149 +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, { useCallback } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import { Dispatch } from 'redux'; - -import { isEmpty, get } from 'lodash/fp'; -import { History } from '../../../lib/history'; -import { Note } from '../../../lib/note'; -import { - appSelectors, - inputsModel, - inputsSelectors, - State, - timelineSelectors, -} from '../../../store'; -import { defaultHeaders } from '../../timeline/body/column_headers/default_headers'; -import { Properties } from '../../timeline/properties'; -import { appActions } from '../../../store/app'; -import { inputsActions } from '../../../store/inputs'; -import { timelineActions } from '../../../store/actions'; -import { TimelineModel } from '../../../store/timeline/model'; -import { timelineDefaults } from '../../../store/timeline/defaults'; -import { InputsModelId } from '../../../store/inputs/constants'; - -interface OwnProps { - timelineId: string; - usersViewing: string[]; -} - -type Props = OwnProps & PropsFromRedux; - -const StatefulFlyoutHeader = React.memo( - ({ - associateNote, - createTimeline, - description, - isFavorite, - isDataInTimeline, - isDatepickerLocked, - title, - noteIds, - notesById, - timelineId, - toggleLock, - updateDescription, - updateIsFavorite, - updateNote, - updateTitle, - usersViewing, - }) => { - const getNotesByIds = useCallback( - (noteIdsVar: string[]): Note[] => appSelectors.getNotes(notesById, noteIdsVar), - [notesById] - ); - return ( - - ); - } -); - -StatefulFlyoutHeader.displayName = 'StatefulFlyoutHeader'; - -const emptyHistory: History[] = []; // stable reference - -const emptyNotesId: string[] = []; // stable reference - -const makeMapStateToProps = () => { - const getTimeline = timelineSelectors.getTimelineByIdSelector(); - const getNotesByIds = appSelectors.notesByIdsSelector(); - const getGlobalInput = inputsSelectors.globalSelector(); - const mapStateToProps = (state: State, { timelineId }: OwnProps) => { - const timeline: TimelineModel = getTimeline(state, timelineId) ?? timelineDefaults; - const globalInput: inputsModel.InputsRange = getGlobalInput(state); - const { - dataProviders, - description = '', - isFavorite = false, - kqlQuery, - title = '', - noteIds = emptyNotesId, - } = timeline; - - const history = emptyHistory; // TODO: get history from store via selector - - return { - description, - notesById: getNotesByIds(state), - history, - isDataInTimeline: - !isEmpty(dataProviders) || !isEmpty(get('filterQuery.kuery.expression', kqlQuery)), - isFavorite, - isDatepickerLocked: globalInput.linkTo.includes('timeline'), - noteIds, - title, - }; - }; - return mapStateToProps; -}; - -const mapDispatchToProps = (dispatch: Dispatch, { timelineId }: OwnProps) => ({ - associateNote: (noteId: string) => dispatch(timelineActions.addNote({ id: timelineId, noteId })), - createTimeline: ({ id, show }: { id: string; show?: boolean }) => - dispatch( - timelineActions.createTimeline({ - id, - columns: defaultHeaders, - show, - }) - ), - updateDescription: ({ id, description }: { id: string; description: string }) => - dispatch(timelineActions.updateDescription({ id, description })), - updateIsFavorite: ({ id, isFavorite }: { id: string; isFavorite: boolean }) => - dispatch(timelineActions.updateIsFavorite({ id, isFavorite })), - updateIsLive: ({ id, isLive }: { id: string; isLive: boolean }) => - dispatch(timelineActions.updateIsLive({ id, isLive })), - updateNote: (note: Note) => dispatch(appActions.updateNote({ note })), - updateTitle: ({ id, title }: { id: string; title: string }) => - dispatch(timelineActions.updateTitle({ id, title })), - toggleLock: ({ linkToId }: { linkToId: InputsModelId }) => - dispatch(inputsActions.toggleTimelineLinkTo({ linkToId })), -}); - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const FlyoutHeader = connector(StatefulFlyoutHeader); diff --git a/x-pack/plugins/siem/public/components/flyout/header_with_close_button/index.test.tsx b/x-pack/plugins/siem/public/components/flyout/header_with_close_button/index.test.tsx deleted file mode 100644 index e0eace2ad5b102..00000000000000 --- a/x-pack/plugins/siem/public/components/flyout/header_with_close_button/index.test.tsx +++ /dev/null @@ -1,45 +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 { mount, shallow } from 'enzyme'; -import React from 'react'; - -import { TestProviders } from '../../../mock'; -import { FlyoutHeaderWithCloseButton } from '.'; - -describe('FlyoutHeaderWithCloseButton', () => { - test('renders correctly against snapshot', () => { - const EmptyComponent = shallow( - - - - ); - expect(EmptyComponent.find('FlyoutHeaderWithCloseButton')).toMatchSnapshot(); - }); - - test('it should invoke onClose when the close button is clicked', () => { - const closeMock = jest.fn(); - const wrapper = mount( - - - - ); - wrapper - .find('[data-test-subj="close-timeline"] button') - .first() - .simulate('click'); - - expect(closeMock).toBeCalled(); - }); -}); diff --git a/x-pack/plugins/siem/public/components/flyout/index.test.tsx b/x-pack/plugins/siem/public/components/flyout/index.test.tsx deleted file mode 100644 index ab41b4617894e7..00000000000000 --- a/x-pack/plugins/siem/public/components/flyout/index.test.tsx +++ /dev/null @@ -1,246 +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 { mount, shallow } from 'enzyme'; -import { set } from 'lodash/fp'; -import React from 'react'; -import { ActionCreator } from 'typescript-fsa'; - -import { apolloClientObservable, mockGlobalState, TestProviders } from '../../mock'; -import { createStore, State } from '../../store'; -import { mockDataProviders } from '../timeline/data_providers/mock/mock_data_providers'; - -import { Flyout, FlyoutComponent } from '.'; -import { FlyoutButton } from './button'; - -jest.mock('../timeline', () => ({ - // eslint-disable-next-line react/display-name - StatefulTimeline: () =>
, -})); - -const testFlyoutHeight = 980; -const usersViewing = ['elastic']; - -describe('Flyout', () => { - const state: State = mockGlobalState; - - describe('rendering', () => { - test('it renders correctly against snapshot', () => { - const wrapper = shallow( - - - - ); - expect(wrapper.find('Flyout')).toMatchSnapshot(); - }); - - test('it renders the default flyout state as a button', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="flyout-button-not-ready-to-drop"]') - .first() - .text() - ).toContain('Timeline'); - }); - - test('it does NOT render the fly out button when its state is set to flyout is true', () => { - const stateShowIsTrue = set('timeline.timelineById.test.show', true, state); - const storeShowIsTrue = createStore(stateShowIsTrue, apolloClientObservable); - - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="flyout-button-not-ready-to-drop"]').exists()).toEqual( - false - ); - }); - - test('it does render the data providers badge when the number is greater than 0', () => { - const stateWithDataProviders = set( - 'timeline.timelineById.test.dataProviders', - mockDataProviders, - state - ); - const storeWithDataProviders = createStore(stateWithDataProviders, apolloClientObservable); - - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="badge"]').exists()).toEqual(true); - }); - - test('it renders the correct number of data providers badge when the number is greater than 0', () => { - const stateWithDataProviders = set( - 'timeline.timelineById.test.dataProviders', - mockDataProviders, - state - ); - const storeWithDataProviders = createStore(stateWithDataProviders, apolloClientObservable); - - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="badge"]') - .first() - .text() - ).toContain('10'); - }); - - test('it hides the data providers badge when the timeline does NOT have data providers', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="badge"]') - .first() - .props().style!.visibility - ).toEqual('hidden'); - }); - - test('it does NOT hide the data providers badge when the timeline has data providers', () => { - const stateWithDataProviders = set( - 'timeline.timelineById.test.dataProviders', - mockDataProviders, - state - ); - const storeWithDataProviders = createStore(stateWithDataProviders, apolloClientObservable); - - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="badge"]') - .first() - .props().style!.visibility - ).toEqual('inherit'); - }); - - test('should call the onOpen when the mouse is clicked for rendering', () => { - const showTimeline = (jest.fn() as unknown) as ActionCreator<{ id: string; show: boolean }>; - const wrapper = mount( - - - - ); - - wrapper - .find('[data-test-subj="flyoutOverlay"]') - .first() - .simulate('click'); - - expect(showTimeline).toBeCalled(); - }); - }); - - describe('showFlyoutButton', () => { - test('should show the flyout button when show is true', () => { - const openMock = jest.fn(); - const wrapper = mount( - - - - ); - expect(wrapper.find('[data-test-subj="flyout-button-not-ready-to-drop"]').exists()).toEqual( - true - ); - }); - - test('should NOT show the flyout button when show is false', () => { - const openMock = jest.fn(); - const wrapper = mount( - - - - ); - expect(wrapper.find('[data-test-subj="flyout-button-not-ready-to-drop"]').exists()).toEqual( - false - ); - }); - - test('should return the flyout button with text', () => { - const openMock = jest.fn(); - const wrapper = mount( - - - - ); - expect( - wrapper - .find('[data-test-subj="flyout-button-not-ready-to-drop"]') - .first() - .text() - ).toContain('Timeline'); - }); - - test('should call the onOpen when it is clicked', () => { - const openMock = jest.fn(); - const wrapper = mount( - - - - ); - wrapper - .find('[data-test-subj="flyoutOverlay"]') - .first() - .simulate('click'); - - expect(openMock).toBeCalled(); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/flyout/index.tsx b/x-pack/plugins/siem/public/components/flyout/index.tsx deleted file mode 100644 index 404ca4a16e0f1c..00000000000000 --- a/x-pack/plugins/siem/public/components/flyout/index.tsx +++ /dev/null @@ -1,111 +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 { EuiBadge } from '@elastic/eui'; -import React, { useCallback } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import styled from 'styled-components'; - -import { State, timelineSelectors } from '../../store'; -import { DataProvider } from '../timeline/data_providers/data_provider'; -import { FlyoutButton } from './button'; -import { Pane } from './pane'; -import { timelineActions } from '../../store/actions'; -import { DEFAULT_TIMELINE_WIDTH } from '../timeline/body/constants'; -import { StatefulTimeline } from '../timeline'; -import { TimelineById } from '../../store/timeline/types'; - -export const Badge = (styled(EuiBadge)` - position: absolute; - padding-left: 4px; - padding-right: 4px; - right: 0%; - top: 0%; - border-bottom-left-radius: 5px; -` as unknown) as typeof EuiBadge; - -Badge.displayName = 'Badge'; - -const Visible = styled.div<{ show?: boolean }>` - visibility: ${({ show }) => (show ? 'visible' : 'hidden')}; -`; - -Visible.displayName = 'Visible'; - -interface OwnProps { - flyoutHeight: number; - timelineId: string; - usersViewing: string[]; -} - -type Props = OwnProps & ProsFromRedux; - -export const FlyoutComponent = React.memo( - ({ dataProviders, flyoutHeight, show, showTimeline, timelineId, usersViewing, width }) => { - const handleClose = useCallback(() => showTimeline({ id: timelineId, show: false }), [ - showTimeline, - timelineId, - ]); - const handleOpen = useCallback(() => showTimeline({ id: timelineId, show: true }), [ - showTimeline, - timelineId, - ]); - - return ( - <> - - - - - - - - ); - } -); - -FlyoutComponent.displayName = 'FlyoutComponent'; - -const DEFAULT_DATA_PROVIDERS: DataProvider[] = []; -const DEFAULT_TIMELINE_BY_ID = {}; - -const mapStateToProps = (state: State, { timelineId }: OwnProps) => { - const timelineById: TimelineById = - timelineSelectors.timelineByIdSelector(state) ?? DEFAULT_TIMELINE_BY_ID; - /* - In case timelineById[timelineId]?.dataProviders is an empty array it will cause unnecessary rerender - of StatefulTimeline which can be expensive, so to avoid that return DEFAULT_DATA_PROVIDERS - */ - const dataProviders = timelineById[timelineId]?.dataProviders.length - ? timelineById[timelineId]?.dataProviders - : DEFAULT_DATA_PROVIDERS; - const show = timelineById[timelineId]?.show ?? false; - const width = timelineById[timelineId]?.width ?? DEFAULT_TIMELINE_WIDTH; - - return { dataProviders, show, width }; -}; - -const mapDispatchToProps = { - showTimeline: timelineActions.showTimeline, -}; - -const connector = connect(mapStateToProps, mapDispatchToProps); - -type ProsFromRedux = ConnectedProps; - -export const Flyout = connector(FlyoutComponent); - -Flyout.displayName = 'Flyout'; diff --git a/x-pack/plugins/siem/public/components/flyout/pane/index.test.tsx b/x-pack/plugins/siem/public/components/flyout/pane/index.test.tsx deleted file mode 100644 index 53cf8f95de0ce7..00000000000000 --- a/x-pack/plugins/siem/public/components/flyout/pane/index.test.tsx +++ /dev/null @@ -1,87 +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 { mount, shallow } from 'enzyme'; -import React from 'react'; - -import { TestProviders } from '../../../mock'; -import { Pane } from '.'; - -const testFlyoutHeight = 980; -const testWidth = 640; - -describe('Pane', () => { - test('renders correctly against snapshot', () => { - const EmptyComponent = shallow( - - - {'I am a child of flyout'} - - - ); - expect(EmptyComponent.find('Pane')).toMatchSnapshot(); - }); - - test('it should NOT let the flyout expand to take up the full width of the element that contains it', () => { - const wrapper = mount( - - - {'I am a child of flyout'} - - - ); - - expect(wrapper.find('Resizable').get(0).props.maxWidth).toEqual('95vw'); - }); - - test('it should render a resize handle', () => { - const wrapper = mount( - - - {'I am a child of flyout'} - - - ); - - expect( - wrapper - .find('[data-test-subj="flyout-resize-handle"]') - .first() - .exists() - ).toEqual(true); - }); - - test('it should render children', () => { - const wrapper = mount( - - - {'I am a mock body'} - - - ); - expect(wrapper.first().text()).toContain('I am a mock body'); - }); -}); diff --git a/x-pack/plugins/siem/public/components/flyout/pane/index.tsx b/x-pack/plugins/siem/public/components/flyout/pane/index.tsx deleted file mode 100644 index 3b5041c1ee3468..00000000000000 --- a/x-pack/plugins/siem/public/components/flyout/pane/index.tsx +++ /dev/null @@ -1,111 +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 { EuiFlyout } from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; -import { useDispatch } from 'react-redux'; -import styled from 'styled-components'; -import { Resizable, ResizeCallback } from 're-resizable'; - -import { TimelineResizeHandle } from './timeline_resize_handle'; -import { EventDetailsWidthProvider } from '../../events_viewer/event_details_width_context'; - -import * as i18n from './translations'; -import { timelineActions } from '../../../store/actions'; - -const minWidthPixels = 550; // do not allow the flyout to shrink below this width (pixels) -const maxWidthPercent = 95; // do not allow the flyout to grow past this percentage of the view -interface FlyoutPaneComponentProps { - children: React.ReactNode; - flyoutHeight: number; - onClose: () => void; - timelineId: string; - width: number; -} - -const EuiFlyoutContainer = styled.div` - .timeline-flyout { - min-width: 150px; - width: auto; - } -`; - -const StyledResizable = styled(Resizable)` - display: flex; - flex-direction: column; -`; - -const RESIZABLE_ENABLE = { left: true }; - -const FlyoutPaneComponent: React.FC = ({ - children, - flyoutHeight, - onClose, - timelineId, - width, -}) => { - const dispatch = useDispatch(); - - const onResizeStop: ResizeCallback = useCallback( - (e, direction, ref, delta) => { - const bodyClientWidthPixels = document.body.clientWidth; - - if (delta.width) { - dispatch( - timelineActions.applyDeltaToWidth({ - bodyClientWidthPixels, - delta: -delta.width, - id: timelineId, - maxWidthPercent, - minWidthPixels, - }) - ); - } - }, - [dispatch] - ); - const resizableDefaultSize = useMemo( - () => ({ - width, - height: '100%', - }), - [] - ); - const resizableHandleComponent = useMemo( - () => ({ - left: , - }), - [flyoutHeight] - ); - - return ( - - - - {children} - - - - ); -}; - -export const Pane = React.memo(FlyoutPaneComponent); - -Pane.displayName = 'Pane'; diff --git a/x-pack/plugins/siem/public/components/formatted_bytes/index.tsx b/x-pack/plugins/siem/public/components/formatted_bytes/index.tsx deleted file mode 100644 index 98a1acf471629e..00000000000000 --- a/x-pack/plugins/siem/public/components/formatted_bytes/index.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import numeral from '@elastic/numeral'; - -import { DEFAULT_BYTES_FORMAT } from '../../../common/constants'; -import { useUiSetting$ } from '../../lib/kibana'; - -type Bytes = string | number; - -export const formatBytes = (value: Bytes, format: string) => { - return numeral(value).format(format); -}; - -export const useFormatBytes = () => { - const [bytesFormat] = useUiSetting$(DEFAULT_BYTES_FORMAT); - - return (value: Bytes) => formatBytes(value, bytesFormat); -}; - -export const PreferenceFormattedBytesComponent = ({ value }: { value: Bytes }) => ( - <>{useFormatBytes()(value)} -); - -PreferenceFormattedBytesComponent.displayName = 'PreferenceFormattedBytesComponent'; - -export const PreferenceFormattedBytes = React.memo(PreferenceFormattedBytesComponent); - -PreferenceFormattedBytes.displayName = 'PreferenceFormattedBytes'; diff --git a/x-pack/plugins/siem/public/components/formatted_duration/helpers.test.ts b/x-pack/plugins/siem/public/components/formatted_duration/helpers.test.ts deleted file mode 100644 index 30254c49c9f3bf..00000000000000 --- a/x-pack/plugins/siem/public/components/formatted_duration/helpers.test.ts +++ /dev/null @@ -1,401 +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 { getEmptyValue } from '../empty_value'; -import { - getFormattedDurationString, - getHumanizedDuration, - ONE_DAY, - ONE_HOUR, - ONE_MILLISECOND_AS_NANOSECONDS, - ONE_MINUTE, - ONE_MONTH, - ONE_SECOND, - ONE_YEAR, -} from './helpers'; -import * as i18n from './translations'; - -describe('FormattedDurationHelpers', () => { - describe('#getFormattedDurationString', () => { - test('it returns a placeholder when the input is undefined', () => { - expect(getFormattedDurationString(undefined)).toEqual(getEmptyValue()); - }); - - test('it returns a placeholder when the input is null', () => { - expect(getFormattedDurationString(null)).toEqual(getEmptyValue()); - }); - - test('it echos back the input as a string when the input is not a number', () => { - expect(getFormattedDurationString('invalid duration')).toEqual('invalid duration'); - }); - - test('it returns the original input (with no formatting) when the input is negative', () => { - expect(getFormattedDurationString(-1)).toEqual('-1'); - }); - - test('it returns the duration formatted as 0 nanoseconds when the input is 0 nanoseconds', () => { - expect(getFormattedDurationString(0)).toEqual('0ns'); - }); - - test('it returns 1 nanosecond when the input is 1 nanosecond', () => { - expect(getFormattedDurationString(1)).toEqual('1ns'); - }); - - test('it returns 1000 nanoseconds when the input is 1000 nanoseconds', () => { - expect(getFormattedDurationString(1000)).toEqual('1000ns'); - }); - - test('it returns 1000 nanoseconds when the input is a string ("1000") instead of a number', () => { - expect(getFormattedDurationString('1000')).toEqual('1000ns'); - }); - - test('it returns the largest value that would be represented as nanoseconds when the input is 1 millisecond - 1 nanosecond', () => { - expect(getFormattedDurationString(ONE_MILLISECOND_AS_NANOSECONDS - 1)).toEqual('999999ns'); - }); - - test('it returns exactly 1 millisecond (with no fractional component) when the input is exactly one millisecond', () => { - expect(getFormattedDurationString(ONE_MILLISECOND_AS_NANOSECONDS)).toEqual('1ms'); - }); - - test('it returns 1 millisecond with a fractional component when the input is 1 millisecond + 1 nanosecond', () => { - expect(getFormattedDurationString(ONE_MILLISECOND_AS_NANOSECONDS + 1)).toEqual('1.000001ms'); - }); - - test('it returns the largest value (in milliseconds) that would be represented as milliseconds with a fractional component when the input is 1 second - 1 nanosecond', () => { - expect(getFormattedDurationString(ONE_SECOND * ONE_MILLISECOND_AS_NANOSECONDS - 1)).toEqual( - '999.999999ms' - ); - }); - - test('it returns exactly one second (with no millisecond component) when the input is exactly one second', () => { - expect(getFormattedDurationString(ONE_SECOND * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual('1s'); - }); - - test('it returns one second with fractional milliseconds when the input is one second + 1 nanosecond', () => { - expect(getFormattedDurationString(ONE_SECOND * ONE_MILLISECOND_AS_NANOSECONDS + 1)).toEqual( - '1s 0.000001ms' - ); - }); - - test('it returns one second with fractional milliseconds when the input is 1 second + 1 millisecond - 1 nanosecond', () => { - expect( - getFormattedDurationString( - ONE_SECOND * ONE_MILLISECOND_AS_NANOSECONDS + ONE_MILLISECOND_AS_NANOSECONDS - 1 - ) - ).toEqual('1s 0.999999ms'); - }); - - test('it returns 1 second, 1 non-fractional millisecond when the input is 1 second + 1 millisecond', () => { - expect( - getFormattedDurationString( - ONE_SECOND * ONE_MILLISECOND_AS_NANOSECONDS + ONE_MILLISECOND_AS_NANOSECONDS - ) - ).toEqual('1s 1ms'); - }); - - test('it returns 1 seconds with fractional milliseconds when the input is 1 second + 1 millisecond + 1 nanosecond', () => { - expect( - getFormattedDurationString( - ONE_SECOND * ONE_MILLISECOND_AS_NANOSECONDS + ONE_MILLISECOND_AS_NANOSECONDS + 1 - ) - ).toEqual('1s 1.000001ms'); - }); - - test('it returns 1 seconds with fractional milliseconds when the input is 1 second + 2 milliseconds - 1 nanosecond', () => { - expect( - getFormattedDurationString( - ONE_SECOND * ONE_MILLISECOND_AS_NANOSECONDS + 2 * ONE_MILLISECOND_AS_NANOSECONDS - 1 - ) - ).toEqual('1s 1.999999ms'); - }); - - test('it returns 59 seconds with fractional milliseconds when the input is 1 minute - 1 nanosecond', () => { - expect(getFormattedDurationString(ONE_MINUTE * ONE_MILLISECOND_AS_NANOSECONDS - 1)).toEqual( - '59s 999.999999ms' - ); - }); - - test('it returns 1 minute with 0 non-fractional seconds (and no milliseconds) when the input is exactly 1 minute', () => { - expect(getFormattedDurationString(ONE_MINUTE * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual( - '1m 0s' - ); - }); - - test('it returns 1 minute, 0 seconds, and fractional milliseconds when the input is 1 minute + 1 nanosecond', () => { - expect(getFormattedDurationString(ONE_MINUTE * ONE_MILLISECOND_AS_NANOSECONDS + 1)).toEqual( - '1m 0s 0.000001ms' - ); - }); - - test('it returns the duration formatted as 1 minute, 59 seconds and fractional milliseconds when the input is 2 minutes - 1 nanosecond', () => { - expect( - getFormattedDurationString(2 * ONE_MINUTE * ONE_MILLISECOND_AS_NANOSECONDS - 1) - ).toEqual('1m 59s 999.999999ms'); - }); - - test('it returns the duration formatted as 59 minutes, 59 seconds and fractional milliseconds when the input is one hour - 1 nanosecond', () => { - expect(getFormattedDurationString(ONE_HOUR * ONE_MILLISECOND_AS_NANOSECONDS - 1)).toEqual( - '59m 59s 999.999999ms' - ); - }); - - test('it returns the duration formatted as 1 hour, 0 minutes, 0 seconds, (and no milliseconds) when the duration is exactly one hour', () => { - expect(getFormattedDurationString(ONE_HOUR * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual( - '1h 0m 0s' - ); - }); - - test('it returns the duration formatted as 1 hour, 0 minutes and seconds, and fractional milliseconds when the duration is exactly 1 hour + 1 nanosecond', () => { - expect(getFormattedDurationString(ONE_HOUR * ONE_MILLISECOND_AS_NANOSECONDS + 1)).toEqual( - '1h 0m 0s 0.000001ms' - ); - }); - - test('it returns the duration formatted as 23 hours, 59 minutes, 59 seconds, and fractional milliseconds when the duration is one day - 1 nanosecond', () => { - expect(getFormattedDurationString(ONE_DAY * ONE_MILLISECOND_AS_NANOSECONDS - 1)).toEqual( - '23h 59m 59s 999.999999ms' - ); - }); - - test('it returns the duration formatted as 1 day, 0 hours, 0 minutes, and 0 seconds, (and no milliseconds) when the duration is exactly one day', () => { - expect(getFormattedDurationString(ONE_DAY * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual( - '1d 0h 0m 0s' - ); - }); - - test('it returns the duration formatted as one day, with zero hours, minutes, seconds, and fractional milliseconds when the duration is one day + 1 nanosecond', () => { - expect(getFormattedDurationString(ONE_DAY * ONE_MILLISECOND_AS_NANOSECONDS + 1)).toEqual( - '1d 0h 0m 0s 0.000001ms' - ); - }); - - test('it returns the duration formatted as 29 days, 23 hours, 59 minutes, 59 seconds, and with fractional milliseconds when the duration is one month - 1 nanosecond', () => { - expect(getFormattedDurationString(ONE_MONTH * ONE_MILLISECOND_AS_NANOSECONDS - 1)).toEqual( - '29d 23h 59m 59s 999.999999ms' - ); - }); - - test('it returns 30 days, zero hours, minutes, seconds, (and no millieconds) when the duration is exactly one month, as is the current behavior of moment', () => { - expect(getFormattedDurationString(ONE_MONTH * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual( - '30d 0h 0m 0s' // see https://github.com/moment/moment/issues/3653 - ); - }); - - test('it returns the duration as 29 days, 23 hours, 59 minutes, 59 seconds, and fractional milliseconds when the duration is 1 month - 1 nanosecond', () => { - expect(getFormattedDurationString(ONE_MONTH * ONE_MILLISECOND_AS_NANOSECONDS - 1)).toEqual( - '29d 23h 59m 59s 999.999999ms' // see https://github.com/moment/moment/issues/3653 - ); - }); - - test('it returns 1 month, zero days, hours, minutes, seconds (and no milliseconds) month when the duration is exactly 1 month + 1 day, as is the current behavior of moment', () => { - expect( - getFormattedDurationString((ONE_MONTH + ONE_DAY) * ONE_MILLISECOND_AS_NANOSECONDS) - ).toEqual( - '1m 0d 0h 0m 0s' // see https://github.com/moment/moment/issues/3653 - ); - }); - - test('it returns the 1 month with 0 days, hours, minutes, seconds, and fractional milliseconds when the duration is exactly 1 month + 1 day + 1 nanosecond, ', () => { - expect( - getFormattedDurationString((ONE_MONTH + ONE_DAY) * ONE_MILLISECOND_AS_NANOSECONDS + 1) - ).toEqual( - '1m 0d 0h 0m 0s 0.000001ms' // see https://github.com/moment/moment/issues/3653 - ); - }); - - test('it returns 11 months, 30 days (with 0 hours, minutes, and non-fractional seconds) when the duration is exactly one year, as is the current behavior of moment', () => { - expect(getFormattedDurationString(ONE_YEAR * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual( - '11m 30d 0h 0m 0s' // see https://github.com/moment/moment/issues/3209 - ); - }); - - test('it returns one year when the duration is exactly 1 year + 1 day, as is the current behavior of moment', () => { - expect( - getFormattedDurationString((ONE_YEAR + ONE_DAY) * ONE_MILLISECOND_AS_NANOSECONDS) - ).toEqual( - '1y 0m 0d 0h 0m 0s' // see https://github.com/moment/moment/issues/3209 - ); - }); - - test('it returns less than 6 months when input is 1 year + 6 months, as is the current behavior of moment', () => { - expect( - getFormattedDurationString((ONE_YEAR + 6 * ONE_MONTH) * ONE_MILLISECOND_AS_NANOSECONDS) - ).toEqual('1y 5m 27d 0h 0m 0s'); // see https://github.com/moment/moment/issues/3209 - }); - }); - - describe('#getHumanizedDuration', () => { - test('it returns "no duration" when the input is undefined', () => { - expect(getHumanizedDuration(undefined)).toEqual(i18n.NO_DURATION); - }); - - test('it returns "no duration" when the input is null', () => { - expect(getHumanizedDuration(null)).toEqual(i18n.NO_DURATION); - }); - - test('it returns "invalid duration" when the input is not a number', () => { - expect(getHumanizedDuration('an invalid duration')).toEqual(i18n.INVALID_DURATION); - }); - - test('it returns the original "invalid duration" when the input is negative', () => { - expect(getHumanizedDuration(-1)).toEqual(i18n.INVALID_DURATION); - }); - - test('it returns "zero nanoseconds" when the input is 0 nanoseconds', () => { - expect(getHumanizedDuration(0)).toEqual(i18n.ZERO_NANOSECONDS); - }); - - test('it returns "a nanosecond" nanosecond when the input is 1 nanosecond', () => { - expect(getHumanizedDuration(1)).toEqual(i18n.A_NANOSECOND); - }); - - test('it returns "a few nanoseconds" when the input is 1000 nanoseconds', () => { - expect(getHumanizedDuration(1000)).toEqual(i18n.A_FEW_NANOSECONDS); - }); - - test('it returns 1000 nanoseconds when the input is a string ("1000") instead of a number', () => { - expect(getHumanizedDuration('1000')).toEqual(i18n.A_FEW_NANOSECONDS); - }); - - test('it returns "a few nanoseconds" given the largest value that would be represented as nanoseconds', () => { - expect(getHumanizedDuration(ONE_MILLISECOND_AS_NANOSECONDS - 1)).toEqual( - i18n.A_FEW_NANOSECONDS - ); - }); - - test('it returns "a millisecond" when the input is exactly one millisecond', () => { - expect(getHumanizedDuration(ONE_MILLISECOND_AS_NANOSECONDS)).toEqual(i18n.A_MILLISECOND); - }); - - test('it returns "a few milliseconds" when the input is 1 millisecond + 1 nanosecond', () => { - expect(getHumanizedDuration(ONE_MILLISECOND_AS_NANOSECONDS + 1)).toEqual( - i18n.A_FEW_MILLISECONDS - ); - }); - - test('it returns "a few milliseconds" when the input is the maximum value for milliseconds', () => { - expect(getHumanizedDuration(ONE_SECOND * ONE_MILLISECOND_AS_NANOSECONDS - 1)).toEqual( - i18n.A_FEW_MILLISECONDS - ); - }); - - test('it returns "a second" when the input is exactly one second', () => { - expect(getHumanizedDuration(ONE_SECOND * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual( - i18n.A_SECOND - ); - }); - - test('it returns "a few seconds" when the input is one second + 1 nanosecond', () => { - expect(getHumanizedDuration(ONE_SECOND * ONE_MILLISECOND_AS_NANOSECONDS + 1)).toEqual( - 'a few seconds' // <-- note for this and the rest of the tests in this 'describe', this value is coming from moment, which has it's own i18n - ); - }); - - test('it rounds to "a minute" when the input is 45 seconds', () => { - expect(getHumanizedDuration(45 * ONE_SECOND * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual( - 'a minute' // <-- debatable, but thats' how moment describes this - ); - }); - - test('it rounds to "a minute" when the input is 1 minute - 1 nanosecond', () => { - expect(getHumanizedDuration(ONE_MINUTE * ONE_MILLISECOND_AS_NANOSECONDS - 1)).toEqual( - 'a minute' - ); - }); - - test('it returns "a minute" when the input is exactly 1 minute', () => { - expect(getHumanizedDuration(ONE_MINUTE * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual('a minute'); - }); - - test('it rounds to "a minute" when the input is 1 minute + 1 nanosecond', () => { - expect(getHumanizedDuration(ONE_MINUTE * ONE_MILLISECOND_AS_NANOSECONDS + 1)).toEqual( - 'a minute' - ); - }); - - test('it rounds to "two minutes" when the input is 2 minutes - 1 nanosecond', () => { - expect(getHumanizedDuration(2 * ONE_MINUTE * ONE_MILLISECOND_AS_NANOSECONDS - 1)).toEqual( - '2 minutes' - ); - }); - - test('it rounds to "an hour" when the input is one hour - 1 nanosecond', () => { - expect(getHumanizedDuration(ONE_HOUR * ONE_MILLISECOND_AS_NANOSECONDS - 1)).toEqual( - 'an hour' - ); - }); - - test('it returns "an hour" when the input is exactly one hour', () => { - expect(getHumanizedDuration(ONE_HOUR * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual('an hour'); - }); - - test('it rounds to "an hour" when the input 1 hour + 1 nanosecond', () => { - expect(getHumanizedDuration(ONE_HOUR * ONE_MILLISECOND_AS_NANOSECONDS + 1)).toEqual( - 'an hour' - ); - }); - - test('it returns "2 hours" when the input is exactly 2 hours', () => { - expect(getHumanizedDuration(2 * ONE_HOUR * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual( - '2 hours' - ); - }); - - test('it rounds to "a day" when the input is one day - 1 nanosecond', () => { - expect(getHumanizedDuration(ONE_DAY * ONE_MILLISECOND_AS_NANOSECONDS - 1)).toEqual('a day'); - }); - - test('it returns "a day" when the input is exactly one day', () => { - expect(getHumanizedDuration(ONE_DAY * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual('a day'); - }); - - test('it rounds to "a day" when the input is one day + 1 nanosecond', () => { - expect(getHumanizedDuration(ONE_DAY * ONE_MILLISECOND_AS_NANOSECONDS + 1)).toEqual('a day'); - }); - - test('it returns "2 days" when the input is exactly two days', () => { - expect(getHumanizedDuration(2 * ONE_DAY * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual('2 days'); - }); - - test('it rounds to "a month" when the input is one month - 1 nanosecond', () => { - expect(getHumanizedDuration(ONE_MONTH * ONE_MILLISECOND_AS_NANOSECONDS - 1)).toEqual( - 'a month' - ); - }); - - test('it returns "a month" when the input is exactly one month', () => { - expect(getHumanizedDuration(ONE_MONTH * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual('a month'); - }); - - test('it rounds to "a month" when the input is 1 month + 1 day', () => { - expect(getHumanizedDuration((ONE_MONTH + ONE_DAY) * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual( - 'a month' - ); - }); - - test('it returns "2 months" when the input is 2 months', () => { - expect(getHumanizedDuration(2 * ONE_MONTH * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual( - '2 months' - ); - }); - - test('it returns "a year" when the input is exactly one year', () => { - expect(getHumanizedDuration(ONE_YEAR * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual('a year'); - }); - - test('it rounds down to "a year" when the input is 1 year + 6 months, as is the current behavior of moment', () => { - expect( - getHumanizedDuration((ONE_YEAR + 6 * ONE_MONTH) * ONE_MILLISECOND_AS_NANOSECONDS) - ).toEqual('a year'); // <-- as a user, you may not expect this - }); - - test('it returns "2 years" when the duration is exactly 2 years', () => { - expect(getHumanizedDuration(2 * ONE_YEAR * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual( - '2 years' - ); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/formatted_duration/helpers.tsx b/x-pack/plugins/siem/public/components/formatted_duration/helpers.tsx deleted file mode 100644 index 44bd76bc6beb05..00000000000000 --- a/x-pack/plugins/siem/public/components/formatted_duration/helpers.tsx +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import moment from 'moment'; - -import { getEmptyValue } from '../empty_value'; - -import * as i18n from './translations'; - -/** one millisecond (as nanoseconds) */ -export const ONE_MILLISECOND_AS_NANOSECONDS = 1000000; - -export const ONE_SECOND = 1000; -export const ONE_MINUTE = 60000; -export const ONE_HOUR = 3600000; -export const ONE_DAY = 86400000; // ms -export const ONE_MONTH = 2592000000; // ms -export const ONE_YEAR = 31536000000; // ms - -const milliseconds = (duration: moment.Duration): string => - Number.isInteger(duration.milliseconds()) - ? `${duration.milliseconds()}ms` - : `${duration.milliseconds().toFixed(6)}ms`; // nanosecond precision -const seconds = (duration: moment.Duration): string => - `${duration.seconds().toFixed()}s${ - duration.milliseconds() > 0 ? ` ${milliseconds(duration)}` : '' - }`; -const minutes = (duration: moment.Duration): string => - `${duration.minutes()}m ${seconds(duration)}`; -const hours = (duration: moment.Duration): string => `${duration.hours()}h ${minutes(duration)}`; -const days = (duration: moment.Duration): string => `${duration.days()}d ${hours(duration)}`; -const months = (duration: moment.Duration): string => - `${duration.years() > 0 || duration.months() > 0 ? `${duration.months()}m ` : ''}${days( - duration - )}`; -const years = (duration: moment.Duration): string => - `${duration.years() > 0 ? `${duration.years()}y ` : ''}${months(duration)}`; - -export const getFormattedDurationString = ( - maybeDurationNanoseconds: string | number | object | undefined | null -): string => { - const totalNanoseconds = Number(maybeDurationNanoseconds); - - if (maybeDurationNanoseconds == null) { - return getEmptyValue(); - } - - if (Number.isNaN(totalNanoseconds) || totalNanoseconds < 0) { - return `${maybeDurationNanoseconds}`; // echo back the duration as a string - } - - if (totalNanoseconds < ONE_MILLISECOND_AS_NANOSECONDS) { - return `${totalNanoseconds}ns`; // display the raw nanoseconds - } - - const duration = moment.duration(totalNanoseconds / ONE_MILLISECOND_AS_NANOSECONDS); - const totalMs = duration.asMilliseconds(); - - if (totalMs < ONE_SECOND) { - return milliseconds(duration); - } else if (totalMs < ONE_MINUTE) { - return seconds(duration); - } else if (totalMs < ONE_HOUR) { - return minutes(duration); - } else if (totalMs < ONE_DAY) { - return hours(duration); - } else if (totalMs < ONE_MONTH) { - return days(duration); - } else if (totalMs < ONE_YEAR) { - return months(duration); - } else { - return years(duration); - } -}; - -export const getHumanizedDuration = ( - maybeDurationNanoseconds: string | number | object | undefined | null -): string => { - if (maybeDurationNanoseconds == null) { - return i18n.NO_DURATION; - } - - const totalNanoseconds = Number(maybeDurationNanoseconds); - - if (Number.isNaN(totalNanoseconds) || totalNanoseconds < 0) { - return i18n.INVALID_DURATION; - } - - if (totalNanoseconds === 0) { - return i18n.ZERO_NANOSECONDS; - } else if (totalNanoseconds === 1) { - return i18n.A_NANOSECOND; - } else if (totalNanoseconds < ONE_MILLISECOND_AS_NANOSECONDS) { - return i18n.A_FEW_NANOSECONDS; - } else if (totalNanoseconds === ONE_MILLISECOND_AS_NANOSECONDS) { - return i18n.A_MILLISECOND; - } - - const totalMs = totalNanoseconds / ONE_MILLISECOND_AS_NANOSECONDS; - if (totalMs < ONE_SECOND) { - return i18n.A_FEW_MILLISECONDS; - } else if (totalMs === ONE_SECOND) { - return i18n.A_SECOND; - } else { - return moment.duration(totalMs).humanize(); - } -}; diff --git a/x-pack/plugins/siem/public/components/formatted_ip/index.tsx b/x-pack/plugins/siem/public/components/formatted_ip/index.tsx deleted file mode 100644 index ba97e8d61451c3..00000000000000 --- a/x-pack/plugins/siem/public/components/formatted_ip/index.tsx +++ /dev/null @@ -1,178 +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 { isArray, isEmpty, isString, uniq } from 'lodash/fp'; -import React from 'react'; - -import { DragEffects, DraggableWrapper } from '../drag_and_drop/draggable_wrapper'; -import { escapeDataProviderId } from '../drag_and_drop/helpers'; -import { getOrEmptyTagFromValue } from '../empty_value'; -import { IPDetailsLink } from '../links'; -import { parseQueryValue } from '../timeline/body/renderers/parse_query_value'; -import { DataProvider, IS_OPERATOR } from '../timeline/data_providers/data_provider'; -import { Provider } from '../timeline/data_providers/provider'; - -const getUniqueId = ({ - contextId, - eventId, - fieldName, - address, -}: { - contextId: string; - eventId: string; - fieldName: string; - address: string | object | null | undefined; -}) => `formatted-ip-data-provider-${contextId}-${fieldName}-${address}-${eventId}`; - -const tryStringify = (value: string | object | null | undefined): string => { - try { - return JSON.stringify(value); - } catch (_) { - return `${value}`; - } -}; - -const getDataProvider = ({ - contextId, - eventId, - fieldName, - address, -}: { - contextId: string; - eventId: string; - fieldName: string; - address: string | object | null | undefined; -}): DataProvider => ({ - enabled: true, - id: escapeDataProviderId(getUniqueId({ contextId, eventId, fieldName, address })), - name: `${fieldName}: ${parseQueryValue(address)}`, - queryMatch: { - field: fieldName, - value: parseQueryValue(address), - operator: IS_OPERATOR, - }, - excluded: false, - kqlQuery: '', - and: [], -}); - -const NonDecoratedIpComponent: React.FC<{ - contextId: string; - eventId: string; - fieldName: string; - truncate?: boolean; - value: string | object | null | undefined; -}> = ({ contextId, eventId, fieldName, truncate, value }) => ( - - snapshot.isDragging ? ( - - - - ) : typeof value !== 'object' ? ( - getOrEmptyTagFromValue(value) - ) : ( - getOrEmptyTagFromValue(tryStringify(value)) - ) - } - truncate={truncate} - /> -); - -const NonDecoratedIp = React.memo(NonDecoratedIpComponent); - -const AddressLinksComponent: React.FC<{ - addresses: string[]; - contextId: string; - eventId: string; - fieldName: string; - truncate?: boolean; -}> = ({ addresses, contextId, eventId, fieldName, truncate }) => ( - <> - {uniq(addresses).map(address => ( - - snapshot.isDragging ? ( - - - - ) : ( - - ) - } - truncate={truncate} - /> - ))} - -); - -const AddressLinks = React.memo(AddressLinksComponent); - -const FormattedIpComponent: React.FC<{ - contextId: string; - eventId: string; - fieldName: string; - truncate?: boolean; - value: string | object | null | undefined; -}> = ({ contextId, eventId, fieldName, truncate, value }) => { - if (isString(value) && !isEmpty(value)) { - try { - const addresses = JSON.parse(value); - if (isArray(addresses)) { - return ( - - ); - } - } catch (_) { - // fall back to formatting it as a single link - } - - // return a single draggable link - return ( - - ); - } else { - return ( - - ); - } -}; - -export const FormattedIp = React.memo(FormattedIpComponent); diff --git a/x-pack/plugins/siem/public/components/generic_downloader/index.tsx b/x-pack/plugins/siem/public/components/generic_downloader/index.tsx deleted file mode 100644 index 6f08f5c8c381cd..00000000000000 --- a/x-pack/plugins/siem/public/components/generic_downloader/index.tsx +++ /dev/null @@ -1,107 +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, { useEffect, useRef } from 'react'; -import styled from 'styled-components'; -import { isFunction } from 'lodash/fp'; -import * as i18n from './translations'; - -import { ExportDocumentsProps } from '../../containers/detection_engine/rules'; -import { useStateToaster, errorToToaster } from '../toasters'; - -const InvisibleAnchor = styled.a` - display: none; -`; - -export type ExportSelectedData = ({ - excludeExportDetails, - filename, - ids, - signal, -}: ExportDocumentsProps) => Promise; - -export interface GenericDownloaderProps { - filename: string; - ids?: string[]; - exportSelectedData: ExportSelectedData; - onExportSuccess?: (exportCount: number) => void; - onExportFailure?: () => void; -} - -/** - * Component for downloading Rules as an exported .ndjson file. Download will occur on each update to `rules` param - * - * @param filename of file to be downloaded - * @param payload Rule[] - * - */ - -export const GenericDownloaderComponent = ({ - exportSelectedData, - filename, - ids, - onExportSuccess, - onExportFailure, -}: GenericDownloaderProps) => { - const anchorRef = useRef(null); - const [, dispatchToaster] = useStateToaster(); - - useEffect(() => { - let isSubscribed = true; - const abortCtrl = new AbortController(); - - const exportData = async () => { - if (anchorRef && anchorRef.current && ids != null && ids.length > 0) { - try { - const exportResponse = await exportSelectedData({ - ids, - signal: abortCtrl.signal, - }); - - if (isSubscribed) { - // this is for supporting IE - if (isFunction(window.navigator.msSaveOrOpenBlob)) { - window.navigator.msSaveBlob(exportResponse); - } else { - const objectURL = window.URL.createObjectURL(exportResponse); - // These are safe-assignments as writes to anchorRef are isolated to exportData - anchorRef.current.href = objectURL; // eslint-disable-line require-atomic-updates - anchorRef.current.download = filename; // eslint-disable-line require-atomic-updates - anchorRef.current.click(); - window.URL.revokeObjectURL(objectURL); - } - - if (onExportSuccess != null) { - onExportSuccess(ids.length); - } - } - } catch (error) { - if (isSubscribed) { - if (onExportFailure != null) { - onExportFailure(); - } - errorToToaster({ title: i18n.EXPORT_FAILURE, error, dispatchToaster }); - } - } - } - }; - - exportData(); - - return () => { - isSubscribed = false; - abortCtrl.abort(); - }; - }, [ids]); - - return ; -}; - -GenericDownloaderComponent.displayName = 'GenericDownloaderComponent'; - -export const GenericDownloader = React.memo(GenericDownloaderComponent); - -GenericDownloader.displayName = 'GenericDownloader'; diff --git a/x-pack/plugins/siem/public/components/header_global/index.tsx b/x-pack/plugins/siem/public/components/header_global/index.tsx deleted file mode 100644 index adc2be4f9c3656..00000000000000 --- a/x-pack/plugins/siem/public/components/header_global/index.tsx +++ /dev/null @@ -1,104 +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 { EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLink } from '@elastic/eui'; -import { pickBy } from 'lodash/fp'; -import React from 'react'; -import styled, { css } from 'styled-components'; - -import { useLocation } from 'react-router-dom'; -import { gutterTimeline } from '../../lib/helpers'; -import { navTabs } from '../../pages/home/home_navigations'; -import { SiemPageName } from '../../pages/home/types'; -import { getOverviewUrl } from '../link_to'; -import { MlPopover } from '../ml_popover/ml_popover'; -import { SiemNavigation } from '../navigation'; -import * as i18n from './translations'; -import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source'; - -const Wrapper = styled.header` - ${({ theme }) => css` - background: ${theme.eui.euiColorEmptyShade}; - border-bottom: ${theme.eui.euiBorderThin}; - padding: ${theme.eui.paddingSizes.m} ${gutterTimeline} ${theme.eui.paddingSizes.m} - ${theme.eui.paddingSizes.l}; - `} -`; -Wrapper.displayName = 'Wrapper'; - -const FlexItem = styled(EuiFlexItem)` - min-width: 0; -`; -FlexItem.displayName = 'FlexItem'; - -interface HeaderGlobalProps { - hideDetectionEngine?: boolean; -} -export const HeaderGlobal = React.memo(({ hideDetectionEngine = false }) => { - const currentLocation = useLocation(); - - return ( - - - - {({ indicesExist }) => ( - <> - - - - - - - - - - {indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - key !== SiemPageName.detections, navTabs) - : navTabs - } - /> - ) : ( - key === SiemPageName.overview, navTabs)} - /> - )} - - - - - - - {indicesExistOrDataTemporarilyUnavailable(indicesExist) && - currentLocation.pathname.includes(`/${SiemPageName.detections}/`) && ( - - - - )} - - - - {i18n.BUTTON_ADD_DATA} - - - - - - )} - - - - ); -}); -HeaderGlobal.displayName = 'HeaderGlobal'; diff --git a/x-pack/plugins/siem/public/components/import_data_modal/index.tsx b/x-pack/plugins/siem/public/components/import_data_modal/index.tsx deleted file mode 100644 index c827411a41e2ea..00000000000000 --- a/x-pack/plugins/siem/public/components/import_data_modal/index.tsx +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - EuiButton, - EuiButtonEmpty, - EuiCheckbox, - // @ts-ignore no-exported-member - EuiFilePicker, - EuiModal, - EuiModalBody, - EuiModalFooter, - EuiModalHeader, - EuiModalHeaderTitle, - EuiOverlayMask, - EuiSpacer, - EuiText, -} from '@elastic/eui'; -import React, { useCallback, useState } from 'react'; - -import { ImportDataResponse, ImportDataProps } from '../../containers/detection_engine/rules'; -import { - displayErrorToast, - displaySuccessToast, - useStateToaster, - errorToToaster, -} from '../toasters'; -import * as i18n from './translations'; - -interface ImportDataModalProps { - checkBoxLabel: string; - closeModal: () => void; - description: string; - errorMessage: string; - failedDetailed: (id: string, statusCode: number, message: string) => string; - importComplete: () => void; - importData: (arg: ImportDataProps) => Promise; - showCheckBox: boolean; - showModal: boolean; - submitBtnText: string; - subtitle: string; - successMessage: (totalCount: number) => string; - title: string; -} - -/** - * Modal component for importing Rules from a json file - */ -export const ImportDataModalComponent = ({ - checkBoxLabel, - closeModal, - description, - errorMessage, - failedDetailed, - importComplete, - importData, - showCheckBox = true, - showModal, - submitBtnText, - subtitle, - successMessage, - title, -}: ImportDataModalProps) => { - const [selectedFiles, setSelectedFiles] = useState(null); - const [isImporting, setIsImporting] = useState(false); - const [overwrite, setOverwrite] = useState(false); - const [, dispatchToaster] = useStateToaster(); - - const cleanupAndCloseModal = useCallback(() => { - setIsImporting(false); - setSelectedFiles(null); - closeModal(); - }, [setIsImporting, setSelectedFiles, closeModal]); - - const importDataCallback = useCallback(async () => { - if (selectedFiles != null) { - setIsImporting(true); - const abortCtrl = new AbortController(); - - try { - const importResponse = await importData({ - fileToImport: selectedFiles[0], - overwrite, - signal: abortCtrl.signal, - }); - - // TODO: Improve error toast details for better debugging failed imports - // e.g. When success == true && success_count === 0 that means no rules were overwritten, etc - if (importResponse.success) { - displaySuccessToast(successMessage(importResponse.success_count), dispatchToaster); - } - if (importResponse.errors.length > 0) { - const formattedErrors = importResponse.errors.map(e => - failedDetailed(e.rule_id, e.error.status_code, e.error.message) - ); - displayErrorToast(errorMessage, formattedErrors, dispatchToaster); - } - - importComplete(); - cleanupAndCloseModal(); - } catch (error) { - cleanupAndCloseModal(); - errorToToaster({ title: errorMessage, error, dispatchToaster }); - } - } - }, [selectedFiles, overwrite]); - - const handleCloseModal = useCallback(() => { - setSelectedFiles(null); - closeModal(); - }, [closeModal]); - - return ( - <> - {showModal && ( - - - - {title} - - - - -

{description}

-
- - - { - setSelectedFiles(files && files.length > 0 ? files : null); - }} - display={'large'} - fullWidth={true} - isLoading={isImporting} - /> - - {showCheckBox && ( - setOverwrite(!overwrite)} - /> - )} -
- - - {i18n.CANCEL_BUTTON} - - {submitBtnText} - - -
-
- )} - - ); -}; - -ImportDataModalComponent.displayName = 'ImportDataModalComponent'; - -export const ImportDataModal = React.memo(ImportDataModalComponent); - -ImportDataModal.displayName = 'ImportDataModal'; diff --git a/x-pack/plugins/siem/public/components/inspect/index.test.tsx b/x-pack/plugins/siem/public/components/inspect/index.test.tsx deleted file mode 100644 index 9492002717e2bf..00000000000000 --- a/x-pack/plugins/siem/public/components/inspect/index.test.tsx +++ /dev/null @@ -1,238 +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 euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import { mount } from 'enzyme'; -import React from 'react'; -import { ThemeProvider } from 'styled-components'; - -import { - TestProviderWithoutDragAndDrop, - mockGlobalState, - apolloClientObservable, -} from '../../mock'; -import { createStore, State } from '../../store'; -import { UpdateQueryParams, upsertQuery } from '../../store/inputs/helpers'; - -import { InspectButton, InspectButtonContainer, BUTTON_CLASS } from '.'; -import { cloneDeep } from 'lodash/fp'; - -describe('Inspect Button', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); - const refetch = jest.fn(); - const state: State = mockGlobalState; - const newQuery: UpdateQueryParams = { - inputId: 'global', - id: 'myQuery', - inspect: null, - loading: false, - refetch, - state: state.inputs, - }; - - let store = createStore(state, apolloClientObservable); - - describe('Render', () => { - beforeEach(() => { - const myState = cloneDeep(state); - myState.inputs = upsertQuery(newQuery); - store = createStore(myState, apolloClientObservable); - }); - test('Eui Empty Button', () => { - const wrapper = mount( - - - - ); - expect( - wrapper - .find('button[data-test-subj="inspect-empty-button"]') - .first() - .exists() - ).toBe(true); - }); - - test('it does NOT render the Eui Empty Button when timeline is timeline and compact is true', () => { - const wrapper = mount( - - - - ); - expect( - wrapper - .find('button[data-test-subj="inspect-empty-button"]') - .first() - .exists() - ).toBe(false); - }); - - test('Eui Icon Button', () => { - const wrapper = mount( - - - - ); - expect( - wrapper - .find('button[data-test-subj="inspect-icon-button"]') - .first() - .exists() - ).toBe(true); - }); - - test('renders the Icon Button when inputId does NOT equal global, but compact is true', () => { - const wrapper = mount( - - - - ); - expect( - wrapper - .find('button[data-test-subj="inspect-icon-button"]') - .first() - .exists() - ).toBe(true); - }); - - test('Eui Empty Button disabled', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find('.euiButtonIcon').get(0).props.disabled).toBe(true); - }); - - test('Eui Icon Button disabled', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find('.euiButtonIcon').get(0).props.disabled).toBe(true); - }); - - describe('InspectButtonContainer', () => { - test('it renders a transparent inspect button by default', async () => { - const wrapper = mount( - - - - - - ); - - expect(wrapper.find(`InspectButtonContainer`)).toHaveStyleRule('opacity', '0', { - modifier: `.${BUTTON_CLASS}`, - }); - }); - - test('it renders an opaque inspect button when it has mouse focus', async () => { - const wrapper = mount( - - - - - - ); - - expect(wrapper.find(`InspectButtonContainer`)).toHaveStyleRule('opacity', '1', { - modifier: `:hover .${BUTTON_CLASS}`, - }); - }); - }); - }); - - describe('Modal Inspect - happy path', () => { - beforeEach(() => { - const myState = cloneDeep(state); - const myQuery = cloneDeep(newQuery); - myQuery.inspect = { - dsl: ['my dsl'], - response: ['my response'], - }; - myState.inputs = upsertQuery(myQuery); - store = createStore(myState, apolloClientObservable); - }); - test('Open Inspect Modal', () => { - const wrapper = mount( - - - - - - ); - wrapper - .find('button[data-test-subj="inspect-icon-button"]') - .first() - .simulate('click'); - - wrapper.update(); - - expect(store.getState().inputs.global.queries[0].isInspected).toBe(true); - expect( - wrapper - .find('button[data-test-subj="modal-inspect-close"]') - .first() - .exists() - ).toBe(true); - }); - - test('Close Inspect Modal', () => { - const wrapper = mount( - - - - - - ); - wrapper - .find('button[data-test-subj="inspect-icon-button"]') - .first() - .simulate('click'); - - wrapper.update(); - - wrapper - .find('button[data-test-subj="modal-inspect-close"]') - .first() - .simulate('click'); - - wrapper.update(); - - expect(store.getState().inputs.global.queries[0].isInspected).toBe(false); - expect( - wrapper - .find('button[data-test-subj="modal-inspect-close"]') - .first() - .exists() - ).toBe(false); - }); - - test('Do not Open Inspect Modal if it is loading', () => { - const wrapper = mount( - - - - ); - store.getState().inputs.global.queries[0].loading = true; - wrapper - .find('button[data-test-subj="inspect-icon-button"]') - .first() - .simulate('click'); - - wrapper.update(); - - expect(store.getState().inputs.global.queries[0].isInspected).toBe(true); - expect( - wrapper - .find('button[data-test-subj="modal-inspect-close"]') - .first() - .exists() - ).toBe(false); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/ip/index.test.tsx b/x-pack/plugins/siem/public/components/ip/index.test.tsx deleted file mode 100644 index 209fc63c7355c6..00000000000000 --- a/x-pack/plugins/siem/public/components/ip/index.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. - */ - -import { shallow } from 'enzyme'; -import React from 'react'; - -import { TestProviders } from '../../mock/test_providers'; -import { useMountAppended } from '../../utils/use_mount_appended'; - -import { Ip } from '.'; - -describe('Port', () => { - const mount = useMountAppended(); - - test('renders correctly against snapshot', () => { - const wrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); - }); - - test('it renders the the ip address', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="formatted-ip"]') - .first() - .text() - ).toEqual('10.1.2.3'); - }); - - test('it hyperlinks to the network/ip page', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="draggable-content-destination.ip"]') - .find('a') - .first() - .props().href - ).toEqual('#/link-to/network/ip/10.1.2.3/source'); - }); -}); diff --git a/x-pack/plugins/siem/public/components/ip/index.tsx b/x-pack/plugins/siem/public/components/ip/index.tsx deleted file mode 100644 index 49237c3bb1bb96..00000000000000 --- a/x-pack/plugins/siem/public/components/ip/index.tsx +++ /dev/null @@ -1,36 +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 { FormattedFieldValue } from '../timeline/body/renderers/formatted_field'; - -export const SOURCE_IP_FIELD_NAME = 'source.ip'; -export const DESTINATION_IP_FIELD_NAME = 'destination.ip'; - -const IP_FIELD_TYPE = 'ip'; - -/** - * Renders text containing a draggable IP address (e.g. `source.ip`, - * `destination.ip`) that contains a hyperlink - */ -export const Ip = React.memo<{ - contextId: string; - eventId: string; - fieldName: string; - value?: string | null; -}>(({ contextId, eventId, fieldName, value }) => ( - -)); - -Ip.displayName = 'Ip'; diff --git a/x-pack/plugins/siem/public/components/ja3_fingerprint/index.test.tsx b/x-pack/plugins/siem/public/components/ja3_fingerprint/index.test.tsx deleted file mode 100644 index c4ea6ff63a0a77..00000000000000 --- a/x-pack/plugins/siem/public/components/ja3_fingerprint/index.test.tsx +++ /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. - */ - -import React from 'react'; - -import { TestProviders } from '../../mock'; -import { useMountAppended } from '../../utils/use_mount_appended'; - -import { Ja3Fingerprint } from '.'; - -describe('Ja3Fingerprint', () => { - const mount = useMountAppended(); - - test('renders the expected label', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="ja3-fingerprint-label"]') - .first() - .text() - ).toEqual('ja3'); - }); - - test('renders the fingerprint as text', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="ja3-fingerprint-link"]') - .first() - .text() - ).toEqual('fff799d91b7c01ae3fe6787cfc895552'); - }); - - test('it renders a hyperlink to an external site to compare the fingerprint against a known set of signatures', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="ja3-fingerprint-link"]') - .first() - .props().href - ).toEqual('https://sslbl.abuse.ch/ja3-fingerprints/fff799d91b7c01ae3fe6787cfc895552'); - }); -}); diff --git a/x-pack/plugins/siem/public/components/ja3_fingerprint/index.tsx b/x-pack/plugins/siem/public/components/ja3_fingerprint/index.tsx deleted file mode 100644 index 955a57576dc8eb..00000000000000 --- a/x-pack/plugins/siem/public/components/ja3_fingerprint/index.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import styled from 'styled-components'; - -import { DraggableBadge } from '../draggables'; -import { ExternalLinkIcon } from '../external_link_icon'; -import { Ja3FingerprintLink } from '../links'; - -import * as i18n from './translations'; - -export const JA3_HASH_FIELD_NAME = 'tls.fingerprints.ja3.hash'; - -const Ja3FingerprintLabel = styled.span` - margin-right: 5px; -`; - -Ja3FingerprintLabel.displayName = 'Ja3FingerprintLabel'; - -/** - * Renders a ja3 fingerprint, which enables (some) clients and servers communicating - * using TLS traffic to be identified, which is possible because SSL - * negotiations happen in the clear - */ -export const Ja3Fingerprint = React.memo<{ - eventId: string; - contextId: string; - fieldName: string; - value?: string | null; -}>(({ contextId, eventId, fieldName, value }) => ( - - - {i18n.JA3_FINGERPRINT_LABEL} - - - - -)); - -Ja3Fingerprint.displayName = 'Ja3Fingerprint'; diff --git a/x-pack/plugins/siem/public/components/last_event_time/index.test.tsx b/x-pack/plugins/siem/public/components/last_event_time/index.test.tsx deleted file mode 100644 index 69a795d0c8db78..00000000000000 --- a/x-pack/plugins/siem/public/components/last_event_time/index.test.tsx +++ /dev/null @@ -1,86 +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 { getEmptyValue } from '../empty_value'; -import { LastEventIndexKey } from '../../graphql/types'; -import { mockLastEventTimeQuery } from '../../containers/events/last_event_time/mock'; - -import { useMountAppended } from '../../utils/use_mount_appended'; -import { useLastEventTimeQuery } from '../../containers/events/last_event_time'; -import { TestProviders } from '../../mock'; - -import { LastEventTime } from '.'; - -const mockUseLastEventTimeQuery: jest.Mock = useLastEventTimeQuery as jest.Mock; -jest.mock('../../containers/events/last_event_time', () => ({ - useLastEventTimeQuery: jest.fn(), -})); - -describe('Last Event Time Stat', () => { - const mount = useMountAppended(); - - beforeEach(() => { - mockUseLastEventTimeQuery.mockReset(); - }); - - test('Loading', async () => { - mockUseLastEventTimeQuery.mockImplementation(() => ({ - loading: true, - lastSeen: null, - errorMessage: null, - })); - const wrapper = mount( - - - - ); - expect(wrapper.html()).toBe( - '' - ); - }); - test('Last seen', async () => { - mockUseLastEventTimeQuery.mockImplementation(() => ({ - loading: false, - lastSeen: mockLastEventTimeQuery[0].result.data!.source.LastEventTime.lastSeen, - errorMessage: mockLastEventTimeQuery[0].result.data!.source.LastEventTime.errorMessage, - })); - const wrapper = mount( - - - - ); - expect(wrapper.html()).toBe('Last event: 12 minutes ago'); - }); - test('Bad date time string', async () => { - mockUseLastEventTimeQuery.mockImplementation(() => ({ - loading: false, - lastSeen: 'something-invalid', - errorMessage: mockLastEventTimeQuery[0].result.data!.source.LastEventTime.errorMessage, - })); - const wrapper = mount( - - - - ); - - expect(wrapper.html()).toBe('something-invalid'); - }); - test('Null time string', async () => { - mockUseLastEventTimeQuery.mockImplementation(() => ({ - loading: false, - lastSeen: null, - errorMessage: mockLastEventTimeQuery[0].result.data!.source.LastEventTime.errorMessage, - })); - const wrapper = mount( - - - - ); - expect(wrapper.html()).toContain(getEmptyValue()); - }); -}); diff --git a/x-pack/plugins/siem/public/components/last_event_time/index.tsx b/x-pack/plugins/siem/public/components/last_event_time/index.tsx deleted file mode 100644 index 2493a1378e944a..00000000000000 --- a/x-pack/plugins/siem/public/components/last_event_time/index.tsx +++ /dev/null @@ -1,63 +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 { EuiIcon, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { memo } from 'react'; - -import { LastEventIndexKey } from '../../graphql/types'; -import { useLastEventTimeQuery } from '../../containers/events/last_event_time'; -import { getEmptyTagValue } from '../empty_value'; -import { FormattedRelativePreferenceDate } from '../formatted_date'; - -export interface LastEventTimeProps { - hostName?: string; - indexKey: LastEventIndexKey; - ip?: string; -} - -export const LastEventTime = memo(({ hostName, indexKey, ip }) => { - const { loading, lastSeen, errorMessage } = useLastEventTimeQuery( - indexKey, - { hostName, ip }, - 'default' - ); - - if (errorMessage != null) { - return ( - - - - ); - } - - return ( - <> - {loading && } - {!loading && lastSeen != null && new Date(lastSeen).toString() === 'Invalid Date' - ? lastSeen - : !loading && - lastSeen != null && ( - , - }} - /> - )} - {!loading && lastSeen == null && getEmptyTagValue()} - - ); -}); - -LastEventTime.displayName = 'LastEventTime'; diff --git a/x-pack/plugins/siem/public/components/links/index.test.tsx b/x-pack/plugins/siem/public/components/links/index.test.tsx deleted file mode 100644 index 214c0294f2cf42..00000000000000 --- a/x-pack/plugins/siem/public/components/links/index.test.tsx +++ /dev/null @@ -1,563 +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 { mount, shallow, ShallowWrapper } from 'enzyme'; -import React from 'react'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; - -import { encodeIpv6 } from '../../lib/helpers'; -import { useUiSetting$ } from '../../lib/kibana'; - -import { - GoogleLink, - HostDetailsLink, - IPDetailsLink, - ReputationLink, - WhoIsLink, - CertificateFingerprintLink, - Ja3FingerprintLink, - PortOrServiceNameLink, - DEFAULT_NUMBER_OF_LINK, - ExternalLink, -} from '.'; - -jest.mock('../../pages/overview/events_by_dataset'); - -jest.mock('../../lib/kibana', () => { - return { - useUiSetting$: jest.fn(), - }; -}); - -describe('Custom Links', () => { - const hostName = 'Host Name'; - const ipv4 = '192.0.2.255'; - const ipv6 = '2001:db8:ffff:ffff:ffff:ffff:ffff:ffff'; - const ipv6Encoded = encodeIpv6(ipv6); - - describe('HostDetailsLink', () => { - test('should render valid link to Host Details with hostName as the display text', () => { - const wrapper = mount(); - expect(wrapper.find('EuiLink').prop('href')).toEqual( - `#/link-to/hosts/${encodeURIComponent(hostName)}` - ); - expect(wrapper.text()).toEqual(hostName); - }); - - test('should render valid link to Host Details with child text as the display text', () => { - const wrapper = mount({hostName}); - expect(wrapper.find('EuiLink').prop('href')).toEqual( - `#/link-to/hosts/${encodeURIComponent(hostName)}` - ); - expect(wrapper.text()).toEqual(hostName); - }); - }); - - describe('IPDetailsLink', () => { - test('should render valid link to IP Details with ipv4 as the display text', () => { - const wrapper = mount(); - expect(wrapper.find('EuiLink').prop('href')).toEqual( - `#/link-to/network/ip/${encodeURIComponent(ipv4)}/source` - ); - expect(wrapper.text()).toEqual(ipv4); - }); - - test('should render valid link to IP Details with child text as the display text', () => { - const wrapper = mount({hostName}); - expect(wrapper.find('EuiLink').prop('href')).toEqual( - `#/link-to/network/ip/${encodeURIComponent(ipv4)}/source` - ); - expect(wrapper.text()).toEqual(hostName); - }); - - test('should render valid link to IP Details with ipv6 as the display text', () => { - const wrapper = mount(); - expect(wrapper.find('EuiLink').prop('href')).toEqual( - `#/link-to/network/ip/${encodeURIComponent(ipv6Encoded)}/source` - ); - expect(wrapper.text()).toEqual(ipv6); - }); - }); - - describe('GoogleLink', () => { - test('it renders text passed in as value', () => { - const wrapper = mountWithIntl( - {'Example Link'} - ); - expect(wrapper.text()).toEqual('Example Link'); - }); - - test('it renders props passed in as link', () => { - const wrapper = mountWithIntl( - {'Example Link'} - ); - expect(wrapper.find('a').prop('href')).toEqual( - 'https://www.google.com/search?q=http%3A%2F%2Fexample.com%2F' - ); - }); - - test("it encodes ", () => { - const wrapper = mountWithIntl( - alert('XSS')"}> - {'Example Link'} - - ); - expect(wrapper.find('a').prop('href')).toEqual( - "https://www.google.com/search?q=http%3A%2F%2Fexample.com%3Fq%3D%3Cscript%3Ealert('XSS')%3C%2Fscript%3E" - ); - }); - }); - - describe('External Link', () => { - const mockLink = 'https://www.virustotal.com/gui/search/'; - const mockLinkName = 'Link'; - let wrapper: ShallowWrapper; - - describe('render', () => { - beforeAll(() => { - wrapper = shallow( - - {mockLinkName} - - ); - }); - - test('it renders tooltip', () => { - expect(wrapper.find('[data-test-subj="externalLinkTooltip"]').exists()).toBeTruthy(); - }); - - test('it renders ExternalLinkIcon', () => { - expect(wrapper.find('[data-test-subj="externalLinkIcon"]').exists()).toBeTruthy(); - }); - - test('it renders correct url', () => { - expect(wrapper.find('[data-test-subj="externalLink"]').prop('href')).toEqual(mockLink); - }); - - test('it renders comma if id is given', () => { - expect(wrapper.find('[data-test-subj="externalLinkComma"]').exists()).toBeTruthy(); - }); - }); - - describe('not render', () => { - test('it should not render if childen prop is not given', () => { - wrapper = shallow( - - ); - expect(wrapper.find('[data-test-subj="externalLinkTooltip"]').exists()).toBeFalsy(); - }); - - test('it should not render if url prop is not given', () => { - wrapper = shallow( - - ); - expect(wrapper.find('[data-test-subj="externalLinkTooltip"]').exists()).toBeFalsy(); - }); - - test('it should not render if url prop is invalid', () => { - wrapper = shallow( - - ); - expect(wrapper.find('[data-test-subj="externalLinkTooltip"]').exists()).toBeFalsy(); - }); - - test('it should not render comma if id is not given', () => { - wrapper = shallow( - - {mockLinkName} - - ); - expect(wrapper.find('[data-test-subj="externalLinkComma"]').exists()).toBeFalsy(); - }); - - test('it should not render comma for the last item', () => { - wrapper = shallow( - - {mockLinkName} - - ); - expect(wrapper.find('[data-test-subj="externalLinkComma"]').exists()).toBeFalsy(); - }); - }); - - describe.each<[number, number, number, boolean]>([ - [0, 2, 5, true], - [1, 2, 5, false], - [2, 2, 5, false], - [3, 2, 5, false], - [4, 2, 5, false], - [5, 2, 5, false], - ])( - 'renders Comma when overflowIndex is smaller than allItems limit', - (idx, overflowIndexStart, allItemsLimit, showComma) => { - beforeAll(() => { - wrapper = shallow( - - {mockLinkName} - - ); - }); - - test(`should render Comma if current id (${idx}) is smaller than the index of last visible item`, () => { - expect(wrapper.find('[data-test-subj="externalLinkComma"]').exists()).toEqual(showComma); - }); - } - ); - - describe.each<[number, number, number, boolean]>([ - [0, 5, 4, true], - [1, 5, 4, true], - [2, 5, 4, true], - [3, 5, 4, false], - [4, 5, 4, false], - [5, 5, 4, false], - ])( - 'When overflowIndex is grater than allItems limit', - (idx, overflowIndexStart, allItemsLimit, showComma) => { - beforeAll(() => { - wrapper = shallow( - - {mockLinkName} - - ); - }); - - test(`Current item (${idx}) should render Comma execpt the last item`, () => { - expect(wrapper.find('[data-test-subj="externalLinkComma"]').exists()).toEqual(showComma); - }); - } - ); - - describe.each<[number, number, number, boolean]>([ - [0, 5, 5, true], - [1, 5, 5, true], - [2, 5, 5, true], - [3, 5, 5, true], - [4, 5, 5, false], - [5, 5, 5, false], - ])( - 'when overflowIndex equals to allItems limit', - (idx, overflowIndexStart, allItemsLimit, showComma) => { - beforeAll(() => { - wrapper = shallow( - - {mockLinkName} - - ); - }); - - test(`Current item (${idx}) should render Comma correctly`, () => { - expect(wrapper.find('[data-test-subj="externalLinkComma"]').exists()).toEqual(showComma); - }); - } - ); - }); - - describe('ReputationLink', () => { - const mockCustomizedReputationLinks = [ - { name: 'Link 1', url_template: 'https://www.virustotal.com/gui/search/{{ip}}' }, - { - name: 'Link 2', - url_template: 'https://talosintelligence.com/reputation_center/lookup?search={{ip}}', - }, - { name: 'Link 3', url_template: 'https://www.virustotal.com/gui/search/{{ip}}' }, - { - name: 'Link 4', - url_template: 'https://talosintelligence.com/reputation_center/lookup?search={{ip}}', - }, - { name: 'Link 5', url_template: 'https://www.virustotal.com/gui/search/{{ip}}' }, - { - name: 'Link 6', - url_template: 'https://talosintelligence.com/reputation_center/lookup?search={{ip}}', - }, - ]; - const mockDefaultReputationLinks = mockCustomizedReputationLinks.slice(0, 2); - - describe('links property', () => { - beforeEach(() => { - (useUiSetting$ as jest.Mock).mockReset(); - (useUiSetting$ as jest.Mock).mockReturnValue([mockDefaultReputationLinks]); - }); - - test('it renders default link text', () => { - const wrapper = shallow(); - wrapper.find('[data-test-subj="externalLink"]').forEach((node, idx) => { - expect(node.at(idx).text()).toEqual(mockDefaultReputationLinks[idx].name); - }); - }); - - test('it renders customized link text', () => { - (useUiSetting$ as jest.Mock).mockReset(); - (useUiSetting$ as jest.Mock).mockReturnValue([mockCustomizedReputationLinks]); - const wrapper = shallow(); - wrapper.find('[data-test-subj="externalLink"]').forEach((node, idx) => { - expect(node.at(idx).text()).toEqual(mockCustomizedReputationLinks[idx].name); - }); - }); - - test('it renders correct href', () => { - const wrapper = shallow(); - wrapper.find('[data-test-subj="externalLink"]').forEach((node, idx) => { - expect(node.prop('href')).toEqual( - mockDefaultReputationLinks[idx].url_template.replace('{{ip}}', '192.0.2.0') - ); - }); - }); - }); - - describe('number of links', () => { - beforeAll(() => { - (useUiSetting$ as jest.Mock).mockReset(); - (useUiSetting$ as jest.Mock).mockReturnValue([mockCustomizedReputationLinks]); - }); - - afterEach(() => { - (useUiSetting$ as jest.Mock).mockClear(); - }); - - test('it renders correct number of links by default', () => { - const wrapper = mountWithIntl(); - expect(wrapper.find('[data-test-subj="externalLinkComponent"]')).toHaveLength( - DEFAULT_NUMBER_OF_LINK - ); - }); - - test('it renders correct number of tooltips by default', () => { - const wrapper = mountWithIntl(); - expect(wrapper.find('[data-test-subj="externalLinkTooltip"]')).toHaveLength( - DEFAULT_NUMBER_OF_LINK - ); - }); - - test('it renders correct number of visible link', () => { - (useUiSetting$ as jest.Mock).mockReset(); - (useUiSetting$ as jest.Mock).mockReturnValue([mockCustomizedReputationLinks]); - - const wrapper = mountWithIntl( - - ); - expect(wrapper.find('[data-test-subj="externalLinkComponent"]')).toHaveLength(1); - }); - - test('it renders correct number of tooltips for visible links', () => { - (useUiSetting$ as jest.Mock).mockReset(); - (useUiSetting$ as jest.Mock).mockReturnValue([mockCustomizedReputationLinks]); - - const wrapper = mountWithIntl( - - ); - expect(wrapper.find('[data-test-subj="externalLinkTooltip"]')).toHaveLength(1); - }); - }); - - describe('invalid customized links', () => { - const mockInvalidLinksEmptyObj = [{}]; - const mockInvalidLinksNoName = [ - { url_template: 'https://talosintelligence.com/reputation_center/lookup?search={{ip}}' }, - ]; - const mockInvalidLinksNoUrl = [{ name: 'Link 1' }]; - const mockInvalidUrl = [{ name: 'Link 1', url_template: "" }]; - afterEach(() => { - (useUiSetting$ as jest.Mock).mockReset(); - }); - - test('it filters empty object', () => { - (useUiSetting$ as jest.Mock).mockReturnValue([mockInvalidLinksEmptyObj]); - - const wrapper = mountWithIntl( - - ); - expect(wrapper.find('[data-test-subj="externalLink"]')).toHaveLength(0); - }); - - test('it filters object without name property', () => { - (useUiSetting$ as jest.Mock).mockReturnValue([mockInvalidLinksNoName]); - - const wrapper = mountWithIntl( - - ); - expect(wrapper.find('[data-test-subj="externalLink"]')).toHaveLength(0); - }); - - test('it filters object without url_template property', () => { - (useUiSetting$ as jest.Mock).mockReturnValue([mockInvalidLinksNoUrl]); - - const wrapper = mountWithIntl( - - ); - expect(wrapper.find('[data-test-subj="externalLink"]')).toHaveLength(0); - }); - - test('it filters object with invalid url', () => { - (useUiSetting$ as jest.Mock).mockReturnValue([mockInvalidUrl]); - - const wrapper = mountWithIntl( - - ); - expect(wrapper.find('[data-test-subj="externalLink"]')).toHaveLength(0); - }); - }); - - describe('external icon', () => { - beforeAll(() => { - (useUiSetting$ as jest.Mock).mockReset(); - (useUiSetting$ as jest.Mock).mockReturnValue([mockCustomizedReputationLinks]); - }); - - afterEach(() => { - (useUiSetting$ as jest.Mock).mockClear(); - }); - - test('it renders correct number of external icons by default', () => { - const wrapper = mountWithIntl(); - expect(wrapper.find('[data-test-subj="externalLinkIcon"]')).toHaveLength(5); - }); - - test('it renders correct number of external icons', () => { - const wrapper = mountWithIntl( - - ); - expect(wrapper.find('[data-test-subj="externalLinkIcon"]')).toHaveLength(1); - }); - }); - }); - - describe('WhoisLink', () => { - test('it renders ip passed in as domain', () => { - const wrapper = mountWithIntl({'Example Link'}); - expect(wrapper.text()).toEqual('Example Link'); - }); - - test('it renders correct href', () => { - const wrapper = mountWithIntl({'Example Link'} ); - expect(wrapper.find('a').prop('href')).toEqual('https://www.iana.org/whois?q=192.0.2.0'); - }); - - test("it encodes ", () => { - const wrapper = mountWithIntl( - alert('XSS')"}>{'Example Link'} - ); - expect(wrapper.find('a').prop('href')).toEqual( - "https://www.iana.org/whois?q=%3Cscript%3Ealert('XSS')%3C%2Fscript%3E" - ); - }); - }); - - describe('CertificateFingerprintLink', () => { - test('it renders link text', () => { - const wrapper = mountWithIntl( - - {'Example Link'} - - ); - expect(wrapper.text()).toEqual('Example Link'); - }); - - test('it renders correct href', () => { - const wrapper = mountWithIntl( - - {'Example Link'} - - ); - expect(wrapper.find('a').prop('href')).toEqual( - 'https://sslbl.abuse.ch/ssl-certificates/sha1/abcd' - ); - }); - - test("it encodes ", () => { - const wrapper = mountWithIntl( - alert('XSS')"}> - {'Example Link'} - - ); - expect(wrapper.find('a').prop('href')).toEqual( - "https://sslbl.abuse.ch/ssl-certificates/sha1/%3Cscript%3Ealert('XSS')%3C%2Fscript%3E" - ); - }); - }); - - describe('Ja3FingerprintLink', () => { - test('it renders link text', () => { - const wrapper = mountWithIntl( - {'Example Link'} - ); - expect(wrapper.text()).toEqual('Example Link'); - }); - - test('it renders correct href', () => { - const wrapper = mountWithIntl( - {'Example Link'} - ); - expect(wrapper.find('a').prop('href')).toEqual( - 'https://sslbl.abuse.ch/ja3-fingerprints/abcd' - ); - }); - - test("it encodes ", () => { - const wrapper = mountWithIntl( - alert('XSS')"}> - {'Example Link'} - - ); - expect(wrapper.find('a').prop('href')).toEqual( - "https://sslbl.abuse.ch/ja3-fingerprints/%3Cscript%3Ealert('XSS')%3C%2Fscript%3E" - ); - }); - }); - - describe('PortOrServiceNameLink', () => { - test('it renders link text', () => { - const wrapper = mountWithIntl( - {'Example Link'} - ); - expect(wrapper.text()).toEqual('Example Link'); - }); - - test('it renders correct href when port is a number', () => { - const wrapper = mountWithIntl( - {'Example Link'} - ); - expect(wrapper.find('a').prop('href')).toEqual( - 'https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=443' - ); - }); - - test('it renders correct href when port is a string', () => { - const wrapper = mountWithIntl( - {'Example Link'} - ); - expect(wrapper.find('a').prop('href')).toEqual( - 'https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=80' - ); - }); - - test("it encodes ", () => { - const wrapper = mountWithIntl( - alert('XSS')"}> - {'Example Link'} - - ); - expect(wrapper.find('a').prop('href')).toEqual( - "https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=%3Cscript%3Ealert('XSS')%3C%2Fscript%3E" - ); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/links/index.tsx b/x-pack/plugins/siem/public/components/links/index.tsx deleted file mode 100644 index 6d473f47217105..00000000000000 --- a/x-pack/plugins/siem/public/components/links/index.tsx +++ /dev/null @@ -1,312 +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 { EuiLink, EuiToolTip, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React, { useMemo } from 'react'; -import { isNil } from 'lodash/fp'; -import styled from 'styled-components'; - -import { IP_REPUTATION_LINKS_SETTING } from '../../../common/constants'; -import { - DefaultFieldRendererOverflow, - DEFAULT_MORE_MAX_HEIGHT, -} from '../field_renderers/field_renderers'; -import { encodeIpv6 } from '../../lib/helpers'; -import { - getCaseDetailsUrl, - getHostDetailsUrl, - getIPDetailsUrl, - getCreateCaseUrl, -} from '../link_to'; -import { FlowTarget, FlowTargetSourceDest } from '../../graphql/types'; -import { useUiSetting$ } from '../../lib/kibana'; -import { isUrlInvalid } from '../../pages/detection_engine/rules/components/step_about_rule/helpers'; -import { ExternalLinkIcon } from '../external_link_icon'; -import { navTabs } from '../../pages/home/home_navigations'; -import { useGetUrlSearch } from '../navigation/use_get_url_search'; - -import * as i18n from './translations'; - -export const DEFAULT_NUMBER_OF_LINK = 5; - -// Internal Links -const HostDetailsLinkComponent: React.FC<{ children?: React.ReactNode; hostName: string }> = ({ - children, - hostName, -}) => ( - - {children ? children : hostName} - -); - -const whitelistUrlSchemes = ['http://', 'https://']; -export const ExternalLink = React.memo<{ - url: string; - children?: React.ReactNode; - idx?: number; - overflowIndexStart?: number; - allItemsLimit?: number; -}>( - ({ - url, - children, - idx, - overflowIndexStart = DEFAULT_NUMBER_OF_LINK, - allItemsLimit = DEFAULT_NUMBER_OF_LINK, - }) => { - const lastVisibleItemIndex = overflowIndexStart - 1; - const lastItemIndex = allItemsLimit - 1; - const lastIndexToShow = Math.max(0, Math.min(lastVisibleItemIndex, lastItemIndex)); - const inWhitelist = whitelistUrlSchemes.some(scheme => url.indexOf(scheme) === 0); - return url && inWhitelist && !isUrlInvalid(url) && children ? ( - - - {children} - - {!isNil(idx) && idx < lastIndexToShow && } - - - ) : null; - } -); - -ExternalLink.displayName = 'ExternalLink'; - -export const HostDetailsLink = React.memo(HostDetailsLinkComponent); - -const IPDetailsLinkComponent: React.FC<{ - children?: React.ReactNode; - ip: string; - flowTarget?: FlowTarget | FlowTargetSourceDest; -}> = ({ children, ip, flowTarget = FlowTarget.source }) => ( - - {children ? children : ip} - -); - -export const IPDetailsLink = React.memo(IPDetailsLinkComponent); - -const CaseDetailsLinkComponent: React.FC<{ - children?: React.ReactNode; - detailName: string; - title?: string; -}> = ({ children, detailName, title }) => { - const search = useGetUrlSearch(navTabs.case); - - return ( - - {children ? children : detailName} - - ); -}; -export const CaseDetailsLink = React.memo(CaseDetailsLinkComponent); -CaseDetailsLink.displayName = 'CaseDetailsLink'; - -export const CreateCaseLink = React.memo<{ children: React.ReactNode }>(({ children }) => { - const search = useGetUrlSearch(navTabs.case); - return {children}; -}); - -CreateCaseLink.displayName = 'CreateCaseLink'; - -// External Links -export const GoogleLink = React.memo<{ children?: React.ReactNode; link: string }>( - ({ children, link }) => ( - - {children ? children : link} - - ) -); - -GoogleLink.displayName = 'GoogleLink'; - -export const PortOrServiceNameLink = React.memo<{ - children?: React.ReactNode; - portOrServiceName: number | string; -}>(({ children, portOrServiceName }) => ( - - {children ? children : portOrServiceName} - -)); - -PortOrServiceNameLink.displayName = 'PortOrServiceNameLink'; - -export const Ja3FingerprintLink = React.memo<{ - children?: React.ReactNode; - ja3Fingerprint: string; -}>(({ children, ja3Fingerprint }) => ( - - {children ? children : ja3Fingerprint} - -)); - -Ja3FingerprintLink.displayName = 'Ja3FingerprintLink'; - -export const CertificateFingerprintLink = React.memo<{ - children?: React.ReactNode; - certificateFingerprint: string; -}>(({ children, certificateFingerprint }) => ( - - {children ? children : certificateFingerprint} - -)); - -CertificateFingerprintLink.displayName = 'CertificateFingerprintLink'; - -enum DefaultReputationLink { - 'virustotal.com' = 'virustotal.com', - 'talosIntelligence.com' = 'talosIntelligence.com', -} - -export interface ReputationLinkSetting { - name: string; - url_template: string; -} - -function isDefaultReputationLink(name: string): name is DefaultReputationLink { - return ( - name === DefaultReputationLink['virustotal.com'] || - name === DefaultReputationLink['talosIntelligence.com'] - ); -} -const isReputationLink = ( - rowItem: string | ReputationLinkSetting -): rowItem is ReputationLinkSetting => - (rowItem as ReputationLinkSetting).url_template !== undefined && - (rowItem as ReputationLinkSetting).name !== undefined; - -export const Comma = styled('span')` - margin-right: 5px; - margin-left: 5px; - &::after { - content: ' ,'; - } -`; - -Comma.displayName = 'Comma'; - -const defaultNameMapping: Record = { - [DefaultReputationLink['virustotal.com']]: i18n.VIEW_VIRUS_TOTAL, - [DefaultReputationLink['talosIntelligence.com']]: i18n.VIEW_TALOS_INTELLIGENCE, -}; - -const ReputationLinkComponent: React.FC<{ - overflowIndexStart?: number; - allItemsLimit?: number; - showDomain?: boolean; - domain: string; - direction?: 'row' | 'column'; -}> = ({ - overflowIndexStart = DEFAULT_NUMBER_OF_LINK, - allItemsLimit = DEFAULT_NUMBER_OF_LINK, - showDomain = false, - domain, - direction = 'row', -}) => { - const [ipReputationLinksSetting] = useUiSetting$( - IP_REPUTATION_LINKS_SETTING - ); - - const ipReputationLinks: ReputationLinkSetting[] = useMemo( - () => - ipReputationLinksSetting - ?.slice(0, allItemsLimit) - .filter( - ({ url_template, name }) => - !isNil(url_template) && !isNil(name) && !isUrlInvalid(url_template) - ) - .map(({ name, url_template }: { name: string; url_template: string }) => ({ - name: isDefaultReputationLink(name) ? defaultNameMapping[name] : name, - url_template: url_template.replace(`{{ip}}`, encodeURIComponent(domain)), - })), - [ipReputationLinksSetting, domain, defaultNameMapping, allItemsLimit] - ); - - return ipReputationLinks?.length > 0 ? ( -
- - - {ipReputationLinks - ?.slice(0, overflowIndexStart) - .map(({ name, url_template: urlTemplate }: ReputationLinkSetting, id) => ( - - <>{showDomain ? domain : name ?? domain} - - ))} - - - - { - return ( - isReputationLink(rowItem) && ( - - <>{rowItem.name ?? domain} - - ) - ); - }} - moreMaxHeight={DEFAULT_MORE_MAX_HEIGHT} - overflowIndexStart={overflowIndexStart} - /> - - -
- ) : null; -}; - -ReputationLinkComponent.displayName = 'ReputationLinkComponent'; - -export const ReputationLink = React.memo(ReputationLinkComponent); - -export const WhoIsLink = React.memo<{ children?: React.ReactNode; domain: string }>( - ({ children, domain }) => ( - - {children ? children : domain} - - ) -); - -WhoIsLink.displayName = 'WhoIsLink'; diff --git a/x-pack/plugins/siem/public/components/links/translations.ts b/x-pack/plugins/siem/public/components/links/translations.ts deleted file mode 100644 index bed867cd5bf504..00000000000000 --- a/x-pack/plugins/siem/public/components/links/translations.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export * from '../page/network/ip_overview/translations'; - -export const CASE_DETAILS_LINK_ARIA = (detailName: string) => - i18n.translate('xpack.siem.case.caseTable.caseDetailsLinkAria', { - values: { detailName }, - defaultMessage: 'click to visit case with title {detailName}', - }); diff --git a/x-pack/plugins/siem/public/components/markdown_editor/form.tsx b/x-pack/plugins/siem/public/components/markdown_editor/form.tsx deleted file mode 100644 index 17c321b15418c9..00000000000000 --- a/x-pack/plugins/siem/public/components/markdown_editor/form.tsx +++ /dev/null @@ -1,64 +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 { EuiFormRow } from '@elastic/eui'; -import React, { useCallback } from 'react'; - -import { FieldHook, getFieldValidityAndErrorMessage } from '../../shared_imports'; -import { CursorPosition, MarkdownEditor } from '.'; - -interface IMarkdownEditorForm { - bottomRightContent?: React.ReactNode; - dataTestSubj: string; - field: FieldHook; - idAria: string; - isDisabled: boolean; - onCursorPositionUpdate?: (cursorPosition: CursorPosition) => void; - placeholder?: string; - topRightContent?: React.ReactNode; -} -export const MarkdownEditorForm = ({ - bottomRightContent, - dataTestSubj, - field, - idAria, - isDisabled = false, - onCursorPositionUpdate, - placeholder, - topRightContent, -}: IMarkdownEditorForm) => { - const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); - - const handleContentChange = useCallback( - (newContent: string) => { - field.setValue(newContent); - }, - [field] - ); - - return ( - - - - ); -}; diff --git a/x-pack/plugins/siem/public/components/matrix_histogram/index.test.tsx b/x-pack/plugins/siem/public/components/matrix_histogram/index.test.tsx deleted file mode 100644 index 3b8a43a0f395a8..00000000000000 --- a/x-pack/plugins/siem/public/components/matrix_histogram/index.test.tsx +++ /dev/null @@ -1,134 +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. - */ - -/* eslint-disable react/display-name */ - -import { mount, ReactWrapper } from 'enzyme'; -import React from 'react'; - -import { MatrixHistogram } from '.'; -import { useQuery } from '../../containers/matrix_histogram'; -import { HistogramType } from '../../graphql/types'; -jest.mock('../../lib/kibana'); - -jest.mock('./matrix_loader', () => { - return { - MatrixLoader: () => { - return
; - }, - }; -}); - -jest.mock('../header_section', () => { - return { - HeaderSection: () =>
, - }; -}); - -jest.mock('../charts/barchart', () => { - return { - BarChart: () =>
, - }; -}); - -jest.mock('../../containers/matrix_histogram', () => { - return { - useQuery: jest.fn(), - }; -}); - -jest.mock('../../components/matrix_histogram/utils', () => { - return { - getBarchartConfigs: jest.fn(), - getCustomChartData: jest.fn().mockReturnValue(true), - }; -}); - -describe('Matrix Histogram Component', () => { - let wrapper: ReactWrapper; - - const mockMatrixOverTimeHistogramProps = { - defaultIndex: ['defaultIndex'], - defaultStackByOption: { text: 'text', value: 'value' }, - endDate: new Date('2019-07-18T20:00:00.000Z').valueOf(), - errorMessage: 'error', - histogramType: HistogramType.alerts, - id: 'mockId', - isInspected: false, - isPtrIncluded: false, - setQuery: jest.fn(), - skip: false, - sourceId: 'default', - stackByField: 'mockStackByField', - stackByOptions: [{ text: 'text', value: 'value' }], - startDate: new Date('2019-07-18T19:00: 00.000Z').valueOf(), - subtitle: 'mockSubtitle', - totalCount: -1, - title: 'mockTitle', - dispatchSetAbsoluteRangeDatePicker: jest.fn(), - }; - - beforeAll(() => { - (useQuery as jest.Mock).mockReturnValue({ - data: null, - loading: false, - inspect: false, - totalCount: null, - }); - wrapper = mount(); - }); - describe('on initial load', () => { - test('it renders MatrixLoader', () => { - expect(wrapper.html()).toMatchSnapshot(); - expect(wrapper.find('MatrixLoader').exists()).toBe(true); - }); - }); - - describe('spacer', () => { - test('it renders a spacer by default', () => { - expect(wrapper.find('[data-test-subj="spacer"]').exists()).toBe(true); - }); - - test('it does NOT render a spacer when showSpacer is false', () => { - wrapper = mount(); - expect(wrapper.find('[data-test-subj="spacer"]').exists()).toBe(false); - }); - }); - - describe('not initial load', () => { - beforeAll(() => { - (useQuery as jest.Mock).mockReturnValue({ - data: [ - { x: 1, y: 2, g: 'g1' }, - { x: 2, y: 4, g: 'g1' }, - { x: 3, y: 6, g: 'g1' }, - { x: 1, y: 1, g: 'g2' }, - { x: 2, y: 3, g: 'g2' }, - { x: 3, y: 5, g: 'g2' }, - ], - loading: false, - inspect: false, - totalCount: 1, - }); - wrapper.setProps({ endDate: 100 }); - wrapper.update(); - }); - test('it renders no MatrixLoader', () => { - expect(wrapper.html()).toMatchSnapshot(); - expect(wrapper.find(`MatrixLoader`).exists()).toBe(false); - }); - - test('it shows BarChart if data available', () => { - expect(wrapper.find(`.barchart`).exists()).toBe(true); - }); - }); - - describe('select dropdown', () => { - test('should be hidden if only one option is provided', () => { - expect(wrapper.find('EuiSelect').exists()).toBe(false); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/matrix_histogram/index.tsx b/x-pack/plugins/siem/public/components/matrix_histogram/index.tsx deleted file mode 100644 index ba3cb4f62af864..00000000000000 --- a/x-pack/plugins/siem/public/components/matrix_histogram/index.tsx +++ /dev/null @@ -1,273 +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, { useState, useEffect, useCallback, useMemo } from 'react'; -import { Position } from '@elastic/charts'; -import styled from 'styled-components'; - -import { EuiFlexGroup, EuiFlexItem, EuiProgress, EuiSelect, EuiSpacer } from '@elastic/eui'; -import { noop } from 'lodash/fp'; -import { compose } from 'redux'; -import { connect } from 'react-redux'; -import * as i18n from './translations'; -import { BarChart } from '../charts/barchart'; -import { HeaderSection } from '../header_section'; -import { MatrixLoader } from './matrix_loader'; -import { Panel } from '../panel'; -import { getBarchartConfigs, getCustomChartData } from './utils'; -import { useQuery } from '../../containers/matrix_histogram'; -import { - MatrixHistogramProps, - MatrixHistogramOption, - HistogramAggregation, - MatrixHistogramQueryProps, -} from './types'; -import { InspectButtonContainer } from '../inspect'; - -import { State, inputsSelectors, hostsModel, networkModel } from '../../store'; - -import { - MatrixHistogramMappingTypes, - GetTitle, - GetSubTitle, -} from '../../components/matrix_histogram/types'; -import { SetQuery } from '../../pages/hosts/navigation/types'; -import { QueryTemplateProps } from '../../containers/query_template'; -import { setAbsoluteRangeDatePicker } from '../../store/inputs/actions'; -import { InputsModelId } from '../../store/inputs/constants'; -import { HistogramType } from '../../graphql/types'; - -export interface OwnProps extends QueryTemplateProps { - defaultStackByOption: MatrixHistogramOption; - errorMessage: string; - headerChildren?: React.ReactNode; - hideHistogramIfEmpty?: boolean; - histogramType: HistogramType; - id: string; - indexToAdd?: string[] | null; - legendPosition?: Position; - mapping?: MatrixHistogramMappingTypes; - showSpacer?: boolean; - setQuery: SetQuery; - setAbsoluteRangeDatePickerTarget?: InputsModelId; - showLegend?: boolean; - stackByOptions: MatrixHistogramOption[]; - subtitle?: string | GetSubTitle; - title: string | GetTitle; - type: hostsModel.HostsType | networkModel.NetworkType; -} - -const DEFAULT_PANEL_HEIGHT = 300; - -const HeaderChildrenFlexItem = styled(EuiFlexItem)` - margin-left: 24px; -`; - -// @ts-ignore - the EUI type definitions for Panel do no play nice with styled-components -const HistogramPanel = styled(Panel)<{ height?: number }>` - display: flex; - flex-direction: column; - ${({ height }) => (height != null ? `height: ${height}px;` : '')} -`; - -export const MatrixHistogramComponent: React.FC = ({ - chartHeight, - defaultStackByOption, - endDate, - errorMessage, - filterQuery, - headerChildren, - histogramType, - hideHistogramIfEmpty = false, - id, - indexToAdd, - isInspected, - legendPosition, - mapping, - panelHeight = DEFAULT_PANEL_HEIGHT, - setAbsoluteRangeDatePickerTarget = 'global', - setQuery, - showLegend, - showSpacer = true, - stackByOptions, - startDate, - subtitle, - title, - titleSize, - dispatchSetAbsoluteRangeDatePicker, - yTickFormatter, -}) => { - const barchartConfigs = useMemo( - () => - getBarchartConfigs({ - chartHeight, - from: startDate, - legendPosition, - to: endDate, - onBrushEnd: ({ x }) => { - if (!x) { - return; - } - const [from, to] = x; - dispatchSetAbsoluteRangeDatePicker({ - id: setAbsoluteRangeDatePickerTarget, - from, - to, - }); - }, - yTickFormatter, - showLegend, - }), - [ - chartHeight, - startDate, - legendPosition, - endDate, - dispatchSetAbsoluteRangeDatePicker, - yTickFormatter, - showLegend, - ] - ); - const [isInitialLoading, setIsInitialLoading] = useState(true); - const [selectedStackByOption, setSelectedStackByOption] = useState( - defaultStackByOption - ); - const setSelectedChartOptionCallback = useCallback( - (event: React.ChangeEvent) => { - setSelectedStackByOption( - stackByOptions.find(co => co.value === event.target.value) ?? defaultStackByOption - ); - }, - [] - ); - - const { data, loading, inspect, totalCount, refetch = noop } = useQuery<{}, HistogramAggregation>( - { - endDate, - errorMessage, - filterQuery, - histogramType, - indexToAdd, - startDate, - isInspected, - stackByField: selectedStackByOption.value, - } - ); - - const titleWithStackByField = useMemo( - () => (title != null && typeof title === 'function' ? title(selectedStackByOption) : title), - [title, selectedStackByOption] - ); - const subtitleWithCounts = useMemo(() => { - if (isInitialLoading) { - return null; - } - - if (typeof subtitle === 'function') { - return totalCount >= 0 ? subtitle(totalCount) : null; - } - - return subtitle; - }, [isInitialLoading, subtitle, totalCount]); - const hideHistogram = useMemo(() => (totalCount <= 0 && hideHistogramIfEmpty ? true : false), [ - totalCount, - hideHistogramIfEmpty, - ]); - const barChartData = useMemo(() => getCustomChartData(data, mapping), [data, mapping]); - - useEffect(() => { - if (!loading && !isInitialLoading) { - setQuery({ id, inspect, loading, refetch }); - } - - if (isInitialLoading && !!barChartData && data) { - setIsInitialLoading(false); - } - }, [ - setQuery, - id, - inspect, - loading, - refetch, - isInitialLoading, - barChartData, - data, - setIsInitialLoading, - ]); - - if (hideHistogram) { - return null; - } - - return ( - <> - - - {loading && !isInitialLoading && ( - - )} - - - - - {stackByOptions.length > 1 && ( - - )} - - {headerChildren} - - - - {isInitialLoading ? ( - - ) : ( - - )} - - - {showSpacer && } - - ); -}; - -export const MatrixHistogram = React.memo(MatrixHistogramComponent); - -const makeMapStateToProps = () => { - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { type, id }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - isInspected, - }; - }; - return mapStateToProps; -}; - -export const MatrixHistogramContainer = compose>( - connect(makeMapStateToProps, { - dispatchSetAbsoluteRangeDatePicker: setAbsoluteRangeDatePicker, - }) -)(MatrixHistogram); diff --git a/x-pack/plugins/siem/public/components/matrix_histogram/types.ts b/x-pack/plugins/siem/public/components/matrix_histogram/types.ts deleted file mode 100644 index c59775ad325d07..00000000000000 --- a/x-pack/plugins/siem/public/components/matrix_histogram/types.ts +++ /dev/null @@ -1,143 +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 { EuiTitleSize } from '@elastic/eui'; -import { ScaleType, Position, TickFormatter } from '@elastic/charts'; -import { ActionCreator } from 'redux'; -import { ESQuery } from '../../../common/typed_json'; -import { SetQuery } from '../../pages/hosts/navigation/types'; -import { InputsModelId } from '../../store/inputs/constants'; -import { HistogramType } from '../../graphql/types'; -import { UpdateDateRange } from '../charts/common'; - -export type MatrixHistogramMappingTypes = Record< - string, - { key: string; value: null; color?: string | undefined } ->; -export interface MatrixHistogramOption { - text: string; - value: string; -} - -export type GetSubTitle = (count: number) => string; -export type GetTitle = (matrixHistogramOption: MatrixHistogramOption) => string; - -export interface MatrixHisrogramConfigs { - defaultStackByOption: MatrixHistogramOption; - errorMessage: string; - hideHistogramIfEmpty?: boolean; - histogramType: HistogramType; - legendPosition?: Position; - mapping?: MatrixHistogramMappingTypes; - stackByOptions: MatrixHistogramOption[]; - subtitle?: string | GetSubTitle; - title: string | GetTitle; - titleSize?: EuiTitleSize; -} - -interface MatrixHistogramBasicProps { - chartHeight?: number; - defaultIndex: string[]; - defaultStackByOption: MatrixHistogramOption; - dispatchSetAbsoluteRangeDatePicker: ActionCreator<{ - id: InputsModelId; - from: number; - to: number; - }>; - endDate: number; - headerChildren?: React.ReactNode; - hideHistogramIfEmpty?: boolean; - id: string; - legendPosition?: Position; - mapping?: MatrixHistogramMappingTypes; - panelHeight?: number; - setQuery: SetQuery; - startDate: number; - stackByOptions: MatrixHistogramOption[]; - subtitle?: string | GetSubTitle; - title?: string | GetTitle; - titleSize?: EuiTitleSize; -} - -export interface MatrixHistogramQueryProps { - endDate: number; - errorMessage: string; - filterQuery?: ESQuery | string | undefined; - setAbsoluteRangeDatePicker?: ActionCreator<{ - id: InputsModelId; - from: number; - to: number; - }>; - setAbsoluteRangeDatePickerTarget?: InputsModelId; - stackByField: string; - startDate: number; - indexToAdd?: string[] | null; - isInspected: boolean; - histogramType: HistogramType; -} - -export interface MatrixHistogramProps extends MatrixHistogramBasicProps { - scaleType?: ScaleType; - yTickFormatter?: (value: number) => string; - showLegend?: boolean; - showSpacer?: boolean; - legendPosition?: Position; -} - -export interface HistogramBucket { - key_as_string: string; - key: number; - doc_count: number; -} -export interface GroupBucket { - key: string; - signals: { - buckets: HistogramBucket[]; - }; -} - -export interface HistogramAggregation { - histogramAgg: { - buckets: GroupBucket[]; - }; -} - -export interface BarchartConfigs { - series: { - xScaleType: ScaleType; - yScaleType: ScaleType; - stackAccessors: string[]; - }; - axis: { - xTickFormatter: TickFormatter; - yTickFormatter: TickFormatter; - tickSize: number; - }; - settings: { - legendPosition: Position; - onBrushEnd: UpdateDateRange; - showLegend: boolean; - showLegendExtra: boolean; - theme: { - scales: { - barsPadding: number; - }; - chartMargins: { - left: number; - right: number; - top: number; - bottom: number; - }; - chartPaddings: { - left: number; - right: number; - top: number; - bottom: number; - }; - }; - }; - customHeight: number; -} diff --git a/x-pack/plugins/siem/public/components/matrix_histogram/utils.ts b/x-pack/plugins/siem/public/components/matrix_histogram/utils.ts deleted file mode 100644 index d31eb1da15ea1f..00000000000000 --- a/x-pack/plugins/siem/public/components/matrix_histogram/utils.ts +++ /dev/null @@ -1,108 +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 { ScaleType, Position } from '@elastic/charts'; -import { get, groupBy, map, toPairs } from 'lodash/fp'; - -import { UpdateDateRange, ChartSeriesData } from '../charts/common'; -import { MatrixHistogramMappingTypes, BarchartConfigs } from './types'; -import { MatrixOverTimeHistogramData } from '../../graphql/types'; -import { histogramDateTimeFormatter } from '../utils'; - -interface GetBarchartConfigsProps { - chartHeight?: number; - from: number; - legendPosition?: Position; - to: number; - onBrushEnd: UpdateDateRange; - yTickFormatter?: (value: number) => string; - showLegend?: boolean; -} - -export const DEFAULT_CHART_HEIGHT = 174; -export const DEFAULT_Y_TICK_FORMATTER = (value: string | number): string => value.toLocaleString(); - -export const getBarchartConfigs = ({ - chartHeight, - from, - legendPosition, - to, - onBrushEnd, - yTickFormatter, - showLegend, -}: GetBarchartConfigsProps): BarchartConfigs => ({ - series: { - xScaleType: ScaleType.Time, - yScaleType: ScaleType.Linear, - stackAccessors: ['g'], - }, - axis: { - xTickFormatter: histogramDateTimeFormatter([from, to]), - yTickFormatter: yTickFormatter != null ? yTickFormatter : DEFAULT_Y_TICK_FORMATTER, - tickSize: 8, - }, - settings: { - legendPosition: legendPosition ?? Position.Right, - onBrushEnd, - showLegend: showLegend ?? true, - showLegendExtra: true, - theme: { - scales: { - barsPadding: 0.08, - }, - chartMargins: { - left: 0, - right: 0, - top: 0, - bottom: 0, - }, - chartPaddings: { - left: 0, - right: 0, - top: 0, - bottom: 0, - }, - }, - }, - customHeight: chartHeight ?? DEFAULT_CHART_HEIGHT, -}); - -export const defaultLegendColors = [ - '#1EA593', - '#2B70F7', - '#CE0060', - '#38007E', - '#FCA5D3', - '#F37020', - '#E49E29', - '#B0916F', - '#7B000B', - '#34130C', -]; - -export const formatToChartDataItem = ([key, value]: [ - string, - MatrixOverTimeHistogramData[] -]): ChartSeriesData => ({ - key, - value, -}); - -export const getCustomChartData = ( - data: MatrixOverTimeHistogramData[] | null, - mapping?: MatrixHistogramMappingTypes -): ChartSeriesData[] => { - if (!data) return []; - const dataGroupedByEvent = groupBy('g', data); - const dataGroupedEntries = toPairs(dataGroupedByEvent); - const formattedChartData = map(formatToChartDataItem, dataGroupedEntries); - - if (mapping) - return map((item: ChartSeriesData) => { - const mapItem = get(item.key, mapping); - return { ...item, color: mapItem?.color }; - }, formattedChartData); - else return formattedChartData; -}; diff --git a/x-pack/plugins/siem/public/components/ml/api/get_ml_capabilities.ts b/x-pack/plugins/siem/public/components/ml/api/get_ml_capabilities.ts deleted file mode 100644 index e69abc1a86e0eb..00000000000000 --- a/x-pack/plugins/siem/public/components/ml/api/get_ml_capabilities.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 { InfluencerInput, MlCapabilities } from '../types'; -import { KibanaServices } from '../../../lib/kibana'; - -export interface Body { - jobIds: string[]; - criteriaFields: string[]; - influencers: InfluencerInput[]; - aggregationInterval: string; - threshold: number; - earliestMs: number; - latestMs: number; - dateFormatTz: string; - maxRecords: number; - maxExamples: number; -} - -export const getMlCapabilities = async (signal: AbortSignal): Promise => { - return KibanaServices.get().http.fetch('/api/ml/ml_capabilities', { - method: 'GET', - asSystemRequest: true, - signal, - }); -}; diff --git a/x-pack/plugins/siem/public/components/ml/types.ts b/x-pack/plugins/siem/public/components/ml/types.ts deleted file mode 100644 index 953fb9f761ea86..00000000000000 --- a/x-pack/plugins/siem/public/components/ml/types.ts +++ /dev/null @@ -1,150 +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 { HostsType } from '../../store/hosts/model'; -import { NetworkType } from '../../store/network/model'; -import { FlowTarget } from '../../graphql/types'; - -export interface Influencer { - influencer_field_name: string; - influencer_field_values: string[]; -} - -export interface Source { - job_id: string; - result_type: string; - probability: number; - multi_bucket_impact: number; - record_score: number; - initial_record_score: number; - bucket_span: number; - detector_index: number; - is_interim: boolean; - timestamp: number; - by_field_name: string; - by_field_value: string; - partition_field_name: string; - partition_field_value: string; - function: string; - function_description: string; - typical: number[]; - actual: number[]; - influencers: Influencer[]; -} - -export interface Influencer { - influencer_field_name: string; - influencer_field_values: string[]; -} - -export interface CriteriaFields { - fieldName: string; - fieldValue: string; -} - -export interface InfluencerInput { - fieldName: string; - fieldValue: string; -} - -export interface Anomaly { - detectorIndex: number; - entityName: string; - entityValue: string; - influencers?: Array>; - jobId: string; - rowId: string; - severity: number; - time: number; - source: Source; -} - -export interface Anomalies { - anomalies: Anomaly[]; - interval: string; -} - -export type NarrowDateRange = (score: Anomaly, interval: string) => void; - -export interface AnomaliesByHost { - hostName: string; - anomaly: Anomaly; -} - -export type DestinationOrSource = 'source.ip' | 'destination.ip'; - -export interface AnomaliesByNetwork { - type: DestinationOrSource; - ip: string; - anomaly: Anomaly; -} - -export interface HostOrNetworkProps { - startDate: number; - endDate: number; - narrowDateRange: NarrowDateRange; - skip: boolean; -} - -export type AnomaliesHostTableProps = HostOrNetworkProps & { - hostName?: string; - type: HostsType; -}; - -export type AnomaliesNetworkTableProps = HostOrNetworkProps & { - ip?: string; - type: NetworkType; - flowTarget?: FlowTarget; -}; - -export interface MlCapabilities { - capabilities: { - canGetJobs: boolean; - canCreateJob: boolean; - canDeleteJob: boolean; - canOpenJob: boolean; - canCloseJob: boolean; - canForecastJob: boolean; - canGetDatafeeds: boolean; - canStartStopDatafeed: boolean; - canUpdateJob: boolean; - canUpdateDatafeed: boolean; - canPreviewDatafeed: boolean; - canGetCalendars: boolean; - canCreateCalendar: boolean; - canDeleteCalendar: boolean; - canGetFilters: boolean; - canCreateFilter: boolean; - canDeleteFilter: boolean; - canFindFileStructure: boolean; - canGetDataFrame: boolean; - canDeleteDataFrame: boolean; - canPreviewDataFrame: boolean; - canCreateDataFrame: boolean; - canStartStopDataFrame: boolean; - canGetDataFrameAnalytics: boolean; - canDeleteDataFrameAnalytics: boolean; - canCreateDataFrameAnalytics: boolean; - canStartStopDataFrameAnalytics: boolean; - }; - isPlatinumOrTrialLicense: boolean; - mlFeatureEnabledInSpace: boolean; - upgradeInProgress: boolean; -} - -const sourceOrDestination = ['source.ip', 'destination.ip']; - -export const isDestinationOrSource = (value: string | null): value is DestinationOrSource => - value != null && sourceOrDestination.includes(value); - -export interface MlError { - msg: string; - response: string; - statusCode: number; - path?: string; - query?: {}; - body?: string; -} diff --git a/x-pack/plugins/siem/public/components/ml_popover/helpers.test.tsx b/x-pack/plugins/siem/public/components/ml_popover/helpers.test.tsx deleted file mode 100644 index 26ebfeb91629bc..00000000000000 --- a/x-pack/plugins/siem/public/components/ml_popover/helpers.test.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { mockSiemJobs } from './__mocks__/api'; -import { filterJobs, getStablePatternTitles, searchFilter } from './helpers'; - -jest.mock('../ml/permissions/has_ml_admin_permissions', () => ({ - hasMlAdminPermissions: () => true, -})); - -describe('helpers', () => { - describe('filterJobs', () => { - test('returns all jobs when no filter is suplied', () => { - const filteredJobs = filterJobs({ - jobs: mockSiemJobs, - selectedGroups: [], - showCustomJobs: false, - showElasticJobs: false, - filterQuery: '', - }); - expect(filteredJobs.length).toEqual(3); - }); - }); - - describe('searchFilter', () => { - test('returns all jobs when nullfilterQuery is provided', () => { - const jobsToDisplay = searchFilter(mockSiemJobs); - expect(jobsToDisplay.length).toEqual(mockSiemJobs.length); - }); - - test('returns correct DisplayJobs when filterQuery matches job.id', () => { - const jobsToDisplay = searchFilter(mockSiemJobs, 'rare_process'); - expect(jobsToDisplay.length).toEqual(2); - }); - - test('returns correct DisplayJobs when filterQuery matches job.description', () => { - const jobsToDisplay = searchFilter(mockSiemJobs, 'Detect unusually'); - expect(jobsToDisplay.length).toEqual(2); - }); - }); - - describe('getStablePatternTitles', () => { - test('it returns a stable reference two times in a row with standard strings', () => { - const one = getStablePatternTitles(['a', 'b', 'c']); - const two = getStablePatternTitles(['a', 'b', 'c']); - expect(one).toBe(two); - }); - - test('it returns a stable reference two times in a row with strings interchanged', () => { - const one = getStablePatternTitles(['c', 'b', 'a']); - const two = getStablePatternTitles(['a', 'b', 'c']); - expect(one).toBe(two); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/ml_popover/types.ts b/x-pack/plugins/siem/public/components/ml_popover/types.ts deleted file mode 100644 index 005f93650a8eb0..00000000000000 --- a/x-pack/plugins/siem/public/components/ml_popover/types.ts +++ /dev/null @@ -1,203 +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 { AuditMessageBase } from '../../../../ml/public'; -import { MlError } from '../ml/types'; - -export interface Group { - id: string; - jobIds: string[]; - calendarIds: string[]; -} - -export interface CheckRecognizerProps { - indexPatternName: string[]; - signal: AbortSignal; -} - -export interface RecognizerModule { - id: string; - title: string; - query: Record; - description: string; - logo: { - icon: string; - }; -} - -export interface GetModulesProps { - moduleId?: string; - signal: AbortSignal; -} - -export interface Module { - id: string; - title: string; - description: string; - type: string; - logoFile: string; - defaultIndexPattern: string; - query: Record; - jobs: ModuleJob[]; - datafeeds: ModuleDatafeed[]; - kibana: object; -} - -/** - * Representation of an ML Job as returned from `the ml/modules/get_module` API - */ -export interface ModuleJob { - id: string; - config: { - groups: string[]; - description: string; - analysis_config: { - bucket_span: string; - summary_count_field_name?: string; - detectors: Detector[]; - influencers: string[]; - }; - analysis_limits: { - model_memory_limit: string; - }; - data_description: { - time_field: string; - time_format?: string; - }; - model_plot_config?: { - enabled: boolean; - }; - custom_settings: { - created_by: string; - custom_urls: CustomURL[]; - }; - job_type: string; - }; -} - -// TODO: Speak to ML team about why the get_module API will sometimes return indexes and other times indices -// See mockGetModuleResponse for examples -export interface ModuleDatafeed { - id: string; - config: { - job_id: string; - indexes?: string[]; - indices?: string[]; - query: Record; - }; -} - -export interface MlSetupArgs { - configTemplate: string; - indexPatternName: string; - jobIdErrorFilter: string[]; - groups: string[]; - prefix?: string; -} - -/** - * Representation of an ML Job as returned from the `ml/jobs/jobs_summary` API - */ -export interface JobSummary { - auditMessage?: AuditMessageBase; - datafeedId: string; - datafeedIndices: string[]; - datafeedState: string; - description: string; - earliestTimestampMs?: number; - latestResultsTimestampMs?: number; - groups: string[]; - hasDatafeed: boolean; - id: string; - isSingleMetricViewerJob: boolean; - jobState: string; - latestTimestampMs?: number; - memory_status: string; - nodeName?: string; - processed_record_count: number; -} - -export interface Detector { - detector_description: string; - function: string; - by_field_name: string; - partition_field_name?: string; -} - -export interface CustomURL { - url_name: string; - url_value: string; -} - -/** - * Representation of an ML Job as used by the SIEM App -- a composition of ModuleJob and JobSummary - * that includes necessary metadata like moduleName, defaultIndexPattern, etc. - */ -export interface SiemJob extends JobSummary { - moduleId: string; - defaultIndexPattern: string; - isCompatible: boolean; - isInstalled: boolean; - isElasticJob: boolean; -} - -export interface AugmentedSiemJobFields { - moduleId: string; - defaultIndexPattern: string; - isCompatible: boolean; - isElasticJob: boolean; -} - -export interface SetupMlResponseJob { - id: string; - success: boolean; - error?: MlError; -} - -export interface SetupMlResponseDatafeed { - id: string; - success: boolean; - started: boolean; - error?: MlError; -} - -export interface SetupMlResponse { - jobs: SetupMlResponseJob[]; - datafeeds: SetupMlResponseDatafeed[]; - kibana: {}; -} - -export interface StartDatafeedResponse { - [key: string]: { - started: boolean; - error?: string; - }; -} - -export interface ErrorResponse { - statusCode?: number; - error?: string; - message?: string; -} - -export interface StopDatafeedResponse { - [key: string]: { - stopped: boolean; - }; -} - -export interface CloseJobsResponse { - [key: string]: { - closed: boolean; - }; -} - -export interface JobsFilters { - filterQuery: string; - showCustomJobs: boolean; - showElasticJobs: boolean; - selectedGroups: string[]; -} diff --git a/x-pack/plugins/siem/public/components/navigation/breadcrumbs/index.ts b/x-pack/plugins/siem/public/components/navigation/breadcrumbs/index.ts deleted file mode 100644 index 8abc099ee7f693..00000000000000 --- a/x-pack/plugins/siem/public/components/navigation/breadcrumbs/index.ts +++ /dev/null @@ -1,170 +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 { getOr, omit } from 'lodash/fp'; - -import { ChromeBreadcrumb } from '../../../../../../../src/core/public'; -import { APP_NAME } from '../../../../common/constants'; -import { StartServices } from '../../../plugin'; -import { getBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../pages/hosts/details/utils'; -import { getBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../pages/network/ip_details'; -import { getBreadcrumbs as getCaseDetailsBreadcrumbs } from '../../../pages/case/utils'; -import { getBreadcrumbs as getDetectionRulesBreadcrumbs } from '../../../pages/detection_engine/rules/utils'; -import { getBreadcrumbs as getTimelinesBreadcrumbs } from '../../../pages/timelines'; -import { SiemPageName } from '../../../pages/home/types'; -import { - RouteSpyState, - HostRouteSpyState, - NetworkRouteSpyState, - TimelineRouteSpyState, -} from '../../../utils/route/types'; -import { getOverviewUrl } from '../../link_to'; - -import { TabNavigationProps } from '../tab_navigation/types'; -import { getSearch } from '../helpers'; -import { SearchNavTab } from '../types'; - -export const setBreadcrumbs = ( - spyState: RouteSpyState & TabNavigationProps, - chrome: StartServices['chrome'] -) => { - const breadcrumbs = getBreadcrumbsForRoute(spyState); - if (breadcrumbs) { - chrome.setBreadcrumbs(breadcrumbs); - } -}; - -export const siemRootBreadcrumb: ChromeBreadcrumb[] = [ - { - text: APP_NAME, - href: getOverviewUrl(), - }, -]; - -const isNetworkRoutes = (spyState: RouteSpyState): spyState is NetworkRouteSpyState => - spyState != null && spyState.pageName === SiemPageName.network; - -const isHostsRoutes = (spyState: RouteSpyState): spyState is HostRouteSpyState => - spyState != null && spyState.pageName === SiemPageName.hosts; - -const isTimelinesRoutes = (spyState: RouteSpyState): spyState is TimelineRouteSpyState => - spyState != null && spyState.pageName === SiemPageName.timelines; - -const isCaseRoutes = (spyState: RouteSpyState): spyState is RouteSpyState => - spyState != null && spyState.pageName === SiemPageName.case; - -const isDetectionsRoutes = (spyState: RouteSpyState) => - spyState != null && spyState.pageName === SiemPageName.detections; - -export const getBreadcrumbsForRoute = ( - object: RouteSpyState & TabNavigationProps -): ChromeBreadcrumb[] | null => { - const spyState: RouteSpyState = omit('navTabs', object); - if (isHostsRoutes(spyState) && object.navTabs) { - const tempNav: SearchNavTab = { urlKey: 'host', isDetailPage: false }; - let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; - if (spyState.tabName != null) { - urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; - } - return [ - ...siemRootBreadcrumb, - ...getHostDetailsBreadcrumbs( - spyState, - urlStateKeys.reduce( - (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], - [] - ) - ), - ]; - } - if (isNetworkRoutes(spyState) && object.navTabs) { - const tempNav: SearchNavTab = { urlKey: 'network', isDetailPage: false }; - let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; - if (spyState.tabName != null) { - urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; - } - return [ - ...siemRootBreadcrumb, - ...getIPDetailsBreadcrumbs( - spyState, - urlStateKeys.reduce( - (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], - [] - ) - ), - ]; - } - if (isDetectionsRoutes(spyState) && object.navTabs) { - const tempNav: SearchNavTab = { urlKey: 'detections', isDetailPage: false }; - let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; - if (spyState.tabName != null) { - urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; - } - - return [ - ...siemRootBreadcrumb, - ...getDetectionRulesBreadcrumbs( - spyState, - urlStateKeys.reduce( - (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], - [] - ) - ), - ]; - } - if (isCaseRoutes(spyState) && object.navTabs) { - const tempNav: SearchNavTab = { urlKey: 'case', isDetailPage: false }; - let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; - if (spyState.tabName != null) { - urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; - } - - return [ - ...siemRootBreadcrumb, - ...getCaseDetailsBreadcrumbs( - spyState, - urlStateKeys.reduce( - (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], - [] - ) - ), - ]; - } - if (isTimelinesRoutes(spyState) && object.navTabs) { - const tempNav: SearchNavTab = { urlKey: 'timeline', isDetailPage: false }; - let urlStateKeys = [getOr(tempNav, spyState.pageName, object.navTabs)]; - if (spyState.tabName != null) { - urlStateKeys = [...urlStateKeys, getOr(tempNav, spyState.tabName, object.navTabs)]; - } - - return [ - ...siemRootBreadcrumb, - ...getTimelinesBreadcrumbs( - spyState, - urlStateKeys.reduce( - (acc: string[], item: SearchNavTab) => [...acc, getSearch(item, object)], - [] - ) - ), - ]; - } - if ( - spyState != null && - object.navTabs && - spyState.pageName && - object.navTabs[spyState.pageName] - ) { - return [ - ...siemRootBreadcrumb, - { - text: object.navTabs[spyState.pageName].name, - href: '', - }, - ]; - } - - return null; -}; diff --git a/x-pack/plugins/siem/public/components/navigation/helpers.ts b/x-pack/plugins/siem/public/components/navigation/helpers.ts deleted file mode 100644 index 291cb90098f78e..00000000000000 --- a/x-pack/plugins/siem/public/components/navigation/helpers.ts +++ /dev/null @@ -1,68 +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 { isEmpty } from 'lodash/fp'; -import { Location } from 'history'; - -import { UrlInputsModel } from '../../store/inputs/model'; -import { TimelineUrl } from '../../store/timeline/model'; -import { CONSTANTS } from '../url_state/constants'; -import { URL_STATE_KEYS, KeyUrlState, UrlState } from '../url_state/types'; -import { - replaceQueryStringInLocation, - replaceStateKeyInQueryString, - getQueryStringFromLocation, -} from '../url_state/helpers'; -import { Query, Filter } from '../../../../../../src/plugins/data/public'; - -import { SearchNavTab } from './types'; - -export const getSearch = (tab: SearchNavTab, urlState: UrlState): string => { - if (tab && tab.urlKey != null && URL_STATE_KEYS[tab.urlKey] != null) { - return URL_STATE_KEYS[tab.urlKey].reduce( - (myLocation: Location, urlKey: KeyUrlState) => { - let urlStateToReplace: UrlInputsModel | Query | Filter[] | TimelineUrl | string = ''; - - if (urlKey === CONSTANTS.appQuery && urlState.query != null) { - if (urlState.query.query === '') { - urlStateToReplace = ''; - } else { - urlStateToReplace = urlState.query; - } - } else if (urlKey === CONSTANTS.filters && urlState.filters != null) { - if (isEmpty(urlState.filters)) { - urlStateToReplace = ''; - } else { - urlStateToReplace = urlState.filters; - } - } else if (urlKey === CONSTANTS.timerange) { - urlStateToReplace = urlState[CONSTANTS.timerange]; - } else if (urlKey === CONSTANTS.timeline && urlState[CONSTANTS.timeline] != null) { - const timeline = urlState[CONSTANTS.timeline]; - if (timeline.id === '') { - urlStateToReplace = ''; - } else { - urlStateToReplace = timeline; - } - } - return replaceQueryStringInLocation( - myLocation, - replaceStateKeyInQueryString( - urlKey, - urlStateToReplace - )(getQueryStringFromLocation(myLocation.search)) - ); - }, - { - pathname: '', - hash: '', - search: '', - state: '', - } - ).search; - } - return ''; -}; diff --git a/x-pack/plugins/siem/public/components/navigation/index.test.tsx b/x-pack/plugins/siem/public/components/navigation/index.test.tsx deleted file mode 100644 index d8b62029138c82..00000000000000 --- a/x-pack/plugins/siem/public/components/navigation/index.test.tsx +++ /dev/null @@ -1,238 +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 { mount } from 'enzyme'; -import React from 'react'; - -import { CONSTANTS } from '../url_state/constants'; -import { SiemNavigationComponent } from './'; -import { setBreadcrumbs } from './breadcrumbs'; -import { navTabs } from '../../pages/home/home_navigations'; -import { HostsTableType } from '../../store/hosts/model'; -import { RouteSpyState } from '../../utils/route/types'; -import { SiemNavigationProps, SiemNavigationComponentProps } from './types'; - -jest.mock('./breadcrumbs', () => ({ - setBreadcrumbs: jest.fn(), -})); - -describe('SIEM Navigation', () => { - const mockProps: SiemNavigationComponentProps & SiemNavigationProps & RouteSpyState = { - pageName: 'hosts', - pathName: '/hosts', - detailName: undefined, - search: '', - tabName: HostsTableType.authentications, - navTabs, - urlState: { - [CONSTANTS.timerange]: { - global: { - [CONSTANTS.timerange]: { - from: 1558048243696, - fromStr: 'now-24h', - kind: 'relative', - to: 1558134643697, - toStr: 'now', - }, - linkTo: ['timeline'], - }, - timeline: { - [CONSTANTS.timerange]: { - from: 1558048243696, - fromStr: 'now-24h', - kind: 'relative', - to: 1558134643697, - toStr: 'now', - }, - linkTo: ['global'], - }, - }, - [CONSTANTS.appQuery]: { query: '', language: 'kuery' }, - [CONSTANTS.filters]: [], - [CONSTANTS.timeline]: { - id: '', - isOpen: false, - }, - }, - }; - const wrapper = mount(); - test('it calls setBreadcrumbs with correct path on mount', () => { - expect(setBreadcrumbs).toHaveBeenNthCalledWith( - 1, - { - detailName: undefined, - navTabs: { - case: { - disabled: false, - href: '#/link-to/case', - id: 'case', - name: 'Cases', - urlKey: 'case', - }, - detections: { - disabled: false, - href: '#/link-to/detections', - id: 'detections', - name: 'Detections', - urlKey: 'detections', - }, - hosts: { - disabled: false, - href: '#/link-to/hosts', - id: 'hosts', - name: 'Hosts', - urlKey: 'host', - }, - network: { - disabled: false, - href: '#/link-to/network', - id: 'network', - name: 'Network', - urlKey: 'network', - }, - overview: { - disabled: false, - href: '#/link-to/overview', - id: 'overview', - name: 'Overview', - urlKey: 'overview', - }, - timelines: { - disabled: false, - href: '#/link-to/timelines', - id: 'timelines', - name: 'Timelines', - urlKey: 'timeline', - }, - }, - pageName: 'hosts', - pathName: '/hosts', - search: '', - tabName: 'authentications', - query: { query: '', language: 'kuery' }, - filters: [], - savedQuery: undefined, - timeline: { - id: '', - isOpen: false, - }, - timerange: { - global: { - linkTo: ['timeline'], - timerange: { - from: 1558048243696, - fromStr: 'now-24h', - kind: 'relative', - to: 1558134643697, - toStr: 'now', - }, - }, - timeline: { - linkTo: ['global'], - timerange: { - from: 1558048243696, - fromStr: 'now-24h', - kind: 'relative', - to: 1558134643697, - toStr: 'now', - }, - }, - }, - }, - undefined - ); - }); - test('it calls setBreadcrumbs with correct path on update', () => { - wrapper.setProps({ - pageName: 'network', - pathName: '/network', - tabName: undefined, - }); - wrapper.update(); - expect(setBreadcrumbs).toHaveBeenNthCalledWith( - 1, - { - detailName: undefined, - filters: [], - navTabs: { - case: { - disabled: false, - href: '#/link-to/case', - id: 'case', - name: 'Cases', - urlKey: 'case', - }, - detections: { - disabled: false, - href: '#/link-to/detections', - id: 'detections', - name: 'Detections', - urlKey: 'detections', - }, - hosts: { - disabled: false, - href: '#/link-to/hosts', - id: 'hosts', - name: 'Hosts', - urlKey: 'host', - }, - network: { - disabled: false, - href: '#/link-to/network', - id: 'network', - name: 'Network', - urlKey: 'network', - }, - overview: { - disabled: false, - href: '#/link-to/overview', - id: 'overview', - name: 'Overview', - urlKey: 'overview', - }, - timelines: { - disabled: false, - href: '#/link-to/timelines', - id: 'timelines', - name: 'Timelines', - urlKey: 'timeline', - }, - }, - pageName: 'hosts', - pathName: '/hosts', - query: { language: 'kuery', query: '' }, - savedQuery: undefined, - search: '', - state: undefined, - tabName: 'authentications', - timeline: { id: '', isOpen: false }, - timerange: { - global: { - linkTo: ['timeline'], - timerange: { - from: 1558048243696, - fromStr: 'now-24h', - kind: 'relative', - to: 1558134643697, - toStr: 'now', - }, - }, - timeline: { - linkTo: ['global'], - timerange: { - from: 1558048243696, - fromStr: 'now-24h', - kind: 'relative', - to: 1558134643697, - toStr: 'now', - }, - }, - }, - }, - undefined - ); - }); -}); diff --git a/x-pack/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx b/x-pack/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx deleted file mode 100644 index 99ded06cfdcc86..00000000000000 --- a/x-pack/plugins/siem/public/components/navigation/tab_navigation/index.test.tsx +++ /dev/null @@ -1,157 +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 { mount } from 'enzyme'; -import React from 'react'; - -import { navTabs } from '../../../pages/home/home_navigations'; -import { SiemPageName } from '../../../pages/home/types'; -import { navTabsHostDetails } from '../../../pages/hosts/details/nav_tabs'; -import { HostsTableType } from '../../../store/hosts/model'; -import { RouteSpyState } from '../../../utils/route/types'; -import { CONSTANTS } from '../../url_state/constants'; -import { TabNavigationComponent } from './'; -import { TabNavigationProps } from './types'; - -describe('Tab Navigation', () => { - const pageName = SiemPageName.hosts; - const hostName = 'siem-window'; - const tabName = HostsTableType.authentications; - const pathName = `/${pageName}/${hostName}/${tabName}`; - - describe('Page Navigation', () => { - const mockProps: TabNavigationProps & RouteSpyState = { - pageName, - pathName, - detailName: undefined, - search: '', - tabName, - navTabs, - [CONSTANTS.timerange]: { - global: { - [CONSTANTS.timerange]: { - from: 1558048243696, - fromStr: 'now-24h', - kind: 'relative', - to: 1558134643697, - toStr: 'now', - }, - linkTo: ['timeline'], - }, - timeline: { - [CONSTANTS.timerange]: { - from: 1558048243696, - fromStr: 'now-24h', - kind: 'relative', - to: 1558134643697, - toStr: 'now', - }, - linkTo: ['global'], - }, - }, - [CONSTANTS.appQuery]: { query: 'host.name:"siem-es"', language: 'kuery' }, - [CONSTANTS.filters]: [], - [CONSTANTS.timeline]: { - id: '', - isOpen: false, - }, - }; - test('it mounts with correct tab highlighted', () => { - const wrapper = mount(); - const hostsTab = wrapper.find('EuiTab[data-test-subj="navigation-hosts"]'); - expect(hostsTab.prop('isSelected')).toBeTruthy(); - }); - test('it changes active tab when nav changes by props', () => { - const wrapper = mount(); - const networkTab = () => wrapper.find('EuiTab[data-test-subj="navigation-network"]').first(); - expect(networkTab().prop('isSelected')).toBeFalsy(); - wrapper.setProps({ - pageName: 'network', - pathName: '/network', - tabName: undefined, - }); - wrapper.update(); - expect(networkTab().prop('isSelected')).toBeTruthy(); - }); - test('it carries the url state in the link', () => { - const wrapper = mount(); - const firstTab = wrapper.find('EuiTab[data-test-subj="navigation-network"]'); - expect(firstTab.props().href).toBe( - "#/link-to/network?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))" - ); - }); - }); - - describe('Table Navigation', () => { - const mockHasMlUserPermissions = true; - const mockProps: TabNavigationProps & RouteSpyState = { - pageName: 'hosts', - pathName: '/hosts', - detailName: undefined, - search: '', - tabName: HostsTableType.authentications, - navTabs: navTabsHostDetails(hostName, mockHasMlUserPermissions), - [CONSTANTS.timerange]: { - global: { - [CONSTANTS.timerange]: { - from: 1558048243696, - fromStr: 'now-24h', - kind: 'relative', - to: 1558134643697, - toStr: 'now', - }, - linkTo: ['timeline'], - }, - timeline: { - [CONSTANTS.timerange]: { - from: 1558048243696, - fromStr: 'now-24h', - kind: 'relative', - to: 1558134643697, - toStr: 'now', - }, - linkTo: ['global'], - }, - }, - [CONSTANTS.appQuery]: { query: 'host.name:"siem-es"', language: 'kuery' }, - [CONSTANTS.filters]: [], - [CONSTANTS.timeline]: { - id: '', - isOpen: false, - }, - }; - test('it mounts with correct tab highlighted', () => { - const wrapper = mount(); - const tableNavigationTab = wrapper.find( - `EuiTab[data-test-subj="navigation-${HostsTableType.authentications}"]` - ); - - expect(tableNavigationTab.prop('isSelected')).toBeTruthy(); - }); - test('it changes active tab when nav changes by props', () => { - const wrapper = mount(); - const tableNavigationTab = () => - wrapper.find(`[data-test-subj="navigation-${HostsTableType.events}"]`).first(); - expect(tableNavigationTab().prop('isSelected')).toBeFalsy(); - wrapper.setProps({ - pageName: SiemPageName.hosts, - pathName: `/${SiemPageName.hosts}`, - tabName: HostsTableType.events, - }); - wrapper.update(); - expect(tableNavigationTab().prop('isSelected')).toBeTruthy(); - }); - test('it carries the url state in the link', () => { - const wrapper = mount(); - const firstTab = wrapper.find( - `EuiTab[data-test-subj="navigation-${HostsTableType.authentications}"]` - ); - expect(firstTab.props().href).toBe( - `#/${pageName}/${hostName}/${HostsTableType.authentications}?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))` - ); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/navigation/tab_navigation/types.ts b/x-pack/plugins/siem/public/components/navigation/tab_navigation/types.ts deleted file mode 100644 index 2e2dea09f8c38b..00000000000000 --- a/x-pack/plugins/siem/public/components/navigation/tab_navigation/types.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. - */ - -import { UrlInputsModel } from '../../../store/inputs/model'; -import { CONSTANTS } from '../../url_state/constants'; -import { HostsTableType } from '../../../store/hosts/model'; -import { TimelineUrl } from '../../../store/timeline/model'; -import { Filter, Query } from '../../../../../../../src/plugins/data/public'; - -import { SiemNavigationProps } from '../types'; - -export interface TabNavigationProps extends SiemNavigationProps { - pathName: string; - pageName: string; - tabName: HostsTableType | undefined; - [CONSTANTS.appQuery]?: Query; - [CONSTANTS.filters]?: Filter[]; - [CONSTANTS.savedQuery]?: string; - [CONSTANTS.timerange]: UrlInputsModel; - [CONSTANTS.timeline]: TimelineUrl; -} - -export interface TabNavigationItemProps { - href: string; - hrefWithSearch: string; - id: string; - disabled: boolean; - name: string; - isSelected: boolean; -} diff --git a/x-pack/plugins/siem/public/components/navigation/types.ts b/x-pack/plugins/siem/public/components/navigation/types.ts deleted file mode 100644 index e8a2865938062b..00000000000000 --- a/x-pack/plugins/siem/public/components/navigation/types.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. - */ - -import { Filter, Query } from '../../../../../../src/plugins/data/public'; -import { HostsTableType } from '../../store/hosts/model'; -import { UrlInputsModel } from '../../store/inputs/model'; -import { TimelineUrl } from '../../store/timeline/model'; -import { CONSTANTS, UrlStateType } from '../url_state/constants'; - -export interface SiemNavigationProps { - display?: 'default' | 'condensed'; - navTabs: Record; -} - -export interface SiemNavigationComponentProps { - pathName: string; - pageName: string; - tabName: HostsTableType | undefined; - urlState: { - [CONSTANTS.appQuery]?: Query; - [CONSTANTS.filters]?: Filter[]; - [CONSTANTS.savedQuery]?: string; - [CONSTANTS.timerange]: UrlInputsModel; - [CONSTANTS.timeline]: TimelineUrl; - }; -} - -export type SearchNavTab = NavTab | { urlKey: UrlStateType; isDetailPage: boolean }; - -export interface NavTab { - id: string; - name: string; - href: string; - disabled: boolean; - urlKey: UrlStateType; - isDetailPage?: boolean; -} diff --git a/x-pack/plugins/siem/public/components/netflow/index.test.tsx b/x-pack/plugins/siem/public/components/netflow/index.test.tsx deleted file mode 100644 index ecf162ebf2739a..00000000000000 --- a/x-pack/plugins/siem/public/components/netflow/index.test.tsx +++ /dev/null @@ -1,534 +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 { get } from 'lodash/fp'; -import React from 'react'; -import { shallow } from 'enzyme'; - -import { asArrayIfExists } from '../../lib/helpers'; -import { getMockNetflowData } from '../../mock'; -import { TestProviders } from '../../mock/test_providers'; -import { - TLS_CLIENT_CERTIFICATE_FINGERPRINT_SHA1_FIELD_NAME, - TLS_SERVER_CERTIFICATE_FINGERPRINT_SHA1_FIELD_NAME, -} from '../certificate_fingerprint'; -import { EVENT_DURATION_FIELD_NAME } from '../duration'; -import { ID_FIELD_NAME } from '../event_details/event_id'; -import { DESTINATION_IP_FIELD_NAME, SOURCE_IP_FIELD_NAME } from '../ip'; -import { JA3_HASH_FIELD_NAME } from '../ja3_fingerprint'; -import { DESTINATION_PORT_FIELD_NAME, SOURCE_PORT_FIELD_NAME } from '../port'; -import { - DESTINATION_GEO_CITY_NAME_FIELD_NAME, - DESTINATION_GEO_CONTINENT_NAME_FIELD_NAME, - DESTINATION_GEO_COUNTRY_ISO_CODE_FIELD_NAME, - DESTINATION_GEO_COUNTRY_NAME_FIELD_NAME, - DESTINATION_GEO_REGION_NAME_FIELD_NAME, - SOURCE_GEO_CITY_NAME_FIELD_NAME, - SOURCE_GEO_CONTINENT_NAME_FIELD_NAME, - SOURCE_GEO_COUNTRY_ISO_CODE_FIELD_NAME, - SOURCE_GEO_COUNTRY_NAME_FIELD_NAME, - SOURCE_GEO_REGION_NAME_FIELD_NAME, -} from '../source_destination/geo_fields'; -import { - DESTINATION_BYTES_FIELD_NAME, - DESTINATION_PACKETS_FIELD_NAME, - SOURCE_BYTES_FIELD_NAME, - SOURCE_PACKETS_FIELD_NAME, -} from '../source_destination/source_destination_arrows'; -import * as i18n from '../timeline/body/renderers/translations'; - -import { Netflow } from '.'; -import { - EVENT_END_FIELD_NAME, - EVENT_START_FIELD_NAME, -} from './netflow_columns/duration_event_start_end'; -import { PROCESS_NAME_FIELD_NAME, USER_NAME_FIELD_NAME } from './netflow_columns/user_process'; -import { - NETWORK_BYTES_FIELD_NAME, - NETWORK_DIRECTION_FIELD_NAME, - NETWORK_COMMUNITY_ID_FIELD_NAME, - NETWORK_PACKETS_FIELD_NAME, - NETWORK_PROTOCOL_FIELD_NAME, - NETWORK_TRANSPORT_FIELD_NAME, -} from '../source_destination/field_names'; -import { useMountAppended } from '../../utils/use_mount_appended'; - -const getNetflowInstance = () => ( - -); - -describe('Netflow', () => { - const mount = useMountAppended(); - - test('renders correctly against snapshot', () => { - const wrapper = shallow(getNetflowInstance()); - expect(wrapper).toMatchSnapshot(); - }); - - test('it renders a destination label', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="destination-label"]') - .first() - .text() - ).toEqual(i18n.DESTINATION); - }); - - test('it renders destination.bytes', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="destination-bytes"]') - .first() - .text() - ).toEqual('40B'); - }); - - test('it renders destination.geo.continent_name', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="destination.geo.continent_name"]') - .first() - .text() - ).toEqual('North America'); - }); - - test('it renders destination.geo.country_name', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="destination.geo.country_name"]') - .first() - .text() - ).toEqual('United States'); - }); - - test('it renders destination.geo.country_iso_code', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="destination.geo.country_iso_code"]') - .first() - .text() - ).toEqual('US'); - }); - - test('it renders destination.geo.region_name', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="destination.geo.region_name"]') - .first() - .text() - ).toEqual('New York'); - }); - - test('it renders destination.geo.city_name', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="destination.geo.city_name"]') - .first() - .text() - ).toEqual('New York'); - }); - - test('it renders the destination ip and port, separated with a colon', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="destination-ip-and-port"]') - .first() - .text() - ).toEqual('10.1.2.3:80'); - }); - - test('it renders destination.packets', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="destination-packets"]') - .first() - .text() - ).toEqual('1 pkts'); - }); - - test('it hyperlinks links destination.port to an external service that describes the purpose of the port', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="destination-ip-and-port"]') - .find('[data-test-subj="port-or-service-name-link"]') - .first() - .props().href - ).toEqual( - 'https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=80' - ); - }); - - test('it renders event.duration', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="event-duration"]') - .first() - .text() - ).toEqual('1ms'); - }); - - test('it renders event.end', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="event-end"]') - .first() - .text().length - ).toBeGreaterThan(0); // the format of this date will depend on the user's locale and settings - }); - - test('it renders event.start', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="event-start"]') - .first() - .text().length - ).toBeGreaterThan(0); // the format of this date will depend on the user's locale and settings - }); - - test('it renders network.bytes', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="network-bytes"]') - .first() - .text() - ).toEqual('100B'); - }); - - test('it renders network.community_id', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="network-community-id"]') - .first() - .text() - ).toEqual('we.live.in.a'); - }); - - test('it renders network.direction', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="network-direction"]') - .first() - .text() - ).toEqual('outgoing'); - }); - - test('it renders network.packets', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="network-packets"]') - .first() - .text() - ).toEqual('3 pkts'); - }); - - test('it renders network.protocol', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="network-protocol"]') - .first() - .text() - ).toEqual('http'); - }); - - test('it renders process.name', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="process-name"]') - .first() - .text() - ).toEqual('rat'); - }); - - test('it renders a source label', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="source-label"]') - .first() - .text() - ).toEqual(i18n.SOURCE); - }); - - test('it renders source.bytes', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="source-bytes"]') - .first() - .text() - ).toEqual('60B'); - }); - - test('it renders source.geo.continent_name', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="source.geo.continent_name"]') - .first() - .text() - ).toEqual('North America'); - }); - - test('it renders source.geo.country_name', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="source.geo.country_name"]') - .first() - .text() - ).toEqual('United States'); - }); - - test('it renders source.geo.country_iso_code', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="source.geo.country_iso_code"]') - .first() - .text() - ).toEqual('US'); - }); - - test('it renders source.geo.region_name', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="source.geo.region_name"]') - .first() - .text() - ).toEqual('Georgia'); - }); - - test('it renders source.geo.city_name', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="source.geo.city_name"]') - .first() - .text() - ).toEqual('Atlanta'); - }); - - test('it renders the source ip and port, separated with a colon', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="source-ip-and-port"]') - .first() - .text() - ).toEqual('192.168.1.2:9987'); - }); - - test('it renders source.packets', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="source-packets"]') - .first() - .text() - ).toEqual('2 pkts'); - }); - - test('it hyperlinks tls.client_certificate.fingerprint.sha1 site to compare the fingerprint against a known set of signatures', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="client-certificate-fingerprint"]') - .find('[data-test-subj="certificate-fingerprint-link"]') - .first() - .props().href - ).toEqual( - 'https://sslbl.abuse.ch/ssl-certificates/sha1/tls.client_certificate.fingerprint.sha1-value' - ); - }); - - test('renders tls.client_certificate.fingerprint.sha1 text', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="client-certificate-fingerprint"]') - .find('[data-test-subj="certificate-fingerprint-link"]') - .first() - .text() - ).toEqual('tls.client_certificate.fingerprint.sha1-value'); - }); - - test('it hyperlinks tls.fingerprints.ja3.hash site to compare the fingerprint against a known set of signatures', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="ja3-fingerprint-link"]') - .first() - .props().href - ).toEqual('https://sslbl.abuse.ch/ja3-fingerprints/tls.fingerprints.ja3.hash-value'); - }); - - test('renders tls.fingerprints.ja3.hash text', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="ja3-fingerprint-link"]') - .first() - .text() - ).toEqual('tls.fingerprints.ja3.hash-value'); - }); - - test('it hyperlinks tls.server_certificate.fingerprint.sha1 site to compare the fingerprint against a known set of signatures', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="server-certificate-fingerprint"]') - .find('[data-test-subj="certificate-fingerprint-link"]') - .first() - .props().href - ).toEqual( - 'https://sslbl.abuse.ch/ssl-certificates/sha1/tls.server_certificate.fingerprint.sha1-value' - ); - }); - - test('renders tls.server_certificate.fingerprint.sha1 text', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="server-certificate-fingerprint"]') - .find('[data-test-subj="certificate-fingerprint-link"]') - .first() - .text() - ).toEqual('tls.server_certificate.fingerprint.sha1-value'); - }); - - test('it renders network.transport', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="network-transport"]') - .first() - .text() - ).toEqual('tcp'); - }); - - test('it renders user.name', () => { - const wrapper = mount({getNetflowInstance()}); - - expect( - wrapper - .find('[data-test-subj="user-name"]') - .first() - .text() - ).toEqual('first.last'); - }); -}); diff --git a/x-pack/plugins/siem/public/components/netflow/netflow_columns/index.tsx b/x-pack/plugins/siem/public/components/netflow/netflow_columns/index.tsx deleted file mode 100644 index f8a0256ff4d435..00000000000000 --- a/x-pack/plugins/siem/public/components/netflow/netflow_columns/index.tsx +++ /dev/null @@ -1,127 +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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React from 'react'; -import styled from 'styled-components'; - -import { SourceDestination } from '../../source_destination'; - -import { DurationEventStartEnd } from './duration_event_start_end'; -import { NetflowColumnsProps } from './types'; -import { UserProcess } from './user_process'; - -export const EVENT_START = 'event.start'; -export const EVENT_END = 'event.end'; - -const EuiFlexItemMarginRight = styled(EuiFlexItem)` - margin-right: 10px; -`; - -EuiFlexItemMarginRight.displayName = 'EuiFlexItemMarginRight'; - -/** - * Renders columns of draggable badges that describe both Netflow data, or more - * generally, hosts interacting over a network connection. This component is - * consumed by the `Netflow` visualization / row renderer. - * - * This component will allow columns to wrap if constraints on width prevent all - * the columns from fitting on a single horizontal row - */ -export const NetflowColumns = React.memo( - ({ - contextId, - destinationBytes, - destinationGeoContinentName, - destinationGeoCountryName, - destinationGeoCountryIsoCode, - destinationGeoRegionName, - destinationGeoCityName, - destinationIp, - destinationPackets, - destinationPort, - eventDuration, - eventId, - eventEnd, - eventStart, - networkBytes, - networkCommunityId, - networkDirection, - networkPackets, - networkProtocol, - processName, - sourceBytes, - sourceGeoContinentName, - sourceGeoCountryName, - sourceGeoCountryIsoCode, - sourceGeoRegionName, - sourceGeoCityName, - sourceIp, - sourcePackets, - sourcePort, - transport, - userName, - }) => ( - - - - - - - - - - - - - - ) -); - -NetflowColumns.displayName = 'NetflowColumns'; diff --git a/x-pack/plugins/siem/public/components/news_feed/helpers.test.ts b/x-pack/plugins/siem/public/components/news_feed/helpers.test.ts deleted file mode 100644 index 96bd9b08bf8bf0..00000000000000 --- a/x-pack/plugins/siem/public/components/news_feed/helpers.test.ts +++ /dev/null @@ -1,492 +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 { NEWS_FEED_URL_SETTING_DEFAULT } from '../../../common/constants'; -import { KibanaServices } from '../../lib/kibana'; -import { rawNewsApiResponse } from '../../mock/news'; -import { rawNewsJSON } from '../../mock/raw_news'; - -import { - fetchNews, - getLocale, - getNewsFeedUrl, - getNewsItemsFromApiResponse, - removeSnapshotFromVersion, - showNewsItem, -} from './helpers'; -import { NewsItem, RawNewsApiResponse } from './types'; - -jest.mock('../../lib/kibana'); - -describe('helpers', () => { - describe('removeSnapshotFromVersion', () => { - test('it should remove an all-caps `-SNAPSHOT`', () => { - const version = '8.0.0-SNAPSHOT'; - - expect(removeSnapshotFromVersion(version)).toEqual('8.0.0'); - }); - - test('it should remove a mixed-case `-SnApShoT`', () => { - const version = '8.0.0-SnApShoT'; - - expect(removeSnapshotFromVersion(version)).toEqual('8.0.0'); - }); - - test('it should remove all occurrences of `-SNAPSHOT`, regardless of where they appear in the version', () => { - const version = '-SNAPSHOT8.0.0-SNAPSHOT'; - - expect(removeSnapshotFromVersion(version)).toEqual('8.0.0'); - }); - - test('it should NOT transform a version when it does not contain a `-SNAPSHOT`', () => { - const version = '8.0.0'; - - expect(removeSnapshotFromVersion(version)).toEqual('8.0.0'); - }); - - test('it should NOT transform a version if it omits the dash in `SNAPSHOT`', () => { - const version = '8.0.0SNAPSHOT'; - - expect(removeSnapshotFromVersion(version)).toEqual('8.0.0SNAPSHOT'); - }); - - test('it should NOT transform a version if has only a partial `-SNAPSHOT`', () => { - const version = '8.0.0-SNAP'; - - expect(removeSnapshotFromVersion(version)).toEqual('8.0.0-SNAP'); - }); - - test('it should NOT transform an undefined version', () => { - const version = undefined; - - expect(removeSnapshotFromVersion(version)).toBeUndefined(); - }); - - test('it should NOT transform an empty version', () => { - const version = ''; - - expect(removeSnapshotFromVersion(version)).toEqual(''); - }); - }); - - describe('getNewsFeedUrl', () => { - const getKibanaVersion = () => '8.0.0'; - - test('it combines the (default) base URL from settings and the Kibana version to return the expected URL', () => { - expect( - getNewsFeedUrl({ newsFeedUrlSetting: NEWS_FEED_URL_SETTING_DEFAULT, getKibanaVersion }) - ).toEqual('https://feeds.elastic.co/security-solution/v8.0.0.json'); - }); - - test('it combines a URL with extra whitespace and the Kibana version to return the expected URL', () => { - const withExtraWhitespace = ` ${NEWS_FEED_URL_SETTING_DEFAULT} `; - - expect(getNewsFeedUrl({ newsFeedUrlSetting: withExtraWhitespace, getKibanaVersion })).toEqual( - 'https://feeds.elastic.co/security-solution/v8.0.0.json' - ); - }); - - test('it combines a URL with a trailing slash and the Kibana version to return the expected URL', () => { - const withTrailingSlash = `${NEWS_FEED_URL_SETTING_DEFAULT}/`; - - expect(getNewsFeedUrl({ newsFeedUrlSetting: withTrailingSlash, getKibanaVersion })).toEqual( - 'https://feeds.elastic.co/security-solution/v8.0.0.json' - ); - }); - - test('it combines a URL with a trailing slash plus whitespace and the Kibana version to return the expected URL', () => { - const withTrailingSlashPlusWhitespace = ` ${NEWS_FEED_URL_SETTING_DEFAULT}/ `; - - expect( - getNewsFeedUrl({ newsFeedUrlSetting: withTrailingSlashPlusWhitespace, getKibanaVersion }) - ).toEqual('https://feeds.elastic.co/security-solution/v8.0.0.json'); - }); - - test('it combines a URL and a Kibana version with a `-SNAPSHOT` to return the expected URL', () => { - const getKibanaVersionWithSnapshot = () => '8.0.0-SNAPSHOT'; - - expect( - getNewsFeedUrl({ - newsFeedUrlSetting: NEWS_FEED_URL_SETTING_DEFAULT, - getKibanaVersion: getKibanaVersionWithSnapshot, - }) - ).toEqual('https://feeds.elastic.co/security-solution/v8.0.0.json'); - }); - }); - - describe('getLocale', () => { - const fallback = 'wowzers'; - - test('it returns language specified in the document', () => { - const lang = 'ja'; - - document.documentElement.lang = lang; - - expect(getLocale(fallback)).toEqual(lang); - }); - - test('it returns the fallback when the language in the document is an empty string', () => { - document.documentElement.lang = ''; - - expect(getLocale(fallback)).toEqual(fallback); - }); - }); - - describe('getNewsItemsFromApiResponse', () => { - const expectedNewsItems: NewsItem[] = [ - { - description: - "There's an awesome community of Elastic SIEM users out there. Join the discussion about configuring, learning, and using the Elastic SIEM app, and detecting threats!", - expireOn: expect.any(Date), - hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', - imageUrl: - 'https://aws1.discourse-cdn.com/elastic/original/3X/f/8/f8c3d0b9971cfcd0be349d973aa5799f71d280cc.png?blade=securitysolutionfeed', - linkUrl: 'https://discuss.elastic.co/c/siem?blade=securitysolutionfeed', - publishOn: expect.any(Date), - title: 'Got SIEM Questions?', - }, - { - description: - 'Elastic Security combines the threat hunting and analytics of Elastic SIEM with the prevention and response provided by Elastic Endpoint Security.', - expireOn: expect.any(Date), - hash: 'edcb2d396ffdd80bfd5a97fbc0dc9f4b73477f9be556863fe0a1caf086679420', - imageUrl: - 'https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/blt1caa35177420c61b/5d0d0394d8ff351753cbf2c5/illustrated-screenshot-hero-siem.png?blade=securitysolutionfeed', - linkUrl: - 'https://www.elastic.co/blog/elastic-security-7-5-0-released?blade=securitysolutionfeed', - publishOn: expect.any(Date), - title: 'Elastic Security 7.5.0 released', - }, - { - description: - 'At Elastic, we’re bringing endpoint protection and SIEM together into the same experience to streamline how you secure your organization.', - expireOn: expect.any(Date), - hash: 'ec970adc85e9eede83f77e4cc6a6fea00cd7822cbe48a71dc2c5f1df10939196', - imageUrl: - 'https://static-www.elastic.co/v3/assets/bltefdd0b53724fa2ce/bltd0eb8689eafe398a/5d970ecc1970e80e85277925/illustration-endpoint-hero.png?blade=securitysolutionfeed', - linkUrl: - 'https://www.elastic.co/webinars/elastic-endpoint-security-overview-security-starts-at-the-endpoint?blade=securitysolutionfeed', - publishOn: expect.any(Date), - title: 'Elastic Endpoint Security Overview Webinar', - }, - { - description: - 'For small businesses and homes, having access to effective security analytics can come at a high cost of either time or money. Well, until now!', - expireOn: expect.any(Date), - hash: 'aa243fd5845356a5ccd54a7a11b208ed307e0d88158873b1fcf7d1164b739bac', - imageUrl: - 'https://images.contentstack.io/v3/assets/bltefdd0b53724fa2ce/blt024c26b7636cb24f/5daf4e293a326d6df6c0e025/home-siem-blog-1-map.jpg?blade=securitysolutionfeed', - linkUrl: - 'https://www.elastic.co/blog/elastic-siem-for-small-business-and-home-1-getting-started?blade=securitysolutionfeed', - publishOn: expect.any(Date), - title: 'Trying Elastic SIEM at Home?', - }, - { - description: - 'Elastic is excited to announce the introduction of Elastic Endpoint Security, based on Elastic’s acquisition of Endgame, a pioneer and industry-recognized leader in endpoint threat prevention, detection, and response.', - expireOn: expect.any(Date), - hash: '3c64576c9749d33ff98726d641cdf2fb2bfde3dd9a6f99ff2573ac8d8c5b2c02', - imageUrl: - 'https://images.contentstack.io/v3/assets/bltefdd0b53724fa2ce/blt1f87637fb7870298/5d9fe27bf8ca980f8717f6f8/screenshot-resolver-trickbot-enrichments-showing-defender-shutdown-endgame-2-optimized.png?blade=securitysolutionfeed', - linkUrl: - 'https://www.elastic.co/blog/introducing-elastic-endpoint-security?blade=securitysolutionfeed', - publishOn: expect.any(Date), - title: 'Introducing Elastic Endpoint Security', - }, - { - description: - 'Elastic SIEM is powered by Elastic Common Schema. With ECS, analytics content such as dashboards, rules, and machine learning jobs can be applied more broadly, searches can be crafted more narrowly, and field names are easier to remember.', - expireOn: expect.any(Date), - hash: 'b8a0d3d21e9638bde891ab5eb32594b3d7a3daacc7f0900c6dd506d5d7b42410', - imageUrl: - 'https://images.contentstack.io/v3/assets/bltefdd0b53724fa2ce/blt71256f06dc672546/5c98d595975fd58f4d12646d/ecs-intro-dashboard-1360.jpg?blade=securitysolutionfeed', - linkUrl: - 'https://www.elastic.co/blog/introducing-the-elastic-common-schema?blade=securitysolutionfeed', - publishOn: expect.any(Date), - title: 'What is Elastic Common Schema (ECS)?', - }, - ]; - - test('it returns an empty collection of news items when the response is undefined', () => { - expect(getNewsItemsFromApiResponse(undefined)).toEqual([]); - }); - - test('it returns an empty collection of news items when the response is null', () => { - expect(getNewsItemsFromApiResponse(null)).toEqual([]); - }); - - test('it returns an empty collection of news items when the response items are undefined', () => { - expect(getNewsItemsFromApiResponse({ items: undefined })).toEqual([]); - }); - - test('it returns an empty collection of news items when the response items are null', () => { - expect(getNewsItemsFromApiResponse({ items: null })).toEqual([]); - }); - - test('it returns the expected news items when the browser language matches the i18n values in the response', () => { - const lang = 'en'; - - document.documentElement.lang = lang; - - expect(getNewsItemsFromApiResponse(rawNewsApiResponse)).toEqual(expectedNewsItems); - }); - - test('it returns the expected news items when an ALL CAPS the browser language matches the i18n values in the response', () => { - const allCapsLang = 'EN'; - - document.documentElement.lang = allCapsLang; - - expect(getNewsItemsFromApiResponse(rawNewsApiResponse)).toEqual(expectedNewsItems); - }); - - test('it returns the expected news items when the browser language does NOT match the i18n values in the response', () => { - const nonMatchingLang = 'ja'; - - document.documentElement.lang = nonMatchingLang; - - expect(getNewsItemsFromApiResponse(rawNewsApiResponse)).toEqual(expectedNewsItems); - }); - - test('it returns the expected news items when the browser language is an empty string', () => { - const emptyLang = ''; - - document.documentElement.lang = emptyLang; - - expect(getNewsItemsFromApiResponse(rawNewsApiResponse)).toEqual(expectedNewsItems); - }); - - test('it returns the expected news item when parsing a raw JSON response', () => { - const lang = 'en'; - - document.documentElement.lang = lang; - - expect(getNewsItemsFromApiResponse(JSON.parse(rawNewsJSON))).toEqual(expectedNewsItems); - }); - - describe('translated items', () => { - const translatedDescription = - 'Elastic SIEMユーザーの素晴らしいコミュニティがそこにあります。 Elastic SIEMアプリの設定、学習、使用、および脅威の検出に関するディスカッションに参加してください!'; - const translatedImageUrl = 'https://aws1.discourse-cdn.com/elastic/translated-image-url'; - const translatedLinkUrl = 'https://discuss.elastic.co/translated-link-url'; - const translatedTitle = 'SIEMに関する質問はありますか?'; - - const withNonDefaultTranslations: RawNewsApiResponse = { - items: [ - { - title: { en: 'Got SIEM Questions?', ja: translatedTitle }, - description: { - en: - "There's an awesome community of Elastic SIEM users out there. Join the discussion about configuring, learning, and using the Elastic SIEM app, and detecting threats!", - ja: translatedDescription, - }, - link_text: null, - link_url: { - en: 'https://discuss.elastic.co/c/siem?blade=securitysolutionfeed', - ja: translatedLinkUrl, - }, - languages: null, - badge: { en: '7.6' }, - image_url: { - en: - 'https://aws1.discourse-cdn.com/elastic/original/3X/f/8/f8c3d0b9971cfcd0be349d973aa5799f71d280cc.png?blade=securitysolutionfeed', - ja: translatedImageUrl, - }, - publish_on: new Date('2020-01-01T00:00:00'), - expire_on: new Date('2020-12-31T00:00:00'), - }, - ], - }; - - test('it returns a translated description when the browser language matches additional translated content', () => { - const lang = 'ja'; // an additional translation for this language is provided in the response - - document.documentElement.lang = lang; - - expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].description).toEqual( - translatedDescription - ); - }); - - test('it returns a translated imageUrl when the browser language matches additional translated content', () => { - const lang = 'ja'; // a translation for this language is provided in the response - - document.documentElement.lang = lang; - - expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].imageUrl).toEqual( - translatedImageUrl - ); - }); - - test('it returns a translated linkUrl when the browser language matches additional translated content', () => { - const lang = 'ja'; // a translation for this language is provided in the response - - document.documentElement.lang = lang; - - expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].linkUrl).toEqual( - translatedLinkUrl - ); - }); - - test('it returns a translated title when the browser language matches additional translated content', () => { - const lang = 'ja'; // a translation for this language is provided in the response - - document.documentElement.lang = lang; - - expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].title).toEqual( - translatedTitle - ); - }); - - test('it returns the default translated title when the browser language matches additional translated content', () => { - const lang = 'fr'; // no translation for this language - - document.documentElement.lang = lang; - - expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].title).toEqual( - 'Got SIEM Questions?' - ); - }); - - test('it returns the default translated title when the browser language is an empty string', () => { - const lang = ''; // just an empty string - - document.documentElement.lang = lang; - - expect(getNewsItemsFromApiResponse(withNonDefaultTranslations)[0].title).toEqual( - 'Got SIEM Questions?' - ); - }); - }); - - test('it generates a news item hash when an item does NOT include it', () => { - const lang = 'en'; - - const itemHasNoHash: RawNewsApiResponse = { - items: [ - { - title: { en: 'Got SIEM Questions?' }, - description: { - en: 'some description', - }, - link_text: null, - link_url: { en: 'https://example.com/link-url' }, - languages: null, - badge: { en: '7.6' }, - image_url: { - en: 'https://example.com/image-url', - }, - publish_on: new Date('2020-01-01T00:00:00'), - expire_on: new Date('2020-12-31T00:00:00'), - }, - ], - }; - - document.documentElement.lang = lang; - - expect(getNewsItemsFromApiResponse(itemHasNoHash)[0].hash.length).toBeGreaterThan(0); - }); - }); - - describe('fetchNews', () => { - const mockKibanaServices = KibanaServices.get as jest.Mock; - const fetchMock = jest.fn(); - mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } }); - - beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(rawNewsApiResponse); - }); - - test('it returns the raw API response from the news feed', async () => { - const newsFeedUrl = 'https://feeds.elastic.co/security-solution/v8.0.0.json'; - expect(await fetchNews({ newsFeedUrl })).toEqual(rawNewsApiResponse); - }); - }); - - describe('showNewsItem', () => { - const MOCK_DATE_NOW = 1579848101395; // 2020-01-24T06:41:41.395Z - - let dateNowSpy: { mockRestore: () => void }; - - beforeAll(() => { - dateNowSpy = jest.spyOn(Date, 'now').mockImplementation(() => MOCK_DATE_NOW); - }); - - afterAll(() => { - dateNowSpy.mockRestore(); - }); - - test('it should return true when the article has already been published, and will expire in the future', () => { - const alreadyPublishedAndNotExpired: NewsItem = { - description: 'description', - expireOn: new Date(MOCK_DATE_NOW + 1000), - hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', - imageUrl: 'https://example.com', - linkUrl: 'https://example.com', - publishOn: new Date(MOCK_DATE_NOW - 1000), - title: 'Show this post', - }; - - expect(showNewsItem(alreadyPublishedAndNotExpired)).toEqual(true); - }); - - test('it should return false when the article was published exactly "now", and will expire in the future', () => { - const publishedJustNowAndNotExpired: NewsItem = { - description: 'description', - expireOn: new Date(MOCK_DATE_NOW + 1000), - hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', - imageUrl: 'https://example.com', - linkUrl: 'https://example.com', - publishOn: new Date(MOCK_DATE_NOW), - title: 'Do NOT show this post', - }; - - expect(showNewsItem(publishedJustNowAndNotExpired)).toEqual(false); - }); - - test('it should return false when the article has not been published yet, and has not expired yet', () => { - const notPublishedAndNotExpired: NewsItem = { - description: 'description', - expireOn: new Date(MOCK_DATE_NOW + 5000), - hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', - imageUrl: 'https://example.com', - linkUrl: 'https://example.com', - publishOn: new Date(MOCK_DATE_NOW + 1000), - title: 'Do NOT show this post', - }; - - expect(showNewsItem(notPublishedAndNotExpired)).toEqual(false); - }); - - test('it should return false when the article was published in the past, and will expire exactly now', () => { - const alreadyPublishedAndExpiredNow: NewsItem = { - description: 'description', - expireOn: new Date(MOCK_DATE_NOW), - hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', - imageUrl: 'https://example.com', - linkUrl: 'https://example.com', - publishOn: new Date(MOCK_DATE_NOW - 1000), - title: 'Do NOT show this post', - }; - - expect(showNewsItem(alreadyPublishedAndExpiredNow)).toEqual(false); - }); - - test('it should return false when the article was published in the past, and it already expired', () => { - const articleJustExpired: NewsItem = { - description: 'description', - expireOn: new Date(MOCK_DATE_NOW - 1000), - hash: '5a35c984a9cdc1c6a25913f3d0b99b1aefc7257bc3b936c39db9fa0435edeed0', - imageUrl: 'https://example.com', - linkUrl: 'https://example.com', - publishOn: new Date(MOCK_DATE_NOW - 5000), - title: 'Do NOT show this post', - }; - - expect(showNewsItem(articleJustExpired)).toEqual(false); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/notes/add_note/index.tsx b/x-pack/plugins/siem/public/components/notes/add_note/index.tsx deleted file mode 100644 index 28cab2b46755f8..00000000000000 --- a/x-pack/plugins/siem/public/components/notes/add_note/index.tsx +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React, { useCallback } from 'react'; -import styled from 'styled-components'; - -import { MarkdownHint } from '../../markdown/markdown_hint'; -import { - AssociateNote, - GetNewNoteId, - updateAndAssociateNode, - UpdateInternalNewNote, - UpdateNote, -} from '../helpers'; -import * as i18n from '../translations'; - -import { NewNote } from './new_note'; - -const AddNotesContainer = styled(EuiFlexGroup)` - margin-bottom: 5px; - user-select: none; -`; - -AddNotesContainer.displayName = 'AddNotesContainer'; - -const ButtonsContainer = styled(EuiFlexGroup)` - margin-top: 5px; -`; - -ButtonsContainer.displayName = 'ButtonsContainer'; - -export const CancelButton = React.memo<{ onCancelAddNote: () => void }>(({ onCancelAddNote }) => ( - - {i18n.CANCEL} - -)); - -CancelButton.displayName = 'CancelButton'; - -/** Displays an input for entering a new note, with an adjacent "Add" button */ -export const AddNote = React.memo<{ - associateNote: AssociateNote; - getNewNoteId: GetNewNoteId; - newNote: string; - onCancelAddNote?: () => void; - updateNewNote: UpdateInternalNewNote; - updateNote: UpdateNote; -}>(({ associateNote, getNewNoteId, newNote, onCancelAddNote, updateNewNote, updateNote }) => { - const handleClick = useCallback( - () => - updateAndAssociateNode({ - associateNote, - getNewNoteId, - newNote, - updateNewNote, - updateNote, - }), - [associateNote, getNewNoteId, newNote, updateNewNote, updateNote] - ); - - return ( - - - - 0} /> - - - {onCancelAddNote != null ? ( - - - - ) : null} - - - {i18n.ADD_NOTE} - - - - - ); -}); - -AddNote.displayName = 'AddNote'; diff --git a/x-pack/plugins/siem/public/components/notes/helpers.tsx b/x-pack/plugins/siem/public/components/notes/helpers.tsx deleted file mode 100644 index c933055186e078..00000000000000 --- a/x-pack/plugins/siem/public/components/notes/helpers.tsx +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiIcon, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; -import moment from 'moment'; -import React from 'react'; -import styled from 'styled-components'; - -import { Note } from '../../lib/note'; - -import * as i18n from './translations'; -import { CountBadge } from '../page'; - -/** Performs IO to update (or add a new) note */ -export type UpdateNote = (note: Note) => void; -/** Performs IO to associate a note with something (e.g. a timeline, an event, etc). (The "something" is opaque to the caller) */ -export type AssociateNote = (noteId: string) => void; -/** Performs IO to get a new note ID */ -export type GetNewNoteId = () => string; -/** Updates the local state containing a new note being edited by the user */ -export type UpdateInternalNewNote = (newNote: string) => void; -/** Closes the notes popover */ -export type OnClosePopover = () => void; -/** Performs IO to associate a note with an event */ -export type AddNoteToEvent = ({ eventId, noteId }: { eventId: string; noteId: string }) => void; - -/** - * Defines the behavior of the search input that appears above the table of data - */ -export const search = { - box: { - incremental: true, - placeholder: i18n.SEARCH_PLACEHOLDER, - schema: { - fields: { - user: 'string', - note: 'string', - }, - }, - }, -}; - -const TitleText = styled.h3` - margin: 0 5px; - cursor: default; - user-select: none; -`; - -TitleText.displayName = 'TitleText'; - -/** Displays a count of the existing notes */ -export const NotesCount = React.memo<{ - noteIds: string[]; -}>(({ noteIds }) => ( - - - - - - - - {i18n.NOTES} - - - - - {noteIds.length} - - -)); - -NotesCount.displayName = 'NotesCount'; - -/** Creates a new instance of a `note` */ -export const createNote = ({ - newNote, - getNewNoteId, -}: { - newNote: string; - getNewNoteId: GetNewNoteId; -}): Note => ({ - created: moment.utc().toDate(), - id: getNewNoteId(), - lastEdit: null, - note: newNote.trim(), - saveObjectId: null, - user: 'elastic', // TODO: get the logged-in Kibana user - version: null, -}); - -interface UpdateAndAssociateNodeParams { - associateNote: AssociateNote; - getNewNoteId: GetNewNoteId; - newNote: string; - updateNewNote: UpdateInternalNewNote; - updateNote: UpdateNote; -} - -export const updateAndAssociateNode = ({ - associateNote, - getNewNoteId, - newNote, - updateNewNote, - updateNote, -}: UpdateAndAssociateNodeParams) => { - const note = createNote({ newNote, getNewNoteId }); - updateNote(note); // perform IO to store the newly-created note - associateNote(note.id); // associate the note with the (opaque) thing - updateNewNote(''); // clear the input -}; diff --git a/x-pack/plugins/siem/public/components/notes/index.tsx b/x-pack/plugins/siem/public/components/notes/index.tsx deleted file mode 100644 index b5fef9a5e4d413..00000000000000 --- a/x-pack/plugins/siem/public/components/notes/index.tsx +++ /dev/null @@ -1,87 +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 { - EuiInMemoryTable, - EuiInMemoryTableProps, - EuiModalBody, - EuiModalHeader, - EuiPanel, - EuiSpacer, -} from '@elastic/eui'; -import React, { useState } from 'react'; -import styled from 'styled-components'; - -import { Note } from '../../lib/note'; - -import { AddNote } from './add_note'; -import { columns } from './columns'; -import { AssociateNote, GetNewNoteId, NotesCount, search, UpdateNote } from './helpers'; -import { NOTES_PANEL_WIDTH, NOTES_PANEL_HEIGHT } from '../timeline/properties/notes_size'; - -interface Props { - associateNote: AssociateNote; - getNotesByIds: (noteIds: string[]) => Note[]; - getNewNoteId: GetNewNoteId; - noteIds: string[]; - updateNote: UpdateNote; -} - -const NotesPanel = styled(EuiPanel)` - height: ${NOTES_PANEL_HEIGHT}px; - width: ${NOTES_PANEL_WIDTH}px; - - & thead { - display: none; - } -`; - -NotesPanel.displayName = 'NotesPanel'; - -const InMemoryTable: typeof EuiInMemoryTable & { displayName: string } = styled( - EuiInMemoryTable as React.ComponentType> -)` - overflow-x: hidden; - overflow-y: auto; - height: 220px; -` as any; // eslint-disable-line @typescript-eslint/no-explicit-any - -InMemoryTable.displayName = 'InMemoryTable'; - -/** A view for entering and reviewing notes */ -export const Notes = React.memo( - ({ associateNote, getNotesByIds, getNewNoteId, noteIds, updateNote }) => { - const [newNote, setNewNote] = useState(''); - - return ( - - - - - - - - - - - - ); - } -); - -Notes.displayName = 'Notes'; diff --git a/x-pack/plugins/siem/public/components/notes/note_cards/index.test.tsx b/x-pack/plugins/siem/public/components/notes/note_cards/index.test.tsx deleted file mode 100644 index f70e841d1eefd3..00000000000000 --- a/x-pack/plugins/siem/public/components/notes/note_cards/index.test.tsx +++ /dev/null @@ -1,137 +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 { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { ThemeProvider } from 'styled-components'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; - -import { Note } from '../../../lib/note'; - -import { NoteCards } from '.'; - -describe('NoteCards', () => { - const noteIds = ['abc', 'def']; - const theme = () => ({ eui: euiDarkVars, darkMode: true }); - - const getNotesByIds = (_: string[]): Note[] => [ - { - created: new Date(), - id: 'abc', - lastEdit: null, - note: 'a fake note', - saveObjectId: null, - user: 'elastic', - version: null, - }, - { - created: new Date(), - id: 'def', - lastEdit: null, - note: 'another fake note', - saveObjectId: null, - user: 'elastic', - version: null, - }, - ]; - - test('it renders the notes column when noteIds are specified', () => { - const wrapper = mountWithIntl( - - - - ); - - expect(wrapper.find('[data-test-subj="notes"]').exists()).toEqual(true); - }); - - test('it does NOT render the notes column when noteIds are NOT specified', () => { - const wrapper = mountWithIntl( - - - - ); - - expect(wrapper.find('[data-test-subj="notes"]').exists()).toEqual(false); - }); - - test('renders note cards', () => { - const wrapper = mountWithIntl( - - - - ); - - expect( - wrapper - .find('[data-test-subj="note-card"]') - .find('[data-test-subj="note-card-body"]') - .find('[data-test-subj="markdown-root"]') - .first() - .text() - ).toEqual(getNotesByIds(noteIds)[0].note); - }); - - test('it shows controls for adding notes when showAddNote is true', () => { - const wrapper = mountWithIntl( - - - - ); - - expect(wrapper.find('[data-test-subj="add-note"]').exists()).toEqual(true); - }); - - test('it does NOT show controls for adding notes when showAddNote is false', () => { - const wrapper = mountWithIntl( - - - - ); - - expect(wrapper.find('[data-test-subj="add-note"]').exists()).toEqual(false); - }); -}); diff --git a/x-pack/plugins/siem/public/components/notes/note_cards/index.tsx b/x-pack/plugins/siem/public/components/notes/note_cards/index.tsx deleted file mode 100644 index 6664660eb6bdc2..00000000000000 --- a/x-pack/plugins/siem/public/components/notes/note_cards/index.tsx +++ /dev/null @@ -1,106 +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 { EuiFlexGroup, EuiPanel } from '@elastic/eui'; -import React, { useState, useCallback } from 'react'; -import styled from 'styled-components'; - -import { Note } from '../../../lib/note'; -import { AddNote } from '../add_note'; -import { AssociateNote, GetNewNoteId, UpdateNote } from '../helpers'; -import { NoteCard } from '../note_card'; - -const AddNoteContainer = styled.div``; -AddNoteContainer.displayName = 'AddNoteContainer'; - -const NoteContainer = styled.div` - margin-top: 5px; -`; -NoteContainer.displayName = 'NoteContainer'; - -interface NoteCardsCompProps { - children: React.ReactNode; -} - -const NoteCardsComp = React.memo(({ children }) => ( - - {children} - -)); -NoteCardsComp.displayName = 'NoteCardsComp'; - -const NotesContainer = styled(EuiFlexGroup)` - padding: 0 5px; - margin-bottom: 5px; -`; -NotesContainer.displayName = 'NotesContainer'; - -interface Props { - associateNote: AssociateNote; - getNotesByIds: (noteIds: string[]) => Note[]; - getNewNoteId: GetNewNoteId; - noteIds: string[]; - showAddNote: boolean; - toggleShowAddNote: () => void; - updateNote: UpdateNote; -} - -/** A view for entering and reviewing notes */ -export const NoteCards = React.memo( - ({ - associateNote, - getNotesByIds, - getNewNoteId, - noteIds, - showAddNote, - toggleShowAddNote, - updateNote, - }) => { - const [newNote, setNewNote] = useState(''); - - const associateNoteAndToggleShow = useCallback( - (noteId: string) => { - associateNote(noteId); - toggleShowAddNote(); - }, - [associateNote, toggleShowAddNote] - ); - - return ( - - {noteIds.length ? ( - - {getNotesByIds(noteIds).map(note => ( - - - - ))} - - ) : null} - - {showAddNote ? ( - - - - ) : null} - - ); - } -); - -NoteCards.displayName = 'NoteCards'; diff --git a/x-pack/plugins/siem/public/components/open_timeline/export_timeline/index.tsx b/x-pack/plugins/siem/public/components/open_timeline/export_timeline/index.tsx deleted file mode 100644 index 12cf952bb1ff86..00000000000000 --- a/x-pack/plugins/siem/public/components/open_timeline/export_timeline/index.tsx +++ /dev/null @@ -1,73 +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, { useState, useCallback } from 'react'; -import { DeleteTimelines } from '../types'; - -import { TimelineDownloader } from './export_timeline'; -import { DeleteTimelineModalOverlay } from '../delete_timeline_modal'; -import { exportSelectedTimeline } from '../../../containers/timeline/api'; - -export interface ExportTimeline { - disableExportTimelineDownloader: () => void; - enableExportTimelineDownloader: () => void; - isEnableDownloader: boolean; -} - -export const useExportTimeline = (): ExportTimeline => { - const [isEnableDownloader, setIsEnableDownloader] = useState(false); - - const enableExportTimelineDownloader = useCallback(() => { - setIsEnableDownloader(true); - }, []); - - const disableExportTimelineDownloader = useCallback(() => { - setIsEnableDownloader(false); - }, []); - - return { - disableExportTimelineDownloader, - enableExportTimelineDownloader, - isEnableDownloader, - }; -}; - -const EditTimelineActionsComponent: React.FC<{ - deleteTimelines: DeleteTimelines | undefined; - ids: string[]; - isEnableDownloader: boolean; - isDeleteTimelineModalOpen: boolean; - onComplete: () => void; - title: string; -}> = ({ - deleteTimelines, - ids, - isEnableDownloader, - isDeleteTimelineModalOpen, - onComplete, - title, -}) => ( - <> - - {deleteTimelines != null && ( - - )} - -); - -export const EditTimelineActions = React.memo(EditTimelineActionsComponent); -export const EditOneTimelineAction = React.memo(EditTimelineActionsComponent); diff --git a/x-pack/plugins/siem/public/components/open_timeline/helpers.test.ts b/x-pack/plugins/siem/public/components/open_timeline/helpers.test.ts deleted file mode 100644 index a7c0b08fc8a21e..00000000000000 --- a/x-pack/plugins/siem/public/components/open_timeline/helpers.test.ts +++ /dev/null @@ -1,893 +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 { cloneDeep, omit } from 'lodash/fp'; -import { Dispatch } from 'redux'; - -import { - mockTimelineResults, - mockTimelineResult, - mockTimelineModel, -} from '../../mock/timeline_results'; -import { timelineDefaults } from '../../store/timeline/defaults'; -import { setTimelineRangeDatePicker as dispatchSetTimelineRangeDatePicker } from '../../store/inputs/actions'; -import { - setKqlFilterQueryDraft as dispatchSetKqlFilterQueryDraft, - applyKqlFilterQuery as dispatchApplyKqlFilterQuery, - addTimeline as dispatchAddTimeline, - addNote as dispatchAddGlobalTimelineNote, -} from '../../store/timeline/actions'; -import { - addNotes as dispatchAddNotes, - updateNote as dispatchUpdateNote, -} from '../../store/app/actions'; -import { - defaultTimelineToTimelineModel, - getNotesCount, - getPinnedEventCount, - isUntitled, - omitTypenameInTimeline, - dispatchUpdateTimeline, -} from './helpers'; -import { OpenTimelineResult, DispatchUpdateTimeline } from './types'; -import { KueryFilterQueryKind } from '../../store/model'; -import { Note } from '../../lib/note'; -import moment from 'moment'; -import sinon from 'sinon'; -import { TimelineType } from '../../../common/types/timeline'; - -jest.mock('../../store/inputs/actions'); -jest.mock('../../store/timeline/actions'); -jest.mock('../../store/app/actions'); -jest.mock('uuid', () => { - return { - v1: jest.fn(() => 'uuid.v1()'), - v4: jest.fn(() => 'uuid.v4()'), - }; -}); - -describe('helpers', () => { - let mockResults: OpenTimelineResult[]; - - beforeEach(() => { - mockResults = cloneDeep(mockTimelineResults); - }); - - describe('#getPinnedEventCount', () => { - test('returns 6 when the timeline has 6 pinned events', () => { - const with6Events = mockResults[0]; - - expect(getPinnedEventCount(with6Events)).toEqual(6); - }); - - test('returns zero when the timeline has an empty collection of pinned events', () => { - const withPinnedEvents = { ...mockResults[0], pinnedEventIds: {} }; - - expect(getPinnedEventCount(withPinnedEvents)).toEqual(0); - }); - - test('returns zero when pinnedEventIds is undefined', () => { - const withPinnedEvents = omit('pinnedEventIds', { ...mockResults[0] }); - - expect(getPinnedEventCount(withPinnedEvents)).toEqual(0); - }); - - test('returns zero when pinnedEventIds is null', () => { - const withPinnedEvents = omit('pinnedEventIds', { ...mockResults[0] }); - - expect(getPinnedEventCount(withPinnedEvents)).toEqual(0); - }); - }); - - describe('#getNotesCount', () => { - test('returns a total of 4 notes when the timeline has 4 notes (event1 [2] + event2 [1] + global [1])', () => { - const with4Notes = mockResults[0]; - - expect(getNotesCount(with4Notes)).toEqual(4); - }); - - test('returns 1 note (global [1]) when eventIdToNoteIds is undefined', () => { - const with1Note = omit('eventIdToNoteIds', { ...mockResults[0] }); - - expect(getNotesCount(with1Note)).toEqual(1); - }); - - test('returns 1 note (global [1]) when eventIdToNoteIds is null', () => { - const eventIdToNoteIdsIsNull = { - ...mockResults[0], - eventIdToNoteIds: null, - }; - expect(getNotesCount(eventIdToNoteIdsIsNull)).toEqual(1); - }); - - test('returns 1 note (global [1]) when eventIdToNoteIds is empty', () => { - const eventIdToNoteIdsIsEmpty = { - ...mockResults[0], - eventIdToNoteIds: {}, - }; - expect(getNotesCount(eventIdToNoteIdsIsEmpty)).toEqual(1); - }); - - test('returns 3 notes (event1 [2] + event2 [1]) when noteIds is undefined', () => { - const noteIdsIsUndefined = omit('noteIds', { ...mockResults[0] }); - - expect(getNotesCount(noteIdsIsUndefined)).toEqual(3); - }); - - test('returns 3 notes (event1 [2] + event2 [1]) when noteIds is null', () => { - const noteIdsIsNull = { - ...mockResults[0], - noteIds: null, - }; - - expect(getNotesCount(noteIdsIsNull)).toEqual(3); - }); - - test('returns 3 notes (event1 [2] + event2 [1]) when noteIds is empty', () => { - const noteIdsIsEmpty = { - ...mockResults[0], - noteIds: [], - }; - - expect(getNotesCount(noteIdsIsEmpty)).toEqual(3); - }); - - test('returns 0 when eventIdToNoteIds and noteIds are undefined', () => { - const eventIdToNoteIdsAndNoteIdsUndefined = omit(['eventIdToNoteIds', 'noteIds'], { - ...mockResults[0], - }); - - expect(getNotesCount(eventIdToNoteIdsAndNoteIdsUndefined)).toEqual(0); - }); - - test('returns 0 when eventIdToNoteIds and noteIds are null', () => { - const eventIdToNoteIdsAndNoteIdsNull = { - ...mockResults[0], - eventIdToNoteIds: null, - noteIds: null, - }; - - expect(getNotesCount(eventIdToNoteIdsAndNoteIdsNull)).toEqual(0); - }); - - test('returns 0 when eventIdToNoteIds and noteIds are empty', () => { - const eventIdToNoteIdsAndNoteIdsEmpty = { - ...mockResults[0], - eventIdToNoteIds: {}, - noteIds: [], - }; - - expect(getNotesCount(eventIdToNoteIdsAndNoteIdsEmpty)).toEqual(0); - }); - }); - - describe('#isUntitled', () => { - test('returns true when title is undefined', () => { - const titleIsUndefined = omit('title', { - ...mockResults[0], - }); - - expect(isUntitled(titleIsUndefined)).toEqual(true); - }); - - test('returns true when title is null', () => { - const titleIsNull = { - ...mockResults[0], - title: null, - }; - - expect(isUntitled(titleIsNull)).toEqual(true); - }); - - test('returns true when title is just whitespace', () => { - const titleIsWitespace = { - ...mockResults[0], - title: ' ', - }; - - expect(isUntitled(titleIsWitespace)).toEqual(true); - }); - - test('returns false when title is surrounded by whitespace', () => { - const titleIsWitespace = { - ...mockResults[0], - title: ' the king of the north ', - }; - - expect(isUntitled(titleIsWitespace)).toEqual(false); - }); - - test('returns false when title is NOT surrounded by whitespace', () => { - const titleIsWitespace = { - ...mockResults[0], - title: 'in the beginning...', - }; - - expect(isUntitled(titleIsWitespace)).toEqual(false); - }); - }); - - describe('#defaultTimelineToTimelineModel', () => { - test('if title is null, we should get the default title', () => { - const timeline = { - savedObjectId: 'savedObject-1', - title: null, - version: '1', - }; - - const newTimeline = defaultTimelineToTimelineModel(timeline, false); - expect(newTimeline).toEqual({ - columns: [ - { - columnHeaderType: 'not-filtered', - id: '@timestamp', - width: 190, - }, - { - columnHeaderType: 'not-filtered', - id: 'message', - width: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'event.category', - width: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'event.action', - width: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'host.name', - width: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'source.ip', - width: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'destination.ip', - width: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'user.name', - width: 180, - }, - ], - dataProviders: [], - dateRange: { - end: 0, - start: 0, - }, - description: '', - deletedEventIds: [], - eventIdToNoteIds: {}, - eventType: 'all', - filters: [], - highlightedDropAndProviderId: '', - historyIds: [], - id: 'savedObject-1', - isFavorite: false, - isLive: false, - isSelectAllChecked: false, - isLoading: false, - isSaving: false, - itemsPerPage: 25, - itemsPerPageOptions: [10, 25, 50, 100], - kqlMode: 'filter', - kqlQuery: { - filterQuery: null, - filterQueryDraft: null, - }, - loadingEventIds: [], - noteIds: [], - pinnedEventIds: {}, - pinnedEventsSaveObject: {}, - savedObjectId: 'savedObject-1', - selectedEventIds: {}, - show: false, - showCheckboxes: false, - showRowRenderers: true, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, - title: '', - timelineType: TimelineType.default, - templateTimelineId: null, - templateTimelineVersion: null, - version: '1', - width: 1100, - }); - }); - test('if columns are null, we should get the default columns', () => { - const timeline = { - savedObjectId: 'savedObject-1', - columns: null, - version: '1', - }; - - const newTimeline = defaultTimelineToTimelineModel(timeline, false); - expect(newTimeline).toEqual({ - columns: [ - { - columnHeaderType: 'not-filtered', - id: '@timestamp', - width: 190, - }, - { - columnHeaderType: 'not-filtered', - id: 'message', - width: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'event.category', - width: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'event.action', - width: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'host.name', - width: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'source.ip', - width: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'destination.ip', - width: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'user.name', - width: 180, - }, - ], - dataProviders: [], - dateRange: { - end: 0, - start: 0, - }, - description: '', - deletedEventIds: [], - eventIdToNoteIds: {}, - eventType: 'all', - filters: [], - highlightedDropAndProviderId: '', - historyIds: [], - id: 'savedObject-1', - isFavorite: false, - isLive: false, - isSelectAllChecked: false, - isLoading: false, - isSaving: false, - itemsPerPage: 25, - itemsPerPageOptions: [10, 25, 50, 100], - kqlMode: 'filter', - kqlQuery: { - filterQuery: null, - filterQueryDraft: null, - }, - loadingEventIds: [], - noteIds: [], - pinnedEventIds: {}, - pinnedEventsSaveObject: {}, - savedObjectId: 'savedObject-1', - selectedEventIds: {}, - show: false, - showCheckboxes: false, - showRowRenderers: true, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, - title: '', - timelineType: TimelineType.default, - templateTimelineId: null, - templateTimelineVersion: null, - version: '1', - width: 1100, - }); - }); - test('should merge columns when event.action is deleted without two extra column names of user.name', () => { - const timeline = { - savedObjectId: 'savedObject-1', - columns: timelineDefaults.columns.filter(column => column.id !== 'event.action'), - version: '1', - }; - - const newTimeline = defaultTimelineToTimelineModel(timeline, false); - expect(newTimeline).toEqual({ - savedObjectId: 'savedObject-1', - columns: [ - { - columnHeaderType: 'not-filtered', - id: '@timestamp', - width: 190, - }, - { - columnHeaderType: 'not-filtered', - id: 'message', - width: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'event.category', - width: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'host.name', - width: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'source.ip', - width: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'destination.ip', - width: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'user.name', - width: 180, - }, - ], - version: '1', - dataProviders: [], - description: '', - deletedEventIds: [], - eventIdToNoteIds: {}, - eventType: 'all', - filters: [], - highlightedDropAndProviderId: '', - historyIds: [], - isFavorite: false, - isLive: false, - isSelectAllChecked: false, - isLoading: false, - isSaving: false, - itemsPerPage: 25, - itemsPerPageOptions: [10, 25, 50, 100], - kqlMode: 'filter', - kqlQuery: { - filterQuery: null, - filterQueryDraft: null, - }, - loadingEventIds: [], - title: '', - timelineType: TimelineType.default, - templateTimelineId: null, - templateTimelineVersion: null, - noteIds: [], - pinnedEventIds: {}, - pinnedEventsSaveObject: {}, - dateRange: { - start: 0, - end: 0, - }, - selectedEventIds: {}, - show: false, - showCheckboxes: false, - showRowRenderers: true, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, - width: 1100, - id: 'savedObject-1', - }); - }); - - test('should merge filters object back with json object', () => { - const timeline = { - savedObjectId: 'savedObject-1', - columns: timelineDefaults.columns.filter(column => column.id !== 'event.action'), - filters: [ - { - meta: { - alias: null, - controlledBy: null, - disabled: false, - index: null, - key: 'event.category', - negate: false, - params: '{"query":"file"}', - type: 'phrase', - value: null, - }, - query: '{"match_phrase":{"event.category":"file"}}', - exists: null, - }, - { - meta: { - alias: null, - controlledBy: null, - disabled: false, - index: null, - key: '@timestamp', - negate: false, - params: null, - type: 'exists', - value: 'exists', - }, - query: null, - exists: '{"field":"@timestamp"}', - }, - ], - version: '1', - }; - - const newTimeline = defaultTimelineToTimelineModel(timeline, false); - expect(newTimeline).toEqual({ - savedObjectId: 'savedObject-1', - columns: [ - { - columnHeaderType: 'not-filtered', - id: '@timestamp', - width: 190, - }, - { - columnHeaderType: 'not-filtered', - id: 'message', - width: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'event.category', - width: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'host.name', - width: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'source.ip', - width: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'destination.ip', - width: 180, - }, - { - columnHeaderType: 'not-filtered', - id: 'user.name', - width: 180, - }, - ], - version: '1', - dataProviders: [], - description: '', - deletedEventIds: [], - eventIdToNoteIds: {}, - eventType: 'all', - filters: [ - { - $state: { - store: 'appState', - }, - meta: { - alias: null, - controlledBy: null, - disabled: false, - index: null, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - value: null, - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - { - $state: { - store: 'appState', - }, - exists: { - field: '@timestamp', - }, - meta: { - alias: null, - controlledBy: null, - disabled: false, - index: null, - key: '@timestamp', - negate: false, - params: null, - type: 'exists', - value: 'exists', - }, - }, - ], - highlightedDropAndProviderId: '', - historyIds: [], - isFavorite: false, - isLive: false, - isSelectAllChecked: false, - isLoading: false, - isSaving: false, - itemsPerPage: 25, - itemsPerPageOptions: [10, 25, 50, 100], - kqlMode: 'filter', - kqlQuery: { - filterQuery: null, - filterQueryDraft: null, - }, - loadingEventIds: [], - title: '', - timelineType: TimelineType.default, - templateTimelineId: null, - templateTimelineVersion: null, - noteIds: [], - pinnedEventIds: {}, - pinnedEventsSaveObject: {}, - dateRange: { - start: 0, - end: 0, - }, - selectedEventIds: {}, - show: false, - showCheckboxes: false, - showRowRenderers: true, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, - width: 1100, - id: 'savedObject-1', - }); - }); - }); - - describe('omitTypenameInTimeline', () => { - test('it does not modify the passed in timeline if no __typename exists', () => { - const result = omitTypenameInTimeline(mockTimelineResult); - - expect(result).toEqual(mockTimelineResult); - }); - - test('it returns timeline with __typename removed when it exists', () => { - const mockTimeline = { - ...mockTimelineResult, - __typename: 'something, something', - }; - const result = omitTypenameInTimeline(mockTimeline); - const expectedTimeline = { - ...mockTimeline, - __typename: undefined, - }; - - expect(result).toEqual(expectedTimeline); - }); - }); - - describe('dispatchUpdateTimeline', () => { - const dispatch = jest.fn() as Dispatch; - const anchor = '2020-03-27T20:34:51.337Z'; - const unix = moment(anchor).valueOf(); - let clock: sinon.SinonFakeTimers; - let timelineDispatch: DispatchUpdateTimeline; - - beforeEach(() => { - jest.clearAllMocks(); - - clock = sinon.useFakeTimers(unix); - timelineDispatch = dispatchUpdateTimeline(dispatch); - }); - - afterEach(function() { - clock.restore(); - }); - - test('it invokes date range picker dispatch', () => { - timelineDispatch({ - duplicate: true, - id: 'timeline-1', - from: 1585233356356, - to: 1585233716356, - notes: [], - timeline: mockTimelineModel, - })(); - - expect(dispatchSetTimelineRangeDatePicker).toHaveBeenCalledWith({ - from: 1585233356356, - to: 1585233716356, - }); - }); - - test('it invokes add timeline dispatch', () => { - timelineDispatch({ - duplicate: true, - id: 'timeline-1', - from: 1585233356356, - to: 1585233716356, - notes: [], - timeline: mockTimelineModel, - })(); - - expect(dispatchAddTimeline).toHaveBeenCalledWith({ - id: 'timeline-1', - timeline: mockTimelineModel, - }); - }); - - test('it does not invoke kql filter query dispatches if timeline.kqlQuery.filterQuery is null', () => { - timelineDispatch({ - duplicate: true, - id: 'timeline-1', - from: 1585233356356, - to: 1585233716356, - notes: [], - timeline: mockTimelineModel, - })(); - - expect(dispatchSetKqlFilterQueryDraft).not.toHaveBeenCalled(); - expect(dispatchApplyKqlFilterQuery).not.toHaveBeenCalled(); - }); - - test('it does not invoke notes dispatch if duplicate is true', () => { - timelineDispatch({ - duplicate: true, - id: 'timeline-1', - from: 1585233356356, - to: 1585233716356, - notes: [], - timeline: mockTimelineModel, - })(); - - expect(dispatchAddNotes).not.toHaveBeenCalled(); - }); - - test('it does not invoke kql filter query dispatches if timeline.kqlQuery.kuery is null', () => { - const mockTimeline = { - ...mockTimelineModel, - kqlQuery: { - filterQuery: { - kuery: null, - serializedQuery: 'some-serialized-query', - }, - filterQueryDraft: null, - }, - }; - timelineDispatch({ - duplicate: true, - id: 'timeline-1', - from: 1585233356356, - to: 1585233716356, - notes: [], - timeline: mockTimeline, - })(); - - expect(dispatchSetKqlFilterQueryDraft).not.toHaveBeenCalled(); - expect(dispatchApplyKqlFilterQuery).not.toHaveBeenCalled(); - }); - - test('it invokes kql filter query dispatches if timeline.kqlQuery.filterQuery.kuery is not null', () => { - const mockTimeline = { - ...mockTimelineModel, - kqlQuery: { - filterQuery: { - kuery: { expression: 'expression', kind: 'kuery' as KueryFilterQueryKind }, - serializedQuery: 'some-serialized-query', - }, - filterQueryDraft: null, - }, - }; - timelineDispatch({ - duplicate: true, - id: 'timeline-1', - from: 1585233356356, - to: 1585233716356, - notes: [], - timeline: mockTimeline, - })(); - - expect(dispatchSetKqlFilterQueryDraft).toHaveBeenCalledWith({ - id: 'timeline-1', - filterQueryDraft: { - kind: 'kuery', - expression: 'expression', - }, - }); - expect(dispatchApplyKqlFilterQuery).toHaveBeenCalledWith({ - id: 'timeline-1', - filterQuery: { - kuery: { - kind: 'kuery', - expression: 'expression', - }, - serializedQuery: 'some-serialized-query', - }, - }); - }); - - test('it invokes dispatchAddNotes if duplicate is false', () => { - timelineDispatch({ - duplicate: false, - id: 'timeline-1', - from: 1585233356356, - to: 1585233716356, - notes: [ - { - created: 1585233356356, - updated: 1585233356356, - noteId: 'note-id', - note: 'I am a note', - }, - ], - timeline: mockTimelineModel, - })(); - - expect(dispatchAddGlobalTimelineNote).not.toHaveBeenCalled(); - expect(dispatchUpdateNote).not.toHaveBeenCalled(); - expect(dispatchAddNotes).toHaveBeenCalledWith({ - notes: [ - { - created: new Date('2020-03-26T14:35:56.356Z'), - id: 'note-id', - lastEdit: new Date('2020-03-26T14:35:56.356Z'), - note: 'I am a note', - user: 'unknown', - saveObjectId: 'note-id', - version: undefined, - }, - ], - }); - }); - - test('it invokes dispatch to create a timeline note if duplicate is true and ruleNote exists', () => { - timelineDispatch({ - duplicate: true, - id: 'timeline-1', - from: 1585233356356, - to: 1585233716356, - notes: [], - timeline: mockTimelineModel, - ruleNote: '# this would be some markdown', - })(); - const expectedNote: Note = { - created: new Date(anchor), - id: 'uuid.v4()', - lastEdit: null, - note: '# this would be some markdown', - saveObjectId: null, - user: 'elastic', - version: null, - }; - - expect(dispatchAddNotes).not.toHaveBeenCalled(); - expect(dispatchUpdateNote).toHaveBeenCalledWith({ note: expectedNote }); - expect(dispatchAddGlobalTimelineNote).toHaveBeenLastCalledWith({ - id: 'timeline-1', - noteId: 'uuid.v4()', - }); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/open_timeline/helpers.ts b/x-pack/plugins/siem/public/components/open_timeline/helpers.ts deleted file mode 100644 index 681d39feb09f81..00000000000000 --- a/x-pack/plugins/siem/public/components/open_timeline/helpers.ts +++ /dev/null @@ -1,319 +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 ApolloClient from 'apollo-client'; -import { getOr, set, isEmpty } from 'lodash/fp'; -import { Action } from 'typescript-fsa'; -import uuid from 'uuid'; -import { Dispatch } from 'redux'; - -import { oneTimelineQuery } from '../../containers/timeline/one/index.gql_query'; -import { TimelineResult, GetOneTimeline, NoteResult } from '../../graphql/types'; -import { - addNotes as dispatchAddNotes, - updateNote as dispatchUpdateNote, -} from '../../store/app/actions'; -import { setTimelineRangeDatePicker as dispatchSetTimelineRangeDatePicker } from '../../store/inputs/actions'; -import { - setKqlFilterQueryDraft as dispatchSetKqlFilterQueryDraft, - applyKqlFilterQuery as dispatchApplyKqlFilterQuery, - addTimeline as dispatchAddTimeline, - addNote as dispatchAddGlobalTimelineNote, -} from '../../store/timeline/actions'; - -import { ColumnHeaderOptions, TimelineModel } from '../../store/timeline/model'; -import { timelineDefaults } from '../../store/timeline/defaults'; -import { - defaultColumnHeaderType, - defaultHeaders, -} from '../timeline/body/column_headers/default_headers'; -import { - DEFAULT_DATE_COLUMN_MIN_WIDTH, - DEFAULT_COLUMN_MIN_WIDTH, -} from '../timeline/body/constants'; - -import { OpenTimelineResult, UpdateTimeline, DispatchUpdateTimeline } from './types'; -import { getTimeRangeSettings } from '../../utils/default_date_settings'; -import { createNote } from '../notes/helpers'; - -export const OPEN_TIMELINE_CLASS_NAME = 'open-timeline'; - -/** Returns a count of the pinned events in a timeline */ -export const getPinnedEventCount = ({ pinnedEventIds }: OpenTimelineResult): number => - pinnedEventIds != null ? Object.keys(pinnedEventIds).length : 0; - -/** Returns the sum of all notes added to pinned events and notes applicable to the timeline */ -export const getNotesCount = ({ eventIdToNoteIds, noteIds }: OpenTimelineResult): number => { - const eventNoteCount = - eventIdToNoteIds != null - ? Object.keys(eventIdToNoteIds).reduce( - (count, eventId) => count + eventIdToNoteIds[eventId].length, - 0 - ) - : 0; - - const globalNoteCount = noteIds != null ? noteIds.length : 0; - - return eventNoteCount + globalNoteCount; -}; - -/** Returns true if the timeline is untitlied */ -export const isUntitled = ({ title }: OpenTimelineResult): boolean => - title == null || title.trim().length === 0; - -const omitTypename = (key: string, value: keyof TimelineModel) => - key === '__typename' ? undefined : value; - -export const omitTypenameInTimeline = (timeline: TimelineResult): TimelineResult => - JSON.parse(JSON.stringify(timeline), omitTypename); - -const parseString = (params: string) => { - try { - return JSON.parse(params); - } catch { - return params; - } -}; - -export const defaultTimelineToTimelineModel = ( - timeline: TimelineResult, - duplicate: boolean -): TimelineModel => { - return Object.entries({ - ...timeline, - columns: - timeline.columns != null - ? timeline.columns.map(col => { - const timelineCols: ColumnHeaderOptions = { - ...col, - columnHeaderType: defaultColumnHeaderType, - id: col.id != null ? col.id : 'unknown', - placeholder: col.placeholder != null ? col.placeholder : undefined, - category: col.category != null ? col.category : undefined, - description: col.description != null ? col.description : undefined, - example: col.example != null ? col.example : undefined, - type: col.type != null ? col.type : undefined, - aggregatable: col.aggregatable != null ? col.aggregatable : undefined, - width: - col.id === '@timestamp' ? DEFAULT_DATE_COLUMN_MIN_WIDTH : DEFAULT_COLUMN_MIN_WIDTH, - }; - return timelineCols; - }) - : defaultHeaders, - eventIdToNoteIds: duplicate - ? {} - : timeline.eventIdToNoteIds != null - ? timeline.eventIdToNoteIds.reduce((acc, note) => { - if (note.eventId != null) { - const eventNotes = getOr([], note.eventId, acc); - return { ...acc, [note.eventId]: [...eventNotes, note.noteId] }; - } - return acc; - }, {}) - : {}, - filters: - timeline.filters != null - ? timeline.filters.map(filter => ({ - $state: { - store: 'appState', - }, - meta: { - ...filter.meta, - ...(filter.meta && filter.meta.field != null - ? { params: parseString(filter.meta.field) } - : {}), - ...(filter.meta && filter.meta.params != null - ? { params: parseString(filter.meta.params) } - : {}), - ...(filter.meta && filter.meta.value != null - ? { value: parseString(filter.meta.value) } - : {}), - }, - ...(filter.exists != null ? { exists: parseString(filter.exists) } : {}), - ...(filter.match_all != null ? { exists: parseString(filter.match_all) } : {}), - ...(filter.missing != null ? { exists: parseString(filter.missing) } : {}), - ...(filter.query != null ? { query: parseString(filter.query) } : {}), - ...(filter.range != null ? { range: parseString(filter.range) } : {}), - ...(filter.script != null ? { exists: parseString(filter.script) } : {}), - })) - : [], - isFavorite: duplicate - ? false - : timeline.favorite != null - ? timeline.favorite.length > 0 - : false, - noteIds: duplicate ? [] : timeline.noteIds != null ? timeline.noteIds : [], - pinnedEventIds: duplicate - ? {} - : timeline.pinnedEventIds != null - ? timeline.pinnedEventIds.reduce( - (acc, pinnedEventId) => ({ ...acc, [pinnedEventId]: true }), - {} - ) - : {}, - pinnedEventsSaveObject: duplicate - ? {} - : timeline.pinnedEventsSaveObject != null - ? timeline.pinnedEventsSaveObject.reduce( - (acc, pinnedEvent) => ({ - ...acc, - ...(pinnedEvent.eventId != null ? { [pinnedEvent.eventId]: pinnedEvent } : {}), - }), - {} - ) - : {}, - id: duplicate ? '' : timeline.savedObjectId, - savedObjectId: duplicate ? null : timeline.savedObjectId, - version: duplicate ? null : timeline.version, - title: duplicate ? '' : timeline.title || '', - templateTimelineId: duplicate ? null : timeline.templateTimelineId, - templateTimelineVersion: duplicate ? null : timeline.templateTimelineVersion, - }).reduce((acc: TimelineModel, [key, value]) => (value != null ? set(key, value, acc) : acc), { - ...timelineDefaults, - id: '', - }); -}; - -export const formatTimelineResultToModel = ( - timelineToOpen: TimelineResult, - duplicate: boolean = false -): { notes: NoteResult[] | null | undefined; timeline: TimelineModel } => { - const { notes, ...timelineModel } = timelineToOpen; - return { - notes, - timeline: defaultTimelineToTimelineModel(timelineModel, duplicate), - }; -}; - -export interface QueryTimelineById { - apolloClient: ApolloClient | ApolloClient<{}> | undefined; - duplicate: boolean; - timelineId: string; - onOpenTimeline?: (timeline: TimelineModel) => void; - openTimeline?: boolean; - updateIsLoading: ({ - id, - isLoading, - }: { - id: string; - isLoading: boolean; - }) => Action<{ id: string; isLoading: boolean }>; - updateTimeline: DispatchUpdateTimeline; -} - -export const queryTimelineById = ({ - apolloClient, - duplicate = false, - timelineId, - onOpenTimeline, - openTimeline = true, - updateIsLoading, - updateTimeline, -}: QueryTimelineById) => { - updateIsLoading({ id: 'timeline-1', isLoading: true }); - if (apolloClient) { - apolloClient - .query({ - query: oneTimelineQuery, - fetchPolicy: 'no-cache', - variables: { id: timelineId }, - }) - // eslint-disable-next-line - .then(result => { - const timelineToOpen: TimelineResult = omitTypenameInTimeline( - getOr({}, 'data.getOneTimeline', result) - ); - - const { timeline, notes } = formatTimelineResultToModel(timelineToOpen, duplicate); - if (onOpenTimeline != null) { - onOpenTimeline(timeline); - } else if (updateTimeline) { - const { from, to } = getTimeRangeSettings(); - updateTimeline({ - duplicate, - from: getOr(from, 'dateRange.start', timeline), - id: 'timeline-1', - notes, - timeline: { - ...timeline, - show: openTimeline, - }, - to: getOr(to, 'dateRange.end', timeline), - })(); - } - }) - .finally(() => { - updateIsLoading({ id: 'timeline-1', isLoading: false }); - }); - } -}; - -export const dispatchUpdateTimeline = (dispatch: Dispatch): DispatchUpdateTimeline => ({ - duplicate, - id, - from, - notes, - timeline, - to, - ruleNote, -}: UpdateTimeline): (() => void) => () => { - dispatch(dispatchSetTimelineRangeDatePicker({ from, to })); - dispatch(dispatchAddTimeline({ id, timeline })); - if ( - timeline.kqlQuery != null && - timeline.kqlQuery.filterQuery != null && - timeline.kqlQuery.filterQuery.kuery != null && - timeline.kqlQuery.filterQuery.kuery.expression !== '' - ) { - dispatch( - dispatchSetKqlFilterQueryDraft({ - id, - filterQueryDraft: { - kind: 'kuery', - expression: timeline.kqlQuery.filterQuery.kuery.expression || '', - }, - }) - ); - dispatch( - dispatchApplyKqlFilterQuery({ - id, - filterQuery: { - kuery: { - kind: 'kuery', - expression: timeline.kqlQuery.filterQuery.kuery.expression || '', - }, - serializedQuery: timeline.kqlQuery.filterQuery.serializedQuery || '', - }, - }) - ); - } - - if (duplicate && ruleNote != null && !isEmpty(ruleNote)) { - const getNewNoteId = (): string => uuid.v4(); - const newNote = createNote({ newNote: ruleNote, getNewNoteId }); - dispatch(dispatchUpdateNote({ note: newNote })); - dispatch(dispatchAddGlobalTimelineNote({ noteId: newNote.id, id })); - } - - if (!duplicate) { - dispatch( - dispatchAddNotes({ - notes: - notes != null - ? notes.map((note: NoteResult) => ({ - created: note.created != null ? new Date(note.created) : new Date(), - id: note.noteId, - lastEdit: note.updated != null ? new Date(note.updated) : new Date(), - note: note.note || '', - user: note.updatedBy || 'unknown', - saveObjectId: note.noteId, - version: note.version, - })) - : [], - }) - ); - } -}; diff --git a/x-pack/plugins/siem/public/components/open_timeline/index.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/index.test.tsx deleted file mode 100644 index 731c6d1ca9806a..00000000000000 --- a/x-pack/plugins/siem/public/components/open_timeline/index.test.tsx +++ /dev/null @@ -1,658 +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 euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import { mount } from 'enzyme'; -import { MockedProvider } from 'react-apollo/test-utils'; -import React from 'react'; -import { ThemeProvider } from 'styled-components'; - -import { wait } from '../../lib/helpers'; -import { TestProviderWithoutDragAndDrop, apolloClient } from '../../mock/test_providers'; -import { mockOpenTimelineQueryResults } from '../../mock/timeline_results'; -import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../pages/timelines/timelines_page'; - -import { NotePreviews } from './note_previews'; -import { OPEN_TIMELINE_CLASS_NAME } from './helpers'; -import { TimelineTabsStyle } from './types'; - -import { StatefulOpenTimeline } from '.'; -import { useGetAllTimeline, getAllTimeline } from '../../containers/timeline/all'; -jest.mock('../../lib/kibana'); -jest.mock('../../containers/timeline/all', () => { - const originalModule = jest.requireActual('../../containers/timeline/all'); - return { - ...originalModule, - useGetAllTimeline: jest.fn(), - getAllTimeline: originalModule.getAllTimeline, - }; -}); -jest.mock('./use_timeline_types', () => { - return { - useTimelineTypes: jest.fn().mockReturnValue({ - timelineType: 'default', - timelineTabs:
, - timelineFilters:
, - }), - }; -}); - -describe('StatefulOpenTimeline', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); - const title = 'All Timelines / Open Timelines'; - beforeEach(() => { - ((useGetAllTimeline as unknown) as jest.Mock).mockReturnValue({ - fetchAllTimeline: jest.fn(), - timelines: getAllTimeline( - '', - mockOpenTimelineQueryResults[0].result.data?.getAllTimeline?.timeline ?? [] - ), - loading: false, - totalCount: mockOpenTimelineQueryResults[0].result.data.getAllTimeline.totalCount, - refetch: jest.fn(), - }); - }); - - test('it has the expected initial state', () => { - const wrapper = mount( - - - - - - - - ); - - const componentProps = wrapper - .find('[data-test-subj="open-timeline"]') - .last() - .props(); - - expect(componentProps).toEqual({ - ...componentProps, - itemIdToExpandedNotesRowMap: {}, - onlyFavorites: false, - pageIndex: 0, - pageSize: 10, - query: '', - selectedItems: [], - sortDirection: 'desc', - sortField: 'updated', - }); - }); - - describe('#onQueryChange', () => { - test('it updates the query state with the expected trimmed value when the user enters a query', () => { - const wrapper = mount( - - - - - - - - ); - wrapper - .find('[data-test-subj="search-bar"] input') - .simulate('keyup', { keyCode: 13, target: { value: ' abcd ' } }); - expect( - wrapper - .find('[data-test-subj="search-row"]') - .first() - .prop('query') - ).toEqual('abcd'); - }); - - test('it appends the word "with" to the Showing in Timelines message when the user enters a query', async () => { - const wrapper = mount( - - - - - - - - ); - - await wait(); - - wrapper - .find('[data-test-subj="search-bar"] input') - .simulate('keyup', { keyCode: 13, target: { value: ' abcd ' } }); - - expect( - wrapper - .find('[data-test-subj="query-message"]') - .first() - .text() - ).toContain('Showing: 11 timelines with'); - }); - - test('echos (renders) the query when the user enters a query', async () => { - const wrapper = mount( - - - - - - - - ); - - await wait(); - - wrapper - .find('[data-test-subj="search-bar"] input') - .simulate('keyup', { keyCode: 13, target: { value: ' abcd ' } }); - - expect( - wrapper - .find('[data-test-subj="selectable-query-text"]') - .first() - .text() - ).toEqual('with "abcd"'); - }); - }); - - describe('#focusInput', () => { - test('focuses the input when the component mounts', async () => { - const wrapper = mount( - - - - - - - - ); - - await wait(); - - expect( - wrapper - .find(`.${OPEN_TIMELINE_CLASS_NAME} input`) - .first() - .getDOMNode().id === document.activeElement!.id - ).toBe(true); - }); - }); - - describe('#onAddTimelinesToFavorites', () => { - // This functionality is hiding for now and waiting to see the light in the near future - test.skip('it invokes addTimelinesToFavorites with the selected timelines when the button is clicked', async () => { - const addTimelinesToFavorites = jest.fn(); - - const wrapper = mount( - - - - - - - - ); - - await wait(); - - wrapper - .find('.euiCheckbox__input') - .first() - .simulate('change', { target: { checked: true } }); - - wrapper - .find('[data-test-subj="favorite-selected"]') - .first() - .simulate('click'); - - expect(addTimelinesToFavorites).toHaveBeenCalledWith([ - 'saved-timeline-11', - 'saved-timeline-10', - 'saved-timeline-9', - 'saved-timeline-8', - 'saved-timeline-6', - 'saved-timeline-5', - 'saved-timeline-4', - 'saved-timeline-3', - 'saved-timeline-2', - ]); - }); - }); - - describe('#onDeleteSelected', () => { - // TODO - Have been skip because we need to re-implement the test as the component changed - test.skip('it invokes deleteTimelines with the selected timelines when the button is clicked', async () => { - const deleteTimelines = jest.fn(); - - const wrapper = mount( - - - - - - - - ); - - await wait(); - - wrapper - .find('.euiCheckbox__input') - .first() - .simulate('change', { target: { checked: true } }); - - wrapper - .find('[data-test-subj="delete-selected"]') - .first() - .simulate('click'); - - expect(deleteTimelines).toHaveBeenCalledWith([ - 'saved-timeline-11', - 'saved-timeline-10', - 'saved-timeline-9', - 'saved-timeline-8', - 'saved-timeline-6', - 'saved-timeline-5', - 'saved-timeline-4', - 'saved-timeline-3', - 'saved-timeline-2', - ]); - }); - }); - - describe('#onSelectionChange', () => { - test('it updates the selection state when timelines are selected', async () => { - const wrapper = mount( - - - - - - - - ); - - await wait(); - - wrapper - .find('.euiCheckbox__input') - .first() - .simulate('change', { target: { checked: true } }); - - const selectedItems: [] = wrapper - .find('[data-test-subj="open-timeline"]') - .last() - .prop('selectedItems'); - - expect(selectedItems.length).toEqual(13); // 13 because we did mock 13 timelines in the query - }); - }); - - describe('#onTableChange', () => { - test('it updates the sort state when the user clicks on a column to sort it', () => { - const wrapper = mount( - - - - - - - - ); - - expect( - wrapper - .find('[data-test-subj="open-timeline"]') - .last() - .prop('sortDirection') - ).toEqual('desc'); - - wrapper - .find('thead tr th button') - .at(0) - .simulate('click'); - - expect( - wrapper - .find('[data-test-subj="open-timeline"]') - .last() - .prop('sortDirection') - ).toEqual('asc'); - }); - }); - - describe('#onToggleOnlyFavorites', () => { - test('it updates the onlyFavorites state when the user clicks the Only Favorites button', () => { - const wrapper = mount( - - - - - - - - ); - - expect( - wrapper - .find('[data-test-subj="open-timeline"]') - .last() - .prop('onlyFavorites') - ).toEqual(false); - - wrapper - .find('[data-test-subj="only-favorites-toggle"]') - .first() - .simulate('click'); - - expect( - wrapper - .find('[data-test-subj="open-timeline"]') - .last() - .prop('onlyFavorites') - ).toEqual(true); - }); - }); - - describe('#onToggleShowNotes', () => { - test('it updates the itemIdToExpandedNotesRowMap state when the user clicks the expand notes button', async () => { - const wrapper = mount( - - - - - - - - ); - - await wait(); - wrapper.update(); - - expect( - wrapper - .find('[data-test-subj="open-timeline"]') - .last() - .prop('itemIdToExpandedNotesRowMap') - ).toEqual({}); - - wrapper - .find('[data-test-subj="expand-notes"]') - .first() - .simulate('click'); - - expect( - wrapper - .find('[data-test-subj="open-timeline"]') - .last() - .prop('itemIdToExpandedNotesRowMap') - ).toEqual({ - '10849df0-7b44-11e9-a608-ab3d811609': ( - ({ ...note, savedObjectId: note.noteId }) - ) - : [] - } - /> - ), - }); - }); - - test('it renders the expanded notes when the expand button is clicked', async () => { - const wrapper = mount( - - - - - - - - ); - - await wait(); - - wrapper.update(); - - wrapper - .find('[data-test-subj="expand-notes"]') - .first() - .simulate('click'); - expect(wrapper.find('[data-test-subj="note-previews-container"]').exists()).toEqual(true); - expect(wrapper.find('[data-test-subj="updated-by"]').exists()).toEqual(true); - - expect( - wrapper - .find('[data-test-subj="note-previews-container"]') - .find('[data-test-subj="updated-by"]') - .first() - .text() - ).toEqual('elastic'); - }); - - test('it renders the title', async () => { - const wrapper = mount( - - - - - - - - ); - - await wait(); - - expect(wrapper.find(`[data-test-subj="timeline-${TimelineTabsStyle.tab}"]`).exists()).toEqual( - true - ); - }); - }); - - describe('#resetSelectionState', () => { - test('when the user deletes selected timelines, resetSelectionState is invoked to clear the selection state', async () => { - const wrapper = mount( - - - - - - - - ); - const getSelectedItem = (): [] => - wrapper - .find('[data-test-subj="open-timeline"]') - .last() - .prop('selectedItems'); - await wait(); - expect(getSelectedItem().length).toEqual(0); - wrapper - .find('.euiCheckbox__input') - .first() - .simulate('change', { target: { checked: true } }); - expect(getSelectedItem().length).toEqual(13); - }); - }); - - test('it renders the expected count of matching timelines when no query has been entered', async () => { - const wrapper = mount( - - - - - - - - ); - - await wait(); - - wrapper.update(); - - expect( - wrapper - .find('[data-test-subj="query-message"]') - .first() - .text() - ).toContain('Showing: 11 timelines '); - }); - - // TODO - Have been skip because we need to re-implement the test as the component changed - test.skip('it invokes onOpenTimeline with the expected parameters when the hyperlink is clicked', async () => { - const onOpenTimeline = jest.fn(); - - const wrapper = mount( - - - - - - - - ); - - await wait(); - - wrapper - .find( - `[data-test-subj="title-${ - mockOpenTimelineQueryResults[0].result.data!.getAllTimeline.timeline[0].savedObjectId - }"]` - ) - .first() - .simulate('click'); - - expect(onOpenTimeline).toHaveBeenCalledWith({ - duplicate: false, - timelineId: mockOpenTimelineQueryResults[0].result.data!.getAllTimeline.timeline[0] - .savedObjectId, - }); - }); - - // TODO - Have been skip because we need to re-implement the test as the component changed - test.skip('it invokes onOpenTimeline with the expected params when the button is clicked', async () => { - const onOpenTimeline = jest.fn(); - - const wrapper = mount( - - - - - - - - ); - - await wait(); - - wrapper - .find('[data-test-subj="open-duplicate"]') - .first() - .simulate('click'); - - expect(onOpenTimeline).toBeCalledWith({ duplicate: true, timelineId: 'saved-timeline-11' }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/open_timeline/index.tsx b/x-pack/plugins/siem/public/components/open_timeline/index.tsx deleted file mode 100644 index ed22673f07a780..00000000000000 --- a/x-pack/plugins/siem/public/components/open_timeline/index.tsx +++ /dev/null @@ -1,343 +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 ApolloClient from 'apollo-client'; -import React, { useEffect, useState, useCallback } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; - -import { Dispatch } from 'redux'; - -import { defaultHeaders } from '../../components/timeline/body/column_headers/default_headers'; -import { deleteTimelineMutation } from '../../containers/timeline/delete/persist.gql_query'; -import { useGetAllTimeline } from '../../containers/timeline/all'; -import { DeleteTimelineMutation, SortFieldTimeline, Direction } from '../../graphql/types'; -import { State, timelineSelectors } from '../../store'; -import { ColumnHeaderOptions, TimelineModel } from '../../store/timeline/model'; -import { timelineDefaults } from '../../store/timeline/defaults'; -import { - createTimeline as dispatchCreateNewTimeline, - updateIsLoading as dispatchUpdateIsLoading, -} from '../../store/timeline/actions'; -import { OpenTimeline } from './open_timeline'; -import { OPEN_TIMELINE_CLASS_NAME, queryTimelineById, dispatchUpdateTimeline } from './helpers'; -import { OpenTimelineModalBody } from './open_timeline_modal/open_timeline_modal_body'; -import { - ActionTimelineToShow, - DeleteTimelines, - EuiSearchBarQuery, - OnDeleteSelected, - OnOpenTimeline, - OnQueryChange, - OnSelectionChange, - OnTableChange, - OnTableChangeParams, - OpenTimelineProps, - OnToggleOnlyFavorites, - OpenTimelineResult, - OnToggleShowNotes, - OnDeleteOneTimeline, -} from './types'; -import { DEFAULT_SORT_FIELD, DEFAULT_SORT_DIRECTION } from './constants'; -import { useTimelineTypes } from './use_timeline_types'; - -interface OwnProps { - apolloClient: ApolloClient; - /** Displays open timeline in modal */ - isModal: boolean; - closeModalTimeline?: () => void; - hideActions?: ActionTimelineToShow[]; - onOpenTimeline?: (timeline: TimelineModel) => void; -} - -export type OpenTimelineOwnProps = OwnProps & - Pick< - OpenTimelineProps, - 'defaultPageSize' | 'title' | 'importDataModalToggle' | 'setImportDataModalToggle' - > & - PropsFromRedux; - -/** Returns a collection of selected timeline ids */ -export const getSelectedTimelineIds = (selectedItems: OpenTimelineResult[]): string[] => - selectedItems.reduce( - (validSelections, timelineResult) => - timelineResult.savedObjectId != null - ? [...validSelections, timelineResult.savedObjectId] - : validSelections, - [] - ); - -/** Manages the state (e.g table selection) of the (pure) `OpenTimeline` component */ -export const StatefulOpenTimelineComponent = React.memo( - ({ - apolloClient, - closeModalTimeline, - createNewTimeline, - defaultPageSize, - hideActions = [], - isModal = false, - importDataModalToggle, - onOpenTimeline, - setImportDataModalToggle, - timeline, - title, - updateTimeline, - updateIsLoading, - }) => { - /** Required by EuiTable for expandable rows: a map of `TimelineResult.savedObjectId` to rendered notes */ - const [itemIdToExpandedNotesRowMap, setItemIdToExpandedNotesRowMap] = useState< - Record - >({}); - /** Only query for favorite timelines when true */ - const [onlyFavorites, setOnlyFavorites] = useState(false); - /** The requested page of results */ - const [pageIndex, setPageIndex] = useState(0); - /** The requested size of each page of search results */ - const [pageSize, setPageSize] = useState(defaultPageSize); - /** The current search criteria */ - const [search, setSearch] = useState(''); - /** The currently-selected timelines in the table */ - const [selectedItems, setSelectedItems] = useState([]); - /** The requested sort direction of the query results */ - const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>(DEFAULT_SORT_DIRECTION); - /** The requested field to sort on */ - const [sortField, setSortField] = useState(DEFAULT_SORT_FIELD); - - const { timelineType, timelineTabs, timelineFilters } = useTimelineTypes(); - const { fetchAllTimeline, timelines, loading, totalCount } = useGetAllTimeline(); - - const refetch = useCallback(() => { - fetchAllTimeline({ - pageInfo: { - pageIndex: pageIndex + 1, - pageSize, - }, - search, - sort: { sortField: sortField as SortFieldTimeline, sortOrder: sortDirection as Direction }, - onlyUserFavorite: onlyFavorites, - timelineType, - }); - }, [pageIndex, pageSize, search, sortField, sortDirection, timelineType, onlyFavorites]); - - /** Invoked when the user presses enters to submit the text in the search input */ - const onQueryChange: OnQueryChange = useCallback((query: EuiSearchBarQuery) => { - setSearch(query.queryText.trim()); - }, []); - - /** Focuses the input that filters the field browser */ - const focusInput = () => { - const elements = document.querySelector(`.${OPEN_TIMELINE_CLASS_NAME} input`); - - if (elements != null) { - elements.focus(); - } - }; - - /* This feature will be implemented in the near future, so we are keeping it to know what to do */ - - /** Invoked when the user clicks the action to add the selected timelines to favorites */ - // const onAddTimelinesToFavorites: OnAddTimelinesToFavorites = () => { - // const { addTimelinesToFavorites } = this.props; - // const { selectedItems } = this.state; - // if (addTimelinesToFavorites != null) { - // addTimelinesToFavorites(getSelectedTimelineIds(selectedItems)); - // TODO: it's not possible to clear the selection state of the newly-favorited - // items, because we can't pass the selection state as props to the table. - // See: https://github.com/elastic/eui/issues/1077 - // TODO: the query must re-execute to show the results of the mutation - // } - // }; - - const deleteTimelines: DeleteTimelines = useCallback( - async (timelineIds: string[]) => { - if (timelineIds.includes(timeline.savedObjectId || '')) { - createNewTimeline({ id: 'timeline-1', columns: defaultHeaders, show: false }); - } - - await apolloClient.mutate< - DeleteTimelineMutation.Mutation, - DeleteTimelineMutation.Variables - >({ - mutation: deleteTimelineMutation, - fetchPolicy: 'no-cache', - variables: { id: timelineIds }, - }); - refetch(); - }, - [apolloClient, createNewTimeline, refetch, timeline] - ); - - const onDeleteOneTimeline: OnDeleteOneTimeline = useCallback( - async (timelineIds: string[]) => { - await deleteTimelines(timelineIds); - }, - [deleteTimelines] - ); - - /** Invoked when the user clicks the action to delete the selected timelines */ - const onDeleteSelected: OnDeleteSelected = useCallback(async () => { - await deleteTimelines(getSelectedTimelineIds(selectedItems)); - - // NOTE: we clear the selection state below, but if the server fails to - // delete a timeline, it will remain selected in the table: - resetSelectionState(); - - // TODO: the query must re-execute to show the results of the deletion - }, [selectedItems, deleteTimelines]); - - /** Invoked when the user selects (or de-selects) timelines */ - const onSelectionChange: OnSelectionChange = useCallback( - (newSelectedItems: OpenTimelineResult[]) => { - setSelectedItems(newSelectedItems); // <-- this is NOT passed down as props to the table: https://github.com/elastic/eui/issues/1077 - }, - [] - ); - - /** Invoked by the EUI table implementation when the user interacts with the table (i.e. to update sorting) */ - const onTableChange: OnTableChange = useCallback(({ page, sort }: OnTableChangeParams) => { - const { index, size } = page; - const { field, direction } = sort; - setPageIndex(index); - setPageSize(size); - setSortDirection(direction); - setSortField(field); - }, []); - - /** Invoked when the user toggles the option to only view favorite timelines */ - const onToggleOnlyFavorites: OnToggleOnlyFavorites = useCallback(() => { - setOnlyFavorites(!onlyFavorites); - }, [onlyFavorites]); - - /** Invoked when the user toggles the expansion or collapse of inline notes in a table row */ - const onToggleShowNotes: OnToggleShowNotes = useCallback( - (newItemIdToExpandedNotesRowMap: Record) => { - setItemIdToExpandedNotesRowMap(newItemIdToExpandedNotesRowMap); - }, - [] - ); - - /** Resets the selection state such that all timelines are unselected */ - const resetSelectionState = useCallback(() => { - setSelectedItems([]); - }, []); - - const openTimeline: OnOpenTimeline = useCallback( - ({ duplicate, timelineId }: { duplicate: boolean; timelineId: string }) => { - if (isModal && closeModalTimeline != null) { - closeModalTimeline(); - } - - queryTimelineById({ - apolloClient, - duplicate, - onOpenTimeline, - timelineId, - updateIsLoading, - updateTimeline, - }); - }, - [apolloClient, updateIsLoading, updateTimeline] - ); - - useEffect(() => { - focusInput(); - }, []); - - useEffect(() => { - refetch(); - }, [refetch]); - - return !isModal ? ( - - ) : ( - - ); - } -); - -const makeMapStateToProps = () => { - const getTimeline = timelineSelectors.getTimelineByIdSelector(); - const mapStateToProps = (state: State) => { - const timeline = getTimeline(state, 'timeline-1') ?? timelineDefaults; - return { - timeline, - }; - }; - return mapStateToProps; -}; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - createNewTimeline: ({ - id, - columns, - show, - }: { - id: string; - columns: ColumnHeaderOptions[]; - show?: boolean; - }) => dispatch(dispatchCreateNewTimeline({ id, columns, show })), - updateIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => - dispatch(dispatchUpdateIsLoading({ id, isLoading })), - updateTimeline: dispatchUpdateTimeline(dispatch), -}); - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const StatefulOpenTimeline = connector(StatefulOpenTimelineComponent); diff --git a/x-pack/plugins/siem/public/components/open_timeline/note_previews/index.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/note_previews/index.test.tsx deleted file mode 100644 index 463111bd9735f0..00000000000000 --- a/x-pack/plugins/siem/public/components/open_timeline/note_previews/index.test.tsx +++ /dev/null @@ -1,188 +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 euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import { cloneDeep } from 'lodash/fp'; -import moment from 'moment'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import React from 'react'; -import { ThemeProvider } from 'styled-components'; - -import { mockTimelineResults } from '../../../mock/timeline_results'; -import { OpenTimelineResult, TimelineResultNote } from '../types'; -import { NotePreviews } from '.'; - -describe('NotePreviews', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); - let mockResults: OpenTimelineResult[]; - let note1updated: number; - let note2updated: number; - let note3updated: number; - - beforeEach(() => { - mockResults = cloneDeep(mockTimelineResults); - note1updated = moment('2019-03-24T04:12:33.000Z').valueOf(); - note2updated = moment(note1updated) - .add(1, 'minute') - .valueOf(); - note3updated = moment(note2updated) - .add(1, 'minute') - .valueOf(); - }); - - test('it renders a note preview for each note when isModal is false', () => { - const hasNotes: OpenTimelineResult[] = [{ ...mockResults[0] }]; - - const wrapper = mountWithIntl( - - - - ); - - hasNotes[0].notes!.forEach(({ savedObjectId }) => { - expect(wrapper.find(`[data-test-subj="note-preview-${savedObjectId}"]`).exists()).toBe(true); - }); - }); - - test('it renders a note preview for each note when isModal is true', () => { - const hasNotes: OpenTimelineResult[] = [{ ...mockResults[0] }]; - - const wrapper = mountWithIntl( - - - - ); - - hasNotes[0].notes!.forEach(({ savedObjectId }) => { - expect(wrapper.find(`[data-test-subj="note-preview-${savedObjectId}"]`).exists()).toBe(true); - }); - }); - - test('it does NOT render the preview container if notes is undefined', () => { - const wrapper = mountWithIntl(); - - expect(wrapper.find('[data-test-subj="note-previews-container"]').exists()).toBe(false); - }); - - test('it does NOT render the preview container if notes is null', () => { - const wrapper = mountWithIntl(); - - expect(wrapper.find('[data-test-subj="note-previews-container"]').exists()).toBe(false); - }); - - test('it does NOT render the preview container if notes is empty', () => { - const wrapper = mountWithIntl(); - - expect(wrapper.find('[data-test-subj="note-previews-container"]').exists()).toBe(false); - }); - - test('it filters-out non-unique savedObjectIds', () => { - const nonUniqueNotes: TimelineResultNote[] = [ - { - note: '1', - savedObjectId: 'noteId1', - updated: note1updated, - updatedBy: 'alice', - }, - { - note: '2 (savedObjectId is the same as the previous entry)', - savedObjectId: 'noteId1', - updated: note2updated, - updatedBy: 'alice', - }, - { - note: '3', - savedObjectId: 'noteId2', - updated: note3updated, - updatedBy: 'bob', - }, - ]; - - const wrapper = mountWithIntl( - - - - ); - - expect( - wrapper - .find(`[data-test-subj="updated-by"]`) - .at(2) - .text() - ).toEqual('bob'); - }); - - test('it filters-out null savedObjectIds', () => { - const nonUniqueNotes: TimelineResultNote[] = [ - { - note: '1', - savedObjectId: 'noteId1', - updated: note1updated, - updatedBy: 'alice', - }, - { - note: '2 (savedObjectId is null)', - savedObjectId: null, - updated: note2updated, - updatedBy: 'alice', - }, - { - note: '3', - savedObjectId: 'noteId2', - updated: note3updated, - updatedBy: 'bob', - }, - ]; - - const wrapper = mountWithIntl( - - - - ); - - expect( - wrapper - .find(`[data-test-subj="updated-by"]`) - .at(2) - .text() - ).toEqual('bob'); - }); - - test('it filters-out undefined savedObjectIds', () => { - const nonUniqueNotes: TimelineResultNote[] = [ - { - note: '1', - savedObjectId: 'noteId1', - updated: note1updated, - updatedBy: 'alice', - }, - { - note: 'b (savedObjectId is undefined)', - updated: note2updated, - updatedBy: 'alice', - }, - { - note: 'c', - savedObjectId: 'noteId2', - updated: note3updated, - updatedBy: 'bob', - }, - ]; - - const wrapper = mountWithIntl( - - - - ); - - expect( - wrapper - .find(`[data-test-subj="updated-by"]`) - .at(2) - .text() - ).toEqual('bob'); - }); -}); diff --git a/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/index.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/index.test.tsx deleted file mode 100644 index 178c69e6957e1d..00000000000000 --- a/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/index.test.tsx +++ /dev/null @@ -1,73 +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 euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import { mount } from 'enzyme'; -import React from 'react'; -import { MockedProvider } from 'react-apollo/test-utils'; -import { ThemeProvider } from 'styled-components'; - -import { wait } from '../../../lib/helpers'; -import { TestProviderWithoutDragAndDrop } from '../../../mock/test_providers'; -import { mockOpenTimelineQueryResults } from '../../../mock/timeline_results'; -import { useGetAllTimeline, getAllTimeline } from '../../../containers/timeline/all'; - -import { OpenTimelineModal } from '.'; - -jest.mock('../../../lib/kibana'); -jest.mock('../../../utils/apollo_context', () => ({ - useApolloClient: () => ({}), -})); -jest.mock('../../../containers/timeline/all', () => { - const originalModule = jest.requireActual('../../../containers/timeline/all'); - return { - useGetAllTimeline: jest.fn(), - getAllTimeline: originalModule.getAllTimeline, - }; -}); -jest.mock('../use_timeline_types', () => { - return { - useTimelineTypes: jest.fn().mockReturnValue({ - timelineType: 'default', - timelineTabs:
, - timelineFilters:
, - }), - }; -}); - -describe('OpenTimelineModal', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); - beforeEach(() => { - ((useGetAllTimeline as unknown) as jest.Mock).mockReturnValue({ - fetchAllTimeline: jest.fn(), - timelines: getAllTimeline( - '', - mockOpenTimelineQueryResults[0].result.data?.getAllTimeline?.timeline ?? [] - ), - loading: false, - totalCount: mockOpenTimelineQueryResults[0].result.data.getAllTimeline.totalCount, - refetch: jest.fn(), - }); - }); - - test('it renders the expected modal', async () => { - const wrapper = mount( - - - - - - - - ); - - await wait(); - - wrapper.update(); - - expect(wrapper.find('div[data-test-subj="open-timeline-modal"].euiModal').length).toEqual(1); - }); -}); diff --git a/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/index.tsx b/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/index.tsx deleted file mode 100644 index c530929a3c96ee..00000000000000 --- a/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/index.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. - */ - -import { EuiModal, EuiOverlayMask } from '@elastic/eui'; -import React from 'react'; - -import { TimelineModel } from '../../../store/timeline/model'; -import { useApolloClient } from '../../../utils/apollo_context'; - -import * as i18n from '../translations'; -import { ActionTimelineToShow } from '../types'; -import { StatefulOpenTimeline } from '..'; - -export interface OpenTimelineModalProps { - onClose: () => void; - hideActions?: ActionTimelineToShow[]; - modalTitle?: string; - onOpen?: (timeline: TimelineModel) => void; -} - -const DEFAULT_SEARCH_RESULTS_PER_PAGE = 10; -const OPEN_TIMELINE_MODAL_WIDTH = 1000; // px - -export const OpenTimelineModal = React.memo( - ({ hideActions = [], modalTitle, onClose, onOpen }) => { - const apolloClient = useApolloClient(); - - if (!apolloClient) return null; - - return ( - - - - - - ); - } -); - -OpenTimelineModal.displayName = 'OpenTimelineModal'; diff --git a/x-pack/plugins/siem/public/components/open_timeline/timelines_table/index.test.tsx b/x-pack/plugins/siem/public/components/open_timeline/timelines_table/index.test.tsx deleted file mode 100644 index 44e6218b5ad259..00000000000000 --- a/x-pack/plugins/siem/public/components/open_timeline/timelines_table/index.test.tsx +++ /dev/null @@ -1,326 +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 euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import { cloneDeep } from 'lodash/fp'; -import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import React from 'react'; -import { ThemeProvider } from 'styled-components'; - -import { mockTimelineResults } from '../../../mock/timeline_results'; -import { OpenTimelineResult } from '../types'; -import { TimelinesTable, TimelinesTableProps } from '.'; -import { getMockTimelinesTableProps } from './mocks'; - -import * as i18n from '../translations'; - -jest.mock('../../../lib/kibana'); - -describe('TimelinesTable', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); - let mockResults: OpenTimelineResult[]; - - beforeEach(() => { - mockResults = cloneDeep(mockTimelineResults); - }); - - test('it renders the select all timelines header checkbox when actionTimelineToShow has the action selectable', () => { - const wrapper = mountWithIntl( - - - - ); - - expect( - wrapper - .find('thead tr th input') - .first() - .exists() - ).toBe(true); - }); - - test('it does NOT render the select all timelines header checkbox when actionTimelineToShow has not the action selectable', () => { - const testProps: TimelinesTableProps = { - ...getMockTimelinesTableProps(mockResults), - actionTimelineToShow: ['delete', 'duplicate'], - }; - const wrapper = mountWithIntl( - - - - ); - - expect( - wrapper - .find('thead tr th input') - .first() - .exists() - ).toBe(false); - }); - - test('it renders the Modified By column when showExtendedColumns is true ', () => { - const testProps: TimelinesTableProps = { - ...getMockTimelinesTableProps(mockResults), - showExtendedColumns: true, - }; - const wrapper = mountWithIntl( - - - - ); - - expect( - wrapper - .find('thead tr th') - .at(4) - .text() - ).toContain(i18n.MODIFIED_BY); - }); - - test('it renders the notes column in the position of the Modified By column when showExtendedColumns is false', () => { - const testProps: TimelinesTableProps = { - ...getMockTimelinesTableProps(mockResults), - showExtendedColumns: false, - }; - const wrapper = mountWithIntl( - - - - ); - - expect( - wrapper - .find('thead tr th') - .at(5) - .find('[data-test-subj="notes-count-header-icon"]') - .first() - .exists() - ).toBe(true); - }); - - test('it renders the delete timeline (trash icon) when actionTimelineToShow has the delete action', () => { - const wrapper = mountWithIntl( - - - - ); - - expect( - wrapper - .find('[data-test-subj="delete-timeline"]') - .first() - .exists() - ).toBe(true); - }); - - test('it does NOT render the delete timeline (trash icon) when actionTimelineToShow has NOT the delete action', () => { - const testProps: TimelinesTableProps = { - ...getMockTimelinesTableProps(mockResults), - actionTimelineToShow: ['duplicate', 'selectable'], - }; - const wrapper = mountWithIntl( - - - - ); - - expect( - wrapper - .find('[data-test-subj="delete-timeline"]') - .first() - .exists() - ).toBe(false); - }); - - test('it renders the rows per page selector when showExtendedColumns is true', () => { - const wrapper = mountWithIntl( - - - - ); - - expect( - wrapper - .find('EuiTablePagination EuiPopover') - .first() - .exists() - ).toBe(true); - }); - - test('it does NOT render the rows per page selector when showExtendedColumns is false', () => { - const testProps: TimelinesTableProps = { - ...getMockTimelinesTableProps(mockResults), - showExtendedColumns: false, - }; - const wrapper = mountWithIntl( - - - - ); - - expect( - wrapper - .find('EuiTablePagination EuiPopover') - .first() - .exists() - ).toBe(false); - }); - - test('it renders the default page size specified by the defaultPageSize prop', () => { - const defaultPageSize = 123; - const testProps = { - ...getMockTimelinesTableProps(mockResults), - defaultPageSize, - pageSize: defaultPageSize, - }; - const wrapper = mountWithIntl( - - - - ); - - expect( - wrapper - .find('EuiTablePagination EuiPopover') - .first() - .text() - ).toEqual('Rows per page: 123'); - }); - - test('it sorts the Last Modified column in descending order when showExtendedColumns is true ', () => { - const wrapper = mountWithIntl( - - - - ); - - expect( - wrapper - .find('[aria-sort="descending"]') - .first() - .text() - ).toContain(i18n.LAST_MODIFIED); - }); - - test('it sorts the Last Modified column in descending order when showExtendedColumns is false ', () => { - const testProps: TimelinesTableProps = { - ...getMockTimelinesTableProps(mockResults), - showExtendedColumns: false, - }; - const wrapper = mountWithIntl( - - - - ); - - expect( - wrapper - .find('[aria-sort="descending"]') - .first() - .text() - ).toContain(i18n.LAST_MODIFIED); - }); - - test('it displays the expected message when no search results are found', () => { - const testProps: TimelinesTableProps = { - ...getMockTimelinesTableProps(mockResults), - searchResults: [], - }; - const wrapper = mountWithIntl( - - - - ); - - expect( - wrapper - .find('tbody tr td div') - .first() - .text() - ).toEqual(i18n.ZERO_TIMELINES_MATCH); - }); - - test('it invokes onTableChange with the expected parameters when a table header is clicked to sort it', () => { - const onTableChange = jest.fn(); - const testProps: TimelinesTableProps = { - ...getMockTimelinesTableProps(mockResults), - onTableChange, - }; - const wrapper = mountWithIntl( - - - - ); - - wrapper - .find('thead tr th button') - .at(0) - .simulate('click'); - - wrapper.update(); - - expect(onTableChange).toHaveBeenCalledWith({ - page: { index: 0, size: 10 }, - sort: { direction: 'asc', field: 'updated' }, - }); - }); - - test('it invokes onSelectionChange when a row is selected', () => { - const onSelectionChange = jest.fn(); - const testProps: TimelinesTableProps = { - ...getMockTimelinesTableProps(mockResults), - onSelectionChange, - }; - const wrapper = mountWithIntl( - - - - ); - - wrapper - .find('thead tr th input') - .at(0) - .simulate('change', { target: { checked: true } }); - - wrapper.update(); - - expect(onSelectionChange).toHaveBeenCalled(); - }); - - test('it enables the table loading animation when isLoading is true', () => { - const testProps: TimelinesTableProps = { - ...getMockTimelinesTableProps(mockResults), - loading: true, - }; - const wrapper = mountWithIntl( - - - - ); - - const props = wrapper - .find('[data-test-subj="timelines-table"]') - .first() - .props() as TimelinesTableProps; - - expect(props.loading).toBe(true); - }); - - test('it disables the table loading animation when isLoading is false', () => { - const wrapper = mountWithIntl( - - - - ); - - const props = wrapper - .find('[data-test-subj="timelines-table"]') - .first() - .props() as TimelinesTableProps; - - expect(props.loading).toBe(false); - }); -}); diff --git a/x-pack/plugins/siem/public/components/open_timeline/timelines_table/mocks.ts b/x-pack/plugins/siem/public/components/open_timeline/timelines_table/mocks.ts deleted file mode 100644 index 519dfc1b66efee..00000000000000 --- a/x-pack/plugins/siem/public/components/open_timeline/timelines_table/mocks.ts +++ /dev/null @@ -1,32 +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_SEARCH_RESULTS_PER_PAGE } from '../../../pages/timelines/timelines_page'; -import { DEFAULT_SORT_DIRECTION, DEFAULT_SORT_FIELD } from '../constants'; -import { OpenTimelineResult } from '../types'; -import { TimelinesTableProps } from '.'; - -export const getMockTimelinesTableProps = ( - mockOpenTimelineResults: OpenTimelineResult[] -): TimelinesTableProps => ({ - actionTimelineToShow: ['delete', 'duplicate', 'selectable'], - deleteTimelines: jest.fn(), - defaultPageSize: DEFAULT_SEARCH_RESULTS_PER_PAGE, - enableExportTimelineDownloader: jest.fn(), - itemIdToExpandedNotesRowMap: {}, - loading: false, - onOpenDeleteTimelineModal: jest.fn(), - onOpenTimeline: jest.fn(), - onSelectionChange: jest.fn(), - onTableChange: jest.fn(), - onToggleShowNotes: jest.fn(), - pageIndex: 0, - pageSize: DEFAULT_SEARCH_RESULTS_PER_PAGE, - searchResults: mockOpenTimelineResults, - showExtendedColumns: true, - sortDirection: DEFAULT_SORT_DIRECTION, - sortField: DEFAULT_SORT_FIELD, - totalSearchResultsCount: mockOpenTimelineResults.length, -}); diff --git a/x-pack/plugins/siem/public/components/open_timeline/title_row/index.tsx b/x-pack/plugins/siem/public/components/open_timeline/title_row/index.tsx deleted file mode 100644 index 559bbc3eecb824..00000000000000 --- a/x-pack/plugins/siem/public/components/open_timeline/title_row/index.tsx +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React from 'react'; - -import * as i18n from '../translations'; -import { OpenTimelineProps } from '../types'; -import { HeaderSection } from '../../header_section'; - -type Props = Pick & { - /** The number of timelines currently selected */ - selectedTimelinesCount: number; - children?: JSX.Element; -}; - -/** - * Renders the row containing the tile (e.g. Open Timelines / All timelines) - * and action buttons (i.e. Favorite Selected and Delete Selected) - */ -export const TitleRow = React.memo( - ({ children, onAddTimelinesToFavorites, selectedTimelinesCount, title }) => ( - - - {onAddTimelinesToFavorites && ( - - - {i18n.FAVORITE_SELECTED} - - - )} - - {children && {children}} - - - ) -); - -TitleRow.displayName = 'TitleRow'; diff --git a/x-pack/plugins/siem/public/components/open_timeline/types.ts b/x-pack/plugins/siem/public/components/open_timeline/types.ts deleted file mode 100644 index 4d953f6fa775e1..00000000000000 --- a/x-pack/plugins/siem/public/components/open_timeline/types.ts +++ /dev/null @@ -1,203 +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 { SetStateAction, Dispatch } from 'react'; -import { AllTimelinesVariables } from '../../containers/timeline/all'; -import { TimelineModel } from '../../store/timeline/model'; -import { NoteResult } from '../../graphql/types'; -import { TimelineType, TimelineTypeLiteral } from '../../../common/types/timeline'; - -/** The users who added a timeline to favorites */ -export interface FavoriteTimelineResult { - userId?: number | null; - userName?: string | null; - favoriteDate?: number | null; -} - -export interface TimelineResultNote { - savedObjectId?: string | null; - note?: string | null; - noteId?: string | null; - updated?: number | null; - updatedBy?: string | null; -} - -export interface TimelineActionsOverflowColumns { - width: string; - actions: Array<{ - name: string; - icon?: string; - onClick?: (timeline: OpenTimelineResult) => void; - description: string; - render?: (timeline: OpenTimelineResult) => JSX.Element; - } | null>; -} - -/** The results of the query run by the OpenTimeline component */ -export interface OpenTimelineResult { - created?: number | null; - description?: string | null; - eventIdToNoteIds?: Readonly> | null; - favorite?: FavoriteTimelineResult[] | null; - noteIds?: string[] | null; - notes?: TimelineResultNote[] | null; - pinnedEventIds?: Readonly> | null; - savedObjectId?: string | null; - title?: string | null; - templateTimelineId?: string | null; - type?: TimelineType.template | TimelineType.default; - updated?: number | null; - updatedBy?: string | null; -} - -/** - * EuiSearchBar returns this object when the user changes the query. At the - * time of this writing, there is no typescript definition for this type, so - * only the properties used by the Open Timeline component are exposed. - */ -export interface EuiSearchBarQuery { - queryText: string; -} - -/** Performs IO to delete the specified timelines */ -export type DeleteTimelines = (timelineIds: string[], variables?: AllTimelinesVariables) => void; - -/** Invoked when the user clicks the action make the selected timelines favorites */ -export type OnAddTimelinesToFavorites = () => void; - -/** Invoked when the user clicks the action to delete the selected timelines */ -export type OnDeleteSelected = () => void; -export type OnDeleteOneTimeline = (timelineIds: string[]) => void; - -/** Invoked when the user clicks on the name of a timeline to open it */ -export type OnOpenTimeline = ({ - duplicate, - timelineId, -}: { - duplicate: boolean; - timelineId: string; -}) => void; - -export type OnOpenDeleteTimelineModal = (selectedItem: OpenTimelineResult) => void; -export type SetActionTimeline = Dispatch>; -export type EnableExportTimelineDownloader = (selectedItem: OpenTimelineResult) => void; -/** Invoked when the user presses enters to submit the text in the search input */ -export type OnQueryChange = (query: EuiSearchBarQuery) => void; - -/** Invoked when the user selects (or de-selects) timelines in the table */ -export type OnSelectionChange = (selectedItems: OpenTimelineResult[]) => void; - -/** Invoked when the user toggles the option to only view favorite timelines */ -export type OnToggleOnlyFavorites = () => void; - -/** Invoked when the user toggles the expansion or collapse of inline notes in a table row */ -export type OnToggleShowNotes = (itemIdToExpandedNotesRowMap: Record) => void; - -/** Parameters to the OnTableChange callback */ -export interface OnTableChangeParams { - page: { - index: number; - size: number; - }; - sort: { - field: string; - direction: 'asc' | 'desc'; - }; -} - -/** Invoked by the EUI table implementation when the user interacts with the table */ -export type OnTableChange = (tableChange: OnTableChangeParams) => void; - -export type ActionTimelineToShow = 'duplicate' | 'delete' | 'export' | 'selectable'; - -export interface OpenTimelineProps { - /** Invoked when the user clicks the delete (trash) icon on an individual timeline */ - deleteTimelines?: DeleteTimelines; - /** The default requested size of each page of search results */ - defaultPageSize: number; - /** Displays an indicator that data is loading when true */ - isLoading: boolean; - /** Required by EuiTable for expandable rows: a map of `TimelineResult.savedObjectId` to rendered notes */ - itemIdToExpandedNotesRowMap: Record; - /** Display import timelines modal*/ - importDataModalToggle?: boolean; - /** If this callback is specified, a "Favorite Selected" button will be displayed, and this callback will be invoked when the button is clicked */ - onAddTimelinesToFavorites?: OnAddTimelinesToFavorites; - /** If this callback is specified, a "Delete Selected" button will be displayed, and this callback will be invoked when the button is clicked */ - onDeleteSelected?: OnDeleteSelected; - /** Only show favorite timelines when true */ - onlyFavorites: boolean; - /** Invoked when the user presses enter after typing in the search bar */ - onQueryChange: OnQueryChange; - /** Invoked when the user selects (or de-selects) timelines in the table */ - onSelectionChange: OnSelectionChange; - /** Invoked when the user clicks on the name of a timeline to open it */ - onOpenTimeline: OnOpenTimeline; - /** Invoked by the EUI table implementation when the user interacts with the table */ - onTableChange: OnTableChange; - /** Invoked when the user toggles the option to only show favorite timelines */ - onToggleOnlyFavorites: OnToggleOnlyFavorites; - /** Invoked when the user toggles the expansion or collapse of inline notes in a table row */ - onToggleShowNotes: OnToggleShowNotes; - /** the requested page of results */ - pageIndex: number; - /** the requested size of each page of search results */ - pageSize: number; - /** The currently applied search criteria */ - query: string; - /** Refetch table */ - refetch?: (existingTimeline?: OpenTimelineResult[], existingCount?: number) => void; - /** The results of executing a search */ - searchResults: OpenTimelineResult[]; - /** the currently-selected timelines in the table */ - selectedItems: OpenTimelineResult[]; - /** Toggle export timelines modal*/ - setImportDataModalToggle?: React.Dispatch>; - /** the requested sort direction of the query results */ - sortDirection: 'asc' | 'desc'; - /** the requested field to sort on */ - sortField: string; - /** timeline / template timeline */ - tabs: JSX.Element; - /** The title of the Open Timeline component */ - title: string; - /** The total (server-side) count of the search results */ - totalSearchResultsCount: number; - /** Hide action on timeline if needed it */ - hideActions?: ActionTimelineToShow[]; -} - -export interface UpdateTimeline { - duplicate: boolean; - id: string; - from: number; - notes: NoteResult[] | null | undefined; - timeline: TimelineModel; - to: number; - ruleNote?: string; -} - -export type DispatchUpdateTimeline = ({ - duplicate, - id, - from, - notes, - timeline, - to, - ruleNote, -}: UpdateTimeline) => () => void; - -export enum TimelineTabsStyle { - tab = 'tab', - filter = 'filter', -} - -export interface TimelineTab { - id: TimelineTypeLiteral; - name: string; - disabled: boolean; - href: string; -} diff --git a/x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.test.tsx b/x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.test.tsx deleted file mode 100644 index 677fc5e102614a..00000000000000 --- a/x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.test.tsx +++ /dev/null @@ -1,170 +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 { mount, shallow } from 'enzyme'; -import React from 'react'; - -import { apolloClientObservable, mockGlobalState, TestProviders } from '../../../mock'; -import { createStore, State } from '../../../store'; -import { AddFilterToGlobalSearchBar } from '.'; - -const mockAddFilters = jest.fn(); -jest.mock('../../../lib/kibana', () => ({ - useKibana: () => ({ - services: { - data: { - query: { - filterManager: { - addFilters: mockAddFilters, - }, - }, - }, - }, - }), -})); - -describe('AddFilterToGlobalSearchBar Component', () => { - const state: State = mockGlobalState; - let store = createStore(state, apolloClientObservable); - - beforeEach(() => { - store = createStore(state, apolloClientObservable); - mockAddFilters.mockClear(); - }); - - test('Rendering', async () => { - const wrapper = shallow( - - <>{'siem-kibana'} - - ); - - expect(wrapper).toMatchSnapshot(); - }); - - test('Rendering tooltip', async () => { - const wrapper = shallow( - - - <>{'siem-kibana'} - - - ); - - wrapper.simulate('mouseenter'); - wrapper.update(); - expect(wrapper.find('[data-test-subj="hover-actions-container"] svg').first()).toBeTruthy(); - }); - - test('Functionality with inputs state', async () => { - const onFilterAdded = jest.fn(); - - const wrapper = mount( - - - <>{'siem-kibana'} - - - ); - - wrapper - .simulate('mouseenter') - .find('[data-test-subj="hover-actions-container"] [data-euiicon-type]') - .first() - .simulate('click'); - wrapper.update(); - - expect(mockAddFilters.mock.calls[0][0]).toEqual({ - meta: { - alias: null, - disabled: false, - key: 'host.name', - negate: false, - params: { - query: 'siem-kibana', - }, - type: 'phrase', - value: 'siem-kibana', - }, - query: { - match: { - 'host.name': { - query: 'siem-kibana', - type: 'phrase', - }, - }, - }, - }); - expect(onFilterAdded).toHaveBeenCalledTimes(1); - }); -}); diff --git a/x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.tsx b/x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.tsx deleted file mode 100644 index 7aed36422bd2f5..00000000000000 --- a/x-pack/plugins/siem/public/components/page/add_filter_to_global_search_bar/index.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import React, { useCallback } from 'react'; - -import { Filter } from '../../../../../../../src/plugins/data/public'; -import { WithHoverActions } from '../../with_hover_actions'; -import { useKibana } from '../../../lib/kibana'; - -import * as i18n from './translations'; - -export * from './helpers'; - -interface OwnProps { - children: JSX.Element; - filter: Filter; - onFilterAdded?: () => void; -} - -export const AddFilterToGlobalSearchBar = React.memo( - ({ children, filter, onFilterAdded }) => { - const { filterManager } = useKibana().services.data.query; - - const filterForValue = useCallback(() => { - filterManager.addFilters(filter); - - if (onFilterAdded != null) { - onFilterAdded(); - } - }, [filterManager, filter, onFilterAdded]); - - const filterOutValue = useCallback(() => { - filterManager.addFilters({ - ...filter, - meta: { - ...filter.meta, - negate: true, - }, - }); - - if (onFilterAdded != null) { - onFilterAdded(); - } - }, [filterManager, filter, onFilterAdded]); - - return ( - - - - - - - - -
- } - render={() => children} - /> - ); - } -); - -AddFilterToGlobalSearchBar.displayName = 'AddFilterToGlobalSearchBar'; diff --git a/x-pack/plugins/siem/public/components/page/hosts/authentications_table/index.test.tsx b/x-pack/plugins/siem/public/components/page/hosts/authentications_table/index.test.tsx deleted file mode 100644 index d7c25e97b3838d..00000000000000 --- a/x-pack/plugins/siem/public/components/page/hosts/authentications_table/index.test.tsx +++ /dev/null @@ -1,85 +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 { shallow } from 'enzyme'; -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { Provider as ReduxStoreProvider } from 'react-redux'; - -import { apolloClientObservable, mockGlobalState } from '../../../../mock'; -import { createStore, hostsModel, State } from '../../../../store'; - -import { mockData } from './mock'; -import * as i18n from './translations'; -import { AuthenticationTable, getAuthenticationColumnsCurated } from '.'; - -describe('Authentication Table Component', () => { - const loadPage = jest.fn(); - const state: State = mockGlobalState; - - let store = createStore(state, apolloClientObservable); - - beforeEach(() => { - store = createStore(state, apolloClientObservable); - }); - - describe('rendering', () => { - test('it renders the authentication table', () => { - const wrapper = shallow( - - - - ); - - expect(wrapper.find('Connect(AuthenticationTableComponent)')).toMatchSnapshot(); - }); - }); - - describe('columns', () => { - test('on hosts page, we expect to get all columns', () => { - expect(getAuthenticationColumnsCurated(hostsModel.HostsType.page).length).toEqual(9); - }); - - test('on host details page, we expect to remove two columns', () => { - const columns = getAuthenticationColumnsCurated(hostsModel.HostsType.details); - expect(columns.length).toEqual(7); - }); - - test('on host details page, we should have Last Failed Destination column', () => { - const columns = getAuthenticationColumnsCurated(hostsModel.HostsType.page); - expect(columns.some(col => col.name === i18n.LAST_FAILED_DESTINATION)).toEqual(true); - }); - - test('on host details page, we should not have Last Failed Destination column', () => { - const columns = getAuthenticationColumnsCurated(hostsModel.HostsType.details); - expect(columns.some(col => col.name === i18n.LAST_FAILED_DESTINATION)).toEqual(false); - }); - - test('on host page, we should have Last Successful Destination column', () => { - const columns = getAuthenticationColumnsCurated(hostsModel.HostsType.page); - expect(columns.some(col => col.name === i18n.LAST_SUCCESSFUL_DESTINATION)).toEqual(true); - }); - - test('on host details page, we should not have Last Successful Destination column', () => { - const columns = getAuthenticationColumnsCurated(hostsModel.HostsType.details); - expect(columns.some(col => col.name === i18n.LAST_SUCCESSFUL_DESTINATION)).toEqual(false); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/page/hosts/authentications_table/index.tsx b/x-pack/plugins/siem/public/components/page/hosts/authentications_table/index.tsx deleted file mode 100644 index 678faff7654db8..00000000000000 --- a/x-pack/plugins/siem/public/components/page/hosts/authentications_table/index.tsx +++ /dev/null @@ -1,346 +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. - */ - -/* eslint-disable react/display-name */ - -import { has } from 'lodash/fp'; -import React, { useCallback, useMemo } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; - -import { hostsActions } from '../../../../store/hosts'; -import { AuthenticationsEdges } from '../../../../graphql/types'; -import { hostsModel, hostsSelectors, State } from '../../../../store'; -import { DragEffects, DraggableWrapper } from '../../../drag_and_drop/draggable_wrapper'; -import { escapeDataProviderId } from '../../../drag_and_drop/helpers'; -import { getEmptyTagValue } from '../../../empty_value'; -import { FormattedRelativePreferenceDate } from '../../../formatted_date'; -import { HostDetailsLink, IPDetailsLink } from '../../../links'; -import { Columns, ItemsPerRow, PaginatedTable } from '../../../paginated_table'; -import { IS_OPERATOR } from '../../../timeline/data_providers/data_provider'; -import { Provider } from '../../../timeline/data_providers/provider'; - -import * as i18n from './translations'; -import { getRowItemDraggables } from '../../../tables/helpers'; - -const tableType = hostsModel.HostsTableType.authentications; - -interface OwnProps { - data: AuthenticationsEdges[]; - fakeTotalCount: number; - loading: boolean; - loadPage: (newActivePage: number) => void; - id: string; - isInspect: boolean; - showMorePagesIndicator: boolean; - totalCount: number; - type: hostsModel.HostsType; -} - -export type AuthTableColumns = [ - Columns, - Columns, - Columns, - Columns, - Columns, - Columns, - Columns, - Columns, - Columns -]; - -type AuthenticationTableProps = OwnProps & PropsFromRedux; - -const rowItems: ItemsPerRow[] = [ - { - text: i18n.ROWS_5, - numberOfRow: 5, - }, - { - text: i18n.ROWS_10, - numberOfRow: 10, - }, -]; - -const AuthenticationTableComponent = React.memo( - ({ - activePage, - data, - fakeTotalCount, - id, - isInspect, - limit, - loading, - loadPage, - showMorePagesIndicator, - totalCount, - type, - updateTableActivePage, - updateTableLimit, - }) => { - const updateLimitPagination = useCallback( - newLimit => - updateTableLimit({ - hostsType: type, - limit: newLimit, - tableType, - }), - [type, updateTableLimit] - ); - - const updateActivePage = useCallback( - newPage => - updateTableActivePage({ - activePage: newPage, - hostsType: type, - tableType, - }), - [type, updateTableActivePage] - ); - - const columns = useMemo(() => getAuthenticationColumnsCurated(type), [type]); - - return ( - - ); - } -); - -AuthenticationTableComponent.displayName = 'AuthenticationTableComponent'; - -const makeMapStateToProps = () => { - const getAuthenticationsSelector = hostsSelectors.authenticationsSelector(); - return (state: State, { type }: OwnProps) => { - return getAuthenticationsSelector(state, type); - }; -}; - -const mapDispatchToProps = { - updateTableActivePage: hostsActions.updateTableActivePage, - updateTableLimit: hostsActions.updateTableLimit, -}; - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const AuthenticationTable = connector(AuthenticationTableComponent); - -const getAuthenticationColumns = (): AuthTableColumns => [ - { - name: i18n.USER, - truncateText: false, - hideForMobile: false, - render: ({ node }) => - getRowItemDraggables({ - rowItems: node.user.name, - attrName: 'user.name', - idPrefix: `authentications-table-${node._id}-userName`, - }), - }, - { - name: i18n.SUCCESSES, - truncateText: false, - hideForMobile: false, - render: ({ node }) => { - const id = escapeDataProviderId( - `authentications-table-${node._id}-node-successes-${node.successes}` - ); - return ( - - snapshot.isDragging ? ( - - - - ) : ( - node.successes - ) - } - /> - ); - }, - width: '8%', - }, - { - name: i18n.FAILURES, - truncateText: false, - hideForMobile: false, - render: ({ node }) => { - const id = escapeDataProviderId( - `authentications-table-${node._id}-failures-${node.failures}` - ); - return ( - - snapshot.isDragging ? ( - - - - ) : ( - node.failures - ) - } - /> - ); - }, - width: '8%', - }, - { - name: i18n.LAST_SUCCESSFUL_TIME, - truncateText: false, - hideForMobile: false, - render: ({ node }) => - has('lastSuccess.timestamp', node) && node.lastSuccess!.timestamp != null ? ( - - ) : ( - getEmptyTagValue() - ), - }, - { - name: i18n.LAST_SUCCESSFUL_SOURCE, - truncateText: false, - hideForMobile: false, - render: ({ node }) => - getRowItemDraggables({ - rowItems: - node.lastSuccess != null && - node.lastSuccess.source != null && - node.lastSuccess.source.ip != null - ? node.lastSuccess.source.ip - : null, - attrName: 'source.ip', - idPrefix: `authentications-table-${node._id}-lastSuccessSource`, - render: item => , - }), - }, - { - name: i18n.LAST_SUCCESSFUL_DESTINATION, - truncateText: false, - hideForMobile: false, - render: ({ node }) => - getRowItemDraggables({ - rowItems: - node.lastSuccess != null && - node.lastSuccess.host != null && - node.lastSuccess.host.name != null - ? node.lastSuccess.host.name - : null, - attrName: 'host.name', - idPrefix: `authentications-table-${node._id}-lastSuccessfulDestination`, - render: item => , - }), - }, - { - name: i18n.LAST_FAILED_TIME, - truncateText: false, - hideForMobile: false, - render: ({ node }) => - has('lastFailure.timestamp', node) && node.lastFailure!.timestamp != null ? ( - - ) : ( - getEmptyTagValue() - ), - }, - { - name: i18n.LAST_FAILED_SOURCE, - truncateText: false, - hideForMobile: false, - render: ({ node }) => - getRowItemDraggables({ - rowItems: - node.lastFailure != null && - node.lastFailure.source != null && - node.lastFailure.source.ip != null - ? node.lastFailure.source.ip - : null, - attrName: 'source.ip', - idPrefix: `authentications-table-${node._id}-lastFailureSource`, - render: item => , - }), - }, - { - name: i18n.LAST_FAILED_DESTINATION, - truncateText: false, - hideForMobile: false, - render: ({ node }) => - getRowItemDraggables({ - rowItems: - node.lastFailure != null && - node.lastFailure.host != null && - node.lastFailure.host.name != null - ? node.lastFailure.host.name - : null, - attrName: 'host.name', - idPrefix: `authentications-table-${node._id}-lastFailureDestination`, - render: item => , - }), - }, -]; - -export const getAuthenticationColumnsCurated = ( - pageType: hostsModel.HostsType -): AuthTableColumns => { - const columns = getAuthenticationColumns(); - - // Columns to exclude from host details pages - if (pageType === hostsModel.HostsType.details) { - return [i18n.LAST_FAILED_DESTINATION, i18n.LAST_SUCCESSFUL_DESTINATION].reduce((acc, name) => { - acc.splice( - acc.findIndex(column => column.name === name), - 1 - ); - return acc; - }, columns); - } - - return columns; -}; diff --git a/x-pack/plugins/siem/public/components/page/hosts/authentications_table/mock.ts b/x-pack/plugins/siem/public/components/page/hosts/authentications_table/mock.ts deleted file mode 100644 index 50a1fa8eb7d72e..00000000000000 --- a/x-pack/plugins/siem/public/components/page/hosts/authentications_table/mock.ts +++ /dev/null @@ -1,82 +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 { AuthenticationsData } from '../../../../graphql/types'; - -export const mockData: { Authentications: AuthenticationsData } = { - Authentications: { - totalCount: 54, - edges: [ - { - node: { - _id: 'cPsuhGcB0WOhS6qyTKC0', - failures: 10, - successes: 0, - user: { name: ['Evan Hassanabad'] }, - lastSuccess: { - timestamp: '2019-01-23T22:35:32.222Z', - source: { - ip: ['127.0.0.1'], - }, - host: { - id: ['host-id-1'], - name: ['host-1'], - }, - }, - lastFailure: { - timestamp: '2019-01-23T22:35:32.222Z', - source: { - ip: ['8.8.8.8'], - }, - host: { - id: ['host-id-1'], - name: ['host-2'], - }, - }, - }, - cursor: { - value: '98966fa2013c396155c460d35c0902be', - }, - }, - { - node: { - _id: 'KwQDiWcB0WOhS6qyXmrW', - failures: 10, - successes: 0, - user: { name: ['Braden Hassanabad'] }, - lastSuccess: { - timestamp: '2019-01-23T22:35:32.222Z', - source: { - ip: ['127.0.0.1'], - }, - host: { - id: ['host-id-1'], - name: ['host-1'], - }, - }, - lastFailure: { - timestamp: '2019-01-23T22:35:32.222Z', - source: { - ip: ['8.8.8.8'], - }, - host: { - id: ['host-id-1'], - name: ['host-2'], - }, - }, - }, - cursor: { - value: 'aa7ca589f1b8220002f2fc61c64cfbf1', - }, - }, - ], - pageInfo: { - activePage: 1, - fakeTotalCount: 50, - showMorePagesIndicator: true, - }, - }, -}; diff --git a/x-pack/plugins/siem/public/components/page/hosts/first_last_seen_host/index.test.tsx b/x-pack/plugins/siem/public/components/page/hosts/first_last_seen_host/index.test.tsx deleted file mode 100644 index 4a836333f3311c..00000000000000 --- a/x-pack/plugins/siem/public/components/page/hosts/first_last_seen_host/index.test.tsx +++ /dev/null @@ -1,138 +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 { cloneDeep } from 'lodash/fp'; -import React from 'react'; -import { MockedProvider } from 'react-apollo/test-utils'; -import { render, act } from '@testing-library/react'; - -import { mockFirstLastSeenHostQuery } from '../../../../containers/hosts/first_last_seen/mock'; -import { wait } from '../../../../lib/helpers'; -import { TestProviders } from '../../../../mock'; - -import { FirstLastSeenHost, FirstLastSeenHostType } from '.'; - -describe('FirstLastSeen Component', () => { - const firstSeen = 'Apr 8, 2019 @ 16:09:40.692'; - const lastSeen = 'Apr 8, 2019 @ 18:35:45.064'; - - // Suppress warnings about "react-apollo" until we migrate to apollo@3 - /* eslint-disable no-console */ - const originalError = console.error; - beforeAll(() => { - console.error = jest.fn(); - }); - afterAll(() => { - console.error = originalError; - }); - - test('Loading', async () => { - const { container } = render( - - - - - - ); - expect(container.innerHTML).toBe( - '' - ); - }); - - test('First Seen', async () => { - const { container } = render( - - - - - - ); - - await act(() => wait()); - - expect(container.innerHTML).toBe( - `
${firstSeen}
` - ); - }); - - test('Last Seen', async () => { - const { container } = render( - - - - - - ); - await act(() => wait()); - expect(container.innerHTML).toBe( - `
${lastSeen}
` - ); - }); - - test('First Seen is empty but not Last Seen', async () => { - const badDateTime = cloneDeep(mockFirstLastSeenHostQuery); - badDateTime[0].result.data!.source.HostFirstLastSeen.firstSeen = null; - const { container } = render( - - - - - - ); - - await act(() => wait()); - - expect(container.innerHTML).toBe( - `
${lastSeen}
` - ); - }); - - test('Last Seen is empty but not First Seen', async () => { - const badDateTime = cloneDeep(mockFirstLastSeenHostQuery); - badDateTime[0].result.data!.source.HostFirstLastSeen.lastSeen = null; - const { container } = render( - - - - - - ); - - await act(() => wait()); - - expect(container.innerHTML).toBe( - `
${firstSeen}
` - ); - }); - - test('First Seen With a bad date time string', async () => { - const badDateTime = cloneDeep(mockFirstLastSeenHostQuery); - badDateTime[0].result.data!.source.HostFirstLastSeen.firstSeen = 'something-invalid'; - const { container } = render( - - - - - - ); - await act(() => wait()); - expect(container.textContent).toBe('something-invalid'); - }); - - test('Last Seen With a bad date time string', async () => { - const badDateTime = cloneDeep(mockFirstLastSeenHostQuery); - badDateTime[0].result.data!.source.HostFirstLastSeen.lastSeen = 'something-invalid'; - const { container } = render( - - - - - - ); - await act(() => wait()); - expect(container.textContent).toBe('something-invalid'); - }); -}); diff --git a/x-pack/plugins/siem/public/components/page/hosts/first_last_seen_host/index.tsx b/x-pack/plugins/siem/public/components/page/hosts/first_last_seen_host/index.tsx deleted file mode 100644 index 70dff5eda59395..00000000000000 --- a/x-pack/plugins/siem/public/components/page/hosts/first_last_seen_host/index.tsx +++ /dev/null @@ -1,64 +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 { EuiIcon, EuiLoadingSpinner, EuiText, EuiToolTip } from '@elastic/eui'; -import React from 'react'; -import { ApolloConsumer } from 'react-apollo'; - -import { useFirstLastSeenHostQuery } from '../../../../containers/hosts/first_last_seen'; -import { getEmptyTagValue } from '../../../empty_value'; -import { FormattedRelativePreferenceDate } from '../../../formatted_date'; - -export enum FirstLastSeenHostType { - FIRST_SEEN = 'first-seen', - LAST_SEEN = 'last-seen', -} - -export const FirstLastSeenHost = React.memo<{ hostname: string; type: FirstLastSeenHostType }>( - ({ hostname, type }) => { - return ( - - {client => { - const { loading, firstSeen, lastSeen, errorMessage } = useFirstLastSeenHostQuery( - hostname, - 'default', - client - ); - if (errorMessage != null) { - return ( - - - - ); - } - const valueSeen = type === FirstLastSeenHostType.FIRST_SEEN ? firstSeen : lastSeen; - return ( - <> - {loading && } - {!loading && valueSeen != null && new Date(valueSeen).toString() === 'Invalid Date' - ? valueSeen - : !loading && - valueSeen != null && ( - - - - )} - {!loading && valueSeen == null && getEmptyTagValue()} - - ); - }} - - ); - } -); - -FirstLastSeenHost.displayName = 'FirstLastSeenHost'; diff --git a/x-pack/plugins/siem/public/components/page/hosts/host_overview/index.test.tsx b/x-pack/plugins/siem/public/components/page/hosts/host_overview/index.test.tsx deleted file mode 100644 index 90cfe696610d94..00000000000000 --- a/x-pack/plugins/siem/public/components/page/hosts/host_overview/index.test.tsx +++ /dev/null @@ -1,36 +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 { shallow } from 'enzyme'; -import React from 'react'; -import { TestProviders } from '../../../../mock'; - -import { HostOverview } from './index'; -import { mockData } from './mock'; -import { mockAnomalies } from '../../../ml/mock'; - -describe('Host Summary Component', () => { - describe('rendering', () => { - test('it renders the default Host Summary', () => { - const wrapper = shallow( - - - - ); - - expect(wrapper.find('HostOverview')).toMatchSnapshot(); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/page/hosts/host_overview/index.tsx b/x-pack/plugins/siem/public/components/page/hosts/host_overview/index.tsx deleted file mode 100644 index 4d0e6a737d303f..00000000000000 --- a/x-pack/plugins/siem/public/components/page/hosts/host_overview/index.tsx +++ /dev/null @@ -1,191 +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 { EuiFlexItem } from '@elastic/eui'; -import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; -import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; -import { getOr } from 'lodash/fp'; -import React from 'react'; - -import { DEFAULT_DARK_MODE } from '../../../../../common/constants'; -import { DescriptionList } from '../../../../../common/utility_types'; -import { useUiSetting$ } from '../../../../lib/kibana'; -import { getEmptyTagValue } from '../../../empty_value'; -import { DefaultFieldRenderer, hostIdRenderer } from '../../../field_renderers/field_renderers'; -import { InspectButton, InspectButtonContainer } from '../../../inspect'; -import { HostItem } from '../../../../graphql/types'; -import { Loader } from '../../../loader'; -import { IPDetailsLink } from '../../../links'; -import { hasMlUserPermissions } from '../../../ml/permissions/has_ml_user_permissions'; -import { useMlCapabilities } from '../../../ml_popover/hooks/use_ml_capabilities'; -import { AnomalyScores } from '../../../ml/score/anomaly_scores'; -import { Anomalies, NarrowDateRange } from '../../../ml/types'; -import { DescriptionListStyled, OverviewWrapper } from '../../index'; -import { FirstLastSeenHost, FirstLastSeenHostType } from '../first_last_seen_host'; - -import * as i18n from './translations'; - -interface HostSummaryProps { - data: HostItem; - id: string; - loading: boolean; - isLoadingAnomaliesData: boolean; - anomaliesData: Anomalies | null; - startDate: number; - endDate: number; - narrowDateRange: NarrowDateRange; -} - -const getDescriptionList = (descriptionList: DescriptionList[], key: number) => ( - - - -); - -export const HostOverview = React.memo( - ({ - data, - loading, - id, - startDate, - endDate, - isLoadingAnomaliesData, - anomaliesData, - narrowDateRange, - }) => { - const capabilities = useMlCapabilities(); - const userPermissions = hasMlUserPermissions(capabilities); - const [darkMode] = useUiSetting$(DEFAULT_DARK_MODE); - - const getDefaultRenderer = (fieldName: string, fieldData: HostItem) => ( - - ); - - const column: DescriptionList[] = [ - { - title: i18n.HOST_ID, - description: data.host - ? hostIdRenderer({ host: data.host, noLink: true }) - : getEmptyTagValue(), - }, - { - title: i18n.FIRST_SEEN, - description: - data.host != null && data.host.name && data.host.name.length ? ( - - ) : ( - getEmptyTagValue() - ), - }, - { - title: i18n.LAST_SEEN, - description: - data.host != null && data.host.name && data.host.name.length ? ( - - ) : ( - getEmptyTagValue() - ), - }, - ]; - const firstColumn = userPermissions - ? [ - ...column, - { - title: i18n.MAX_ANOMALY_SCORE_BY_JOB, - description: ( - - ), - }, - ] - : column; - - const descriptionLists: Readonly = [ - firstColumn, - [ - { - title: i18n.IP_ADDRESSES, - description: ( - (ip != null ? : getEmptyTagValue())} - /> - ), - }, - { - title: i18n.MAC_ADDRESSES, - description: getDefaultRenderer('host.mac', data), - }, - { title: i18n.PLATFORM, description: getDefaultRenderer('host.os.platform', data) }, - ], - [ - { title: i18n.OS, description: getDefaultRenderer('host.os.name', data) }, - { title: i18n.FAMILY, description: getDefaultRenderer('host.os.family', data) }, - { title: i18n.VERSION, description: getDefaultRenderer('host.os.version', data) }, - { title: i18n.ARCHITECTURE, description: getDefaultRenderer('host.architecture', data) }, - ], - [ - { - title: i18n.CLOUD_PROVIDER, - description: getDefaultRenderer('cloud.provider', data), - }, - { - title: i18n.REGION, - description: getDefaultRenderer('cloud.region', data), - }, - { - title: i18n.INSTANCE_ID, - description: getDefaultRenderer('cloud.instance.id', data), - }, - { - title: i18n.MACHINE_TYPE, - description: getDefaultRenderer('cloud.machine.type', data), - }, - ], - ]; - - return ( - - - - - {descriptionLists.map((descriptionList, index) => - getDescriptionList(descriptionList, index) - )} - - {loading && ( - - )} - - - ); - } -); - -HostOverview.displayName = 'HostOverview'; diff --git a/x-pack/plugins/siem/public/components/page/hosts/host_overview/mock.ts b/x-pack/plugins/siem/public/components/page/hosts/host_overview/mock.ts deleted file mode 100644 index d9a93272c09866..00000000000000 --- a/x-pack/plugins/siem/public/components/page/hosts/host_overview/mock.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { HostsData } from '../../../../graphql/types'; - -export const mockData: { Hosts: HostsData; DateFields: string[] } = { - Hosts: { - totalCount: 1, - edges: [ - { - node: { - _id: 'yneHlmgBjVl2VqDlAjPR', - host: { - architecture: ['x86_64'], - id: ['aa7ca589f1b8220002f2fc61c64cfbf1'], - ip: ['10.142.0.7', 'fe80::4001:aff:fe8e:7'], - mac: ['42:01:0a:8e:00:07'], - name: ['siem-kibana'], - os: { - family: ['debian'], - name: ['Debian GNU/Linux'], - platform: ['debian'], - version: ['9 (stretch)'], - }, - }, - cloud: { - instance: { - id: ['423232333829362673777'], - }, - machine: { - type: ['custom-4-16384'], - }, - provider: ['gce'], - region: ['us-east-1'], - }, - }, - cursor: { - value: 'aa7ca589f1b8220002f2fc61c64cfbf1', - }, - }, - ], - pageInfo: { - activePage: 1, - fakeTotalCount: 50, - showMorePagesIndicator: true, - }, - }, - DateFields: ['lastBeat'], -}; diff --git a/x-pack/plugins/siem/public/components/page/hosts/hosts_table/columns.tsx b/x-pack/plugins/siem/public/components/page/hosts/hosts_table/columns.tsx deleted file mode 100644 index 6bd82f3192f9be..00000000000000 --- a/x-pack/plugins/siem/public/components/page/hosts/hosts_table/columns.tsx +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiIcon, EuiToolTip } from '@elastic/eui'; -import React from 'react'; -import { DragEffects, DraggableWrapper } from '../../../drag_and_drop/draggable_wrapper'; -import { escapeDataProviderId } from '../../../drag_and_drop/helpers'; -import { getEmptyTagValue } from '../../../empty_value'; -import { HostDetailsLink } from '../../../links'; -import { FormattedRelativePreferenceDate } from '../../../formatted_date'; -import { IS_OPERATOR } from '../../../timeline/data_providers/data_provider'; -import { Provider } from '../../../timeline/data_providers/provider'; -import { AddFilterToGlobalSearchBar, createFilter } from '../../add_filter_to_global_search_bar'; -import { HostsTableColumns } from './'; - -import * as i18n from './translations'; - -export const getHostsColumns = (): HostsTableColumns => [ - { - field: 'node.host.name', - name: i18n.NAME, - truncateText: false, - hideForMobile: false, - sortable: true, - render: hostName => { - if (hostName != null && hostName.length > 0) { - const id = escapeDataProviderId(`hosts-table-hostName-${hostName[0]}`); - return ( - - snapshot.isDragging ? ( - - - - ) : ( - - ) - } - /> - ); - } - return getEmptyTagValue(); - }, - width: '35%', - }, - { - field: 'node.lastSeen', - name: ( - - <> - {i18n.LAST_SEEN}{' '} - - - - ), - truncateText: false, - hideForMobile: false, - sortable: true, - render: lastSeen => { - if (lastSeen != null) { - return ; - } - return getEmptyTagValue(); - }, - }, - { - field: 'node.host.os.name', - name: i18n.OS, - truncateText: false, - hideForMobile: false, - sortable: false, - render: hostOsName => { - if (hostOsName != null) { - return ( - - <>{hostOsName} - - ); - } - return getEmptyTagValue(); - }, - }, - { - field: 'node.host.os.version', - name: i18n.VERSION, - truncateText: false, - hideForMobile: false, - sortable: false, - render: hostOsVersion => { - if (hostOsVersion != null) { - return ( - - <>{hostOsVersion} - - ); - } - return getEmptyTagValue(); - }, - }, -]; diff --git a/x-pack/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx b/x-pack/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx deleted file mode 100644 index e561594013deaa..00000000000000 --- a/x-pack/plugins/siem/public/components/page/hosts/hosts_table/index.test.tsx +++ /dev/null @@ -1,137 +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 { shallow } from 'enzyme'; -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { MockedProvider } from 'react-apollo/test-utils'; - -import { - apolloClientObservable, - mockIndexPattern, - mockGlobalState, - TestProviders, -} from '../../../../mock'; -import { useMountAppended } from '../../../../utils/use_mount_appended'; -import { createStore, hostsModel, State } from '../../../../store'; -import { HostsTableType } from '../../../../store/hosts/model'; -import { HostsTable } from './index'; -import { mockData } from './mock'; - -// Test will fail because we will to need to mock some core services to make the test work -// For now let's forget about SiemSearchBar and QueryBar -jest.mock('../../../search_bar', () => ({ - SiemSearchBar: () => null, -})); -jest.mock('../../../query_bar', () => ({ - QueryBar: () => null, -})); - -describe('Hosts Table', () => { - const loadPage = jest.fn(); - const state: State = mockGlobalState; - - let store = createStore(state, apolloClientObservable); - const mount = useMountAppended(); - - beforeEach(() => { - store = createStore(state, apolloClientObservable); - }); - - describe('rendering', () => { - test('it renders the default Hosts table', () => { - const wrapper = shallow( - - - - ); - - expect(wrapper.find('HostsTable')).toMatchSnapshot(); - }); - - describe('Sorting on Table', () => { - let wrapper: ReturnType; - - beforeEach(() => { - wrapper = mount( - - - - - - ); - }); - test('Initial value of the store', () => { - expect(store.getState().hosts.page.queries[HostsTableType.hosts]).toEqual({ - activePage: 0, - direction: 'desc', - sortField: 'lastSeen', - limit: 10, - }); - expect( - wrapper - .find('.euiTable thead tr th button') - .at(1) - .text() - ).toEqual('Last seen Click to sort in ascending order'); - expect( - wrapper - .find('.euiTable thead tr th button') - .at(1) - .find('svg') - ).toBeTruthy(); - }); - - test('when you click on the column header, you should show the sorting icon', () => { - wrapper - .find('.euiTable thead tr th button') - .first() - .simulate('click'); - - wrapper.update(); - - expect(store.getState().hosts.page.queries[HostsTableType.hosts]).toEqual({ - activePage: 0, - direction: 'asc', - sortField: 'hostName', - limit: 10, - }); - expect( - wrapper - .find('.euiTable thead tr th button') - .first() - .text() - ).toEqual('Host nameClick to sort in descending order'); - }); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/page/hosts/hosts_table/index.tsx b/x-pack/plugins/siem/public/components/page/hosts/hosts_table/index.tsx deleted file mode 100644 index f09834d87e423d..00000000000000 --- a/x-pack/plugins/siem/public/components/page/hosts/hosts_table/index.tsx +++ /dev/null @@ -1,208 +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, { useMemo, useCallback } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import { IIndexPattern } from 'src/plugins/data/public'; - -import { hostsActions } from '../../../../store/actions'; -import { - Direction, - HostFields, - HostItem, - HostsEdges, - HostsFields, - HostsSortField, - OsFields, -} from '../../../../graphql/types'; -import { assertUnreachable } from '../../../../lib/helpers'; -import { hostsModel, hostsSelectors, State } from '../../../../store'; -import { - Columns, - Criteria, - ItemsPerRow, - PaginatedTable, - SortingBasicTable, -} from '../../../paginated_table'; - -import { getHostsColumns } from './columns'; -import * as i18n from './translations'; - -const tableType = hostsModel.HostsTableType.hosts; - -interface OwnProps { - data: HostsEdges[]; - fakeTotalCount: number; - id: string; - indexPattern: IIndexPattern; - isInspect: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - showMorePagesIndicator: boolean; - totalCount: number; - type: hostsModel.HostsType; -} - -export type HostsTableColumns = [ - Columns, - Columns, - Columns, - Columns -]; - -type HostsTableProps = OwnProps & PropsFromRedux; - -const rowItems: ItemsPerRow[] = [ - { - text: i18n.ROWS_5, - numberOfRow: 5, - }, - { - text: i18n.ROWS_10, - numberOfRow: 10, - }, -]; -const getSorting = ( - trigger: string, - sortField: HostsFields, - direction: Direction -): SortingBasicTable => ({ field: getNodeField(sortField), direction }); - -const HostsTableComponent = React.memo( - ({ - activePage, - data, - direction, - fakeTotalCount, - id, - indexPattern, - isInspect, - limit, - loading, - loadPage, - showMorePagesIndicator, - sortField, - totalCount, - type, - updateHostsSort, - updateTableActivePage, - updateTableLimit, - }) => { - const updateLimitPagination = useCallback( - newLimit => - updateTableLimit({ - hostsType: type, - limit: newLimit, - tableType, - }), - [type, updateTableLimit] - ); - - const updateActivePage = useCallback( - newPage => - updateTableActivePage({ - activePage: newPage, - hostsType: type, - tableType, - }), - [type, updateTableActivePage] - ); - - const onChange = useCallback( - (criteria: Criteria) => { - if (criteria.sort != null) { - const sort: HostsSortField = { - field: getSortField(criteria.sort.field), - direction: criteria.sort.direction as Direction, - }; - if (sort.direction !== direction || sort.field !== sortField) { - updateHostsSort({ - sort, - hostsType: type, - }); - } - } - }, - [direction, sortField, type, updateHostsSort] - ); - - const hostsColumns = useMemo(() => getHostsColumns(), []); - - const sorting = useMemo(() => getSorting(`${sortField}-${direction}`, sortField, direction), [ - sortField, - direction, - ]); - - return ( - - ); - } -); - -HostsTableComponent.displayName = 'HostsTableComponent'; - -const getSortField = (field: string): HostsFields => { - switch (field) { - case 'node.host.name': - return HostsFields.hostName; - case 'node.lastSeen': - return HostsFields.lastSeen; - default: - return HostsFields.lastSeen; - } -}; - -const getNodeField = (field: HostsFields): string => { - switch (field) { - case HostsFields.hostName: - return 'node.host.name'; - case HostsFields.lastSeen: - return 'node.lastSeen'; - } - assertUnreachable(field); -}; - -const makeMapStateToProps = () => { - const getHostsSelector = hostsSelectors.hostsSelector(); - const mapStateToProps = (state: State, { type }: OwnProps) => { - return getHostsSelector(state, type); - }; - return mapStateToProps; -}; - -const mapDispatchToProps = { - updateHostsSort: hostsActions.updateHostsSort, - updateTableActivePage: hostsActions.updateTableActivePage, - updateTableLimit: hostsActions.updateTableLimit, -}; - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const HostsTable = connector(HostsTableComponent); - -HostsTable.displayName = 'HostsTable'; diff --git a/x-pack/plugins/siem/public/components/page/hosts/hosts_table/mock.ts b/x-pack/plugins/siem/public/components/page/hosts/hosts_table/mock.ts deleted file mode 100644 index b5a9c925c599ad..00000000000000 --- a/x-pack/plugins/siem/public/components/page/hosts/hosts_table/mock.ts +++ /dev/null @@ -1,60 +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 { HostsData } from '../../../../graphql/types'; - -export const mockData: { Hosts: HostsData } = { - Hosts: { - totalCount: 4, - edges: [ - { - node: { - _id: 'cPsuhGcB0WOhS6qyTKC0', - host: { - name: ['elrond.elstc.co'], - os: { - name: ['Ubuntu'], - version: ['18.04.1 LTS (Bionic Beaver)'], - }, - }, - }, - cursor: { - value: '98966fa2013c396155c460d35c0902be', - }, - }, - { - node: { - _id: 'KwQDiWcB0WOhS6qyXmrW', - host: { - name: ['siem-kibana'], - os: { - name: ['Debian GNU/Linux'], - version: ['9 (stretch)'], - }, - }, - cloud: { - instance: { - id: ['423232333829362673777'], - }, - machine: { - type: ['custom-4-16384'], - }, - provider: ['gce'], - region: ['us-east-1'], - }, - }, - cursor: { - value: 'aa7ca589f1b8220002f2fc61c64cfbf1', - }, - }, - ], - pageInfo: { - activePage: 1, - fakeTotalCount: 50, - showMorePagesIndicator: true, - }, - }, -}; diff --git a/x-pack/plugins/siem/public/components/page/hosts/index.tsx b/x-pack/plugins/siem/public/components/page/hosts/index.tsx deleted file mode 100644 index 9b3f36faa065d2..00000000000000 --- a/x-pack/plugins/siem/public/components/page/hosts/index.tsx +++ /dev/null @@ -1,10 +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 * from './authentications_table'; -export * from './hosts_table'; -export * from './uncommon_process_table'; -export * from './kpi_hosts'; diff --git a/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/index.test.tsx b/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/index.test.tsx deleted file mode 100644 index dc2340d42ebd90..00000000000000 --- a/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/index.test.tsx +++ /dev/null @@ -1,106 +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 { mockKpiHostsData, mockKpiHostDetailsData } from './mock'; -import React from 'react'; -import { shallow, ShallowWrapper } from 'enzyme'; -import { KpiHostsComponentBase } from '.'; -import * as statItems from '../../../stat_items'; -import { kpiHostsMapping } from './kpi_hosts_mapping'; -import { kpiHostDetailsMapping } from './kpi_host_details_mapping'; - -describe('kpiHostsComponent', () => { - const ID = 'kpiHost'; - const from = new Date('2019-06-15T06:00:00.000Z').valueOf(); - const to = new Date('2019-06-18T06:00:00.000Z').valueOf(); - const narrowDateRange = () => {}; - describe('render', () => { - test('it should render spinner if it is loading', () => { - const wrapper: ShallowWrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); - }); - - test('it should render KpiHostsData', () => { - const wrapper: ShallowWrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); - }); - - test('it should render KpiHostDetailsData', () => { - const wrapper: ShallowWrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); - }); - }); - - const table = [ - [mockKpiHostsData, kpiHostsMapping] as [typeof mockKpiHostsData, typeof kpiHostsMapping], - [mockKpiHostDetailsData, kpiHostDetailsMapping] as [ - typeof mockKpiHostDetailsData, - typeof kpiHostDetailsMapping - ], - ]; - - describe.each(table)( - 'it should handle KpiHostsProps and KpiHostDetailsProps', - (data, mapping) => { - let mockUseKpiMatrixStatus: jest.SpyInstance; - beforeAll(() => { - mockUseKpiMatrixStatus = jest.spyOn(statItems, 'useKpiMatrixStatus'); - }); - - beforeEach(() => { - shallow( - - ); - }); - - afterEach(() => { - mockUseKpiMatrixStatus.mockClear(); - }); - - afterAll(() => { - mockUseKpiMatrixStatus.mockRestore(); - }); - - test(`it should apply correct mapping by given data type`, () => { - expect(mockUseKpiMatrixStatus).toBeCalledWith(mapping, data, ID, from, to, narrowDateRange); - }); - } - ); -}); diff --git a/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/index.tsx b/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/index.tsx deleted file mode 100644 index 65d59248218447..00000000000000 --- a/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/index.tsx +++ /dev/null @@ -1,80 +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 { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; -import React from 'react'; -import styled from 'styled-components'; - -import { KpiHostsData, KpiHostDetailsData } from '../../../../graphql/types'; -import { StatItemsComponent, StatItemsProps, useKpiMatrixStatus } from '../../../stat_items'; -import { kpiHostsMapping } from './kpi_hosts_mapping'; -import { kpiHostDetailsMapping } from './kpi_host_details_mapping'; -import { UpdateDateRange } from '../../../charts/common'; - -const kpiWidgetHeight = 247; - -interface GenericKpiHostProps { - from: number; - id: string; - loading: boolean; - to: number; - narrowDateRange: UpdateDateRange; -} - -interface KpiHostsProps extends GenericKpiHostProps { - data: KpiHostsData; -} - -interface KpiHostDetailsProps extends GenericKpiHostProps { - data: KpiHostDetailsData; -} - -const FlexGroupSpinner = styled(EuiFlexGroup)` - { - min-height: ${kpiWidgetHeight}px; - } -`; - -FlexGroupSpinner.displayName = 'FlexGroupSpinner'; - -export const KpiHostsComponentBase = ({ - data, - from, - loading, - id, - to, - narrowDateRange, -}: KpiHostsProps | KpiHostDetailsProps) => { - const mappings = - (data as KpiHostsData).hosts !== undefined ? kpiHostsMapping : kpiHostDetailsMapping; - const statItemsProps: StatItemsProps[] = useKpiMatrixStatus( - mappings, - data, - id, - from, - to, - narrowDateRange - ); - return loading ? ( - - - - - - ) : ( - - {statItemsProps.map((mappedStatItemProps, idx) => { - return ; - })} - - ); -}; - -KpiHostsComponentBase.displayName = 'KpiHostsComponentBase'; - -export const KpiHostsComponent = React.memo(KpiHostsComponentBase); - -KpiHostsComponent.displayName = 'KpiHostsComponent'; diff --git a/x-pack/plugins/siem/public/components/page/hosts/uncommon_process_table/index.test.tsx b/x-pack/plugins/siem/public/components/page/hosts/uncommon_process_table/index.test.tsx deleted file mode 100644 index 76fc2a0c389c38..00000000000000 --- a/x-pack/plugins/siem/public/components/page/hosts/uncommon_process_table/index.test.tsx +++ /dev/null @@ -1,345 +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 { shallow } from 'enzyme'; -import { getOr } from 'lodash/fp'; -import React from 'react'; - -import { TestProviders } from '../../../../mock'; -import { hostsModel } from '../../../../store'; -import { getEmptyValue } from '../../../empty_value'; -import { useMountAppended } from '../../../../utils/use_mount_appended'; - -import { getArgs, UncommonProcessTable, getUncommonColumnsCurated } from '.'; -import { mockData } from './mock'; -import { HostsType } from '../../../../store/hosts/model'; -import * as i18n from './translations'; - -describe('Uncommon Process Table Component', () => { - const loadPage = jest.fn(); - const mount = useMountAppended(); - - describe('rendering', () => { - test('it renders the default Uncommon process table', () => { - const wrapper = shallow( - - - - ); - - expect(wrapper.find('UncommonProcessTable')).toMatchSnapshot(); - }); - - test('it has a double dash (empty value) without any hosts at all', () => { - const wrapper = mount( - - - - ); - expect( - wrapper - .find('.euiTableRow') - .at(0) - .find('.euiTableRowCell') - .at(3) - .text() - ).toBe(`Host names${getEmptyValue()}`); - }); - - test('it has a single host without any extra comma when the number of hosts is exactly 1', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('.euiTableRow') - .at(1) - .find('.euiTableRowCell') - .at(3) - .text() - ).toBe('Host nameshello-world '); - }); - - test('it has a single link when the number of hosts is exactly 1', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('.euiTableRow') - .at(1) - .find('.euiTableRowCell') - .at(3) - .find('a').length - ).toBe(1); - }); - - test('it has a comma separated list of hosts when the number of hosts is greater than 1', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('.euiTableRow') - .at(2) - .find('.euiTableRowCell') - .at(3) - .text() - ).toBe('Host nameshello-world,hello-world-2 '); - }); - - test('it has 2 links when the number of hosts is equal to 2', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('.euiTableRow') - .at(2) - .find('.euiTableRowCell') - .at(3) - .find('a').length - ).toBe(2); - }); - - test('it is empty when all hosts are invalid because they do not contain an id and a name', () => { - const wrapper = mount( - - - - ); - expect( - wrapper - .find('.euiTableRow') - .at(3) - .find('.euiTableRowCell') - .at(3) - .text() - ).toBe(`Host names${getEmptyValue()}`); - }); - - test('it has no link when all hosts are invalid because they do not contain an id and a name', () => { - const wrapper = mount( - - - - ); - expect( - wrapper - .find('.euiTableRow') - .at(3) - .find('.euiTableRowCell') - .at(3) - .find('a').length - ).toBe(0); - }); - - test('it is returns two hosts when others are invalid because they do not contain an id and a name', () => { - const wrapper = mount( - - - - ); - expect( - wrapper - .find('.euiTableRow') - .at(4) - .find('.euiTableRowCell') - .at(3) - .text() - ).toBe('Host nameshello-world,hello-world-2 '); - }); - }); - - describe('#getArgs', () => { - test('it works with string array', () => { - const args = ['1', '2', '3']; - expect(getArgs(args)).toEqual('1 2 3'); - }); - - test('it returns null if empty array', () => { - const args: string[] = []; - expect(getArgs(args)).toEqual(null); - }); - - test('it returns null if given null', () => { - expect(getArgs(null)).toEqual(null); - }); - - test('it returns null if given undefined', () => { - expect(getArgs(undefined)).toEqual(null); - }); - }); - - describe('#getUncommonColumnsCurated', () => { - test('on hosts page, we expect to get all columns', () => { - expect(getUncommonColumnsCurated(HostsType.page).length).toEqual(6); - }); - - test('on host details page, we expect to remove two columns', () => { - const columns = getUncommonColumnsCurated(HostsType.details); - expect(columns.length).toEqual(4); - }); - - test('on host page, we should have hosts', () => { - const columns = getUncommonColumnsCurated(HostsType.page); - expect(columns.some(col => col.name === i18n.HOSTS)).toEqual(true); - }); - - test('on host page, we should have number of hosts', () => { - const columns = getUncommonColumnsCurated(HostsType.page); - expect(columns.some(col => col.name === i18n.NUMBER_OF_HOSTS)).toEqual(true); - }); - - test('on host details page, we should not have hosts', () => { - const columns = getUncommonColumnsCurated(HostsType.details); - expect(columns.some(col => col.name === i18n.HOSTS)).toEqual(false); - }); - - test('on host details page, we should not have number of hosts', () => { - const columns = getUncommonColumnsCurated(HostsType.details); - expect(columns.some(col => col.name === i18n.NUMBER_OF_HOSTS)).toEqual(false); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/page/hosts/uncommon_process_table/index.tsx b/x-pack/plugins/siem/public/components/page/hosts/uncommon_process_table/index.tsx deleted file mode 100644 index 2e59afcba4ac86..00000000000000 --- a/x-pack/plugins/siem/public/components/page/hosts/uncommon_process_table/index.tsx +++ /dev/null @@ -1,238 +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. - */ - -/* eslint-disable react/display-name */ - -import React, { useCallback, useMemo } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; - -import { hostsActions } from '../../../../store/actions'; -import { UncommonProcessesEdges, UncommonProcessItem } from '../../../../graphql/types'; -import { hostsModel, hostsSelectors, State } from '../../../../store'; -import { defaultToEmptyTag, getEmptyValue } from '../../../empty_value'; -import { HostDetailsLink } from '../../../links'; -import { Columns, ItemsPerRow, PaginatedTable } from '../../../paginated_table'; - -import * as i18n from './translations'; -import { getRowItemDraggables } from '../../../tables/helpers'; -import { HostsType } from '../../../../store/hosts/model'; -const tableType = hostsModel.HostsTableType.uncommonProcesses; -interface OwnProps { - data: UncommonProcessesEdges[]; - fakeTotalCount: number; - id: string; - isInspect: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - showMorePagesIndicator: boolean; - totalCount: number; - type: hostsModel.HostsType; -} - -export type UncommonProcessTableColumns = [ - Columns, - Columns, - Columns, - Columns, - Columns, - Columns -]; - -type UncommonProcessTableProps = OwnProps & PropsFromRedux; - -const rowItems: ItemsPerRow[] = [ - { - text: i18n.ROWS_5, - numberOfRow: 5, - }, - { - text: i18n.ROWS_10, - numberOfRow: 10, - }, -]; - -export const getArgs = (args: string[] | null | undefined): string | null => { - if (args != null && args.length !== 0) { - return args.join(' '); - } else { - return null; - } -}; - -const UncommonProcessTableComponent = React.memo( - ({ - activePage, - data, - fakeTotalCount, - id, - isInspect, - limit, - loading, - loadPage, - totalCount, - showMorePagesIndicator, - updateTableActivePage, - updateTableLimit, - type, - }) => { - const updateLimitPagination = useCallback( - newLimit => - updateTableLimit({ - hostsType: type, - limit: newLimit, - tableType, - }), - [type, updateTableLimit] - ); - - const updateActivePage = useCallback( - newPage => - updateTableActivePage({ - activePage: newPage, - hostsType: type, - tableType, - }), - [type, updateTableActivePage] - ); - - const columns = useMemo(() => getUncommonColumnsCurated(type), [type]); - - return ( - - ); - } -); - -UncommonProcessTableComponent.displayName = 'UncommonProcessTableComponent'; - -const makeMapStateToProps = () => { - const getUncommonProcessesSelector = hostsSelectors.uncommonProcessesSelector(); - return (state: State, { type }: OwnProps) => getUncommonProcessesSelector(state, type); -}; - -const mapDispatchToProps = { - updateTableActivePage: hostsActions.updateTableActivePage, - updateTableLimit: hostsActions.updateTableLimit, -}; - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const UncommonProcessTable = connector(UncommonProcessTableComponent); - -UncommonProcessTable.displayName = 'UncommonProcessTable'; - -const getUncommonColumns = (): UncommonProcessTableColumns => [ - { - name: i18n.NAME, - truncateText: false, - hideForMobile: false, - render: ({ node }) => - getRowItemDraggables({ - rowItems: node.process.name, - attrName: 'process.name', - idPrefix: `uncommon-process-table-${node._id}-processName`, - }), - width: '20%', - }, - { - align: 'right', - name: i18n.NUMBER_OF_HOSTS, - truncateText: false, - hideForMobile: false, - render: ({ node }) => <>{node.hosts != null ? node.hosts.length : getEmptyValue()}, - width: '8%', - }, - { - align: 'right', - name: i18n.NUMBER_OF_INSTANCES, - truncateText: false, - hideForMobile: false, - render: ({ node }) => defaultToEmptyTag(node.instances), - width: '8%', - }, - { - name: i18n.HOSTS, - truncateText: false, - hideForMobile: false, - render: ({ node }) => - getRowItemDraggables({ - rowItems: getHostNames(node), - attrName: 'host.name', - idPrefix: `uncommon-process-table-${node._id}-processHost`, - render: item => , - }), - width: '25%', - }, - { - name: i18n.LAST_COMMAND, - truncateText: false, - hideForMobile: false, - render: ({ node }) => - getRowItemDraggables({ - rowItems: node.process != null ? node.process.args : null, - attrName: 'process.args', - idPrefix: `uncommon-process-table-${node._id}-processArgs`, - displayCount: 1, // TODO: Change this back once we have improved the UI - }), - width: '25%', - }, - { - name: i18n.LAST_USER, - truncateText: false, - hideForMobile: false, - render: ({ node }) => - getRowItemDraggables({ - rowItems: node.user != null ? node.user.name : null, - attrName: 'user.name', - idPrefix: `uncommon-process-table-${node._id}-processUser`, - }), - }, -]; - -export const getHostNames = (node: UncommonProcessItem): string[] => { - if (node.hosts != null) { - return node.hosts - .filter(host => host.name != null && host.name[0] != null) - .map(host => (host.name != null && host.name[0] != null ? host.name[0] : '')); - } else { - return []; - } -}; - -export const getUncommonColumnsCurated = (pageType: HostsType): UncommonProcessTableColumns => { - const columns: UncommonProcessTableColumns = getUncommonColumns(); - if (pageType === HostsType.details) { - return [i18n.HOSTS, i18n.NUMBER_OF_HOSTS].reduce((acc, name) => { - acc.splice( - acc.findIndex(column => column.name === name), - 1 - ); - return acc; - }, columns); - } else { - return columns; - } -}; diff --git a/x-pack/plugins/siem/public/components/page/hosts/uncommon_process_table/mock.ts b/x-pack/plugins/siem/public/components/page/hosts/uncommon_process_table/mock.ts deleted file mode 100644 index bcd76706e30354..00000000000000 --- a/x-pack/plugins/siem/public/components/page/hosts/uncommon_process_table/mock.ts +++ /dev/null @@ -1,119 +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 { UncommonProcessesData } from '../../../../graphql/types'; - -export const mockData: { UncommonProcess: UncommonProcessesData } = { - UncommonProcess: { - totalCount: 5, - edges: [ - { - node: { - _id: 'cPsuhGcB0WOhS6qyTKC0', - process: { - title: ['Hello World'], - name: ['elrond.elstc.co'], - }, - hosts: [], - instances: 93, - user: { - id: ['0'], - name: ['root'], - }, - }, - cursor: { - value: '98966fa2013c396155c460d35c0902be', - }, - }, - { - node: { - _id: 'cPsuhGcB0WOhS6qyTKC0', - process: { - title: ['Hello World'], - name: ['elrond.elstc.co'], - }, - hosts: [{ id: ['host-id-1'], name: ['hello-world'] }], - instances: 93, - user: { - id: ['0'], - name: ['root'], - }, - }, - cursor: { - value: '98966fa2013c396155c460d35c0902be', - }, - }, - { - node: { - _id: 'KwQDiWcB0WOhS6qyXmrW', - process: { - title: ['Hello World'], - name: ['siem-kibana'], - }, - hosts: [ - { id: ['host-id-1'], name: ['hello-world'] }, - { id: ['host-id-2'], name: ['hello-world-2'] }, - ], - instances: 97, - user: { - id: ['1'], - name: ['Evan'], - }, - }, - cursor: { - value: 'aa7ca589f1b8220002f2fc61c64cfbf1', - }, - }, - { - node: { - _id: 'KwQDiWcB0WOhS6qyXmrW', - process: { - title: ['Hello World'], - name: ['siem-kibana'], - }, - hosts: [{ ip: ['127.0.0.1'] }], - instances: 97, - user: { - id: ['1'], - name: ['Evan'], - }, - }, - cursor: { - value: 'aa7ca589f1b8220002f2fc61c64cfbf1', - }, - }, - { - node: { - _id: 'KwQDiWcB0WOhS6qyXmrW', - process: { - title: ['Hello World'], - name: ['siem-kibana'], - }, - hosts: [ - { ip: ['127.0.0.1'] }, - { id: ['host-id-1'], name: ['hello-world'] }, - { ip: ['127.0.0.1'] }, - { id: ['host-id-2'], name: ['hello-world-2'] }, - { ip: ['127.0.0.1'] }, - ], - instances: 97, - user: { - id: ['1'], - name: ['Evan'], - }, - }, - cursor: { - value: 'aa7ca589f1b8220002f2fc61c64cfbf1', - }, - }, - ], - pageInfo: { - activePage: 1, - fakeTotalCount: 50, - showMorePagesIndicator: true, - }, - }, -}; diff --git a/x-pack/plugins/siem/public/components/page/network/flow_target_select_connected/index.test.tsx b/x-pack/plugins/siem/public/components/page/network/flow_target_select_connected/index.test.tsx deleted file mode 100644 index e71be5a51e5053..00000000000000 --- a/x-pack/plugins/siem/public/components/page/network/flow_target_select_connected/index.test.tsx +++ /dev/null @@ -1,42 +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 { mount } from 'enzyme'; -import React from 'react'; -import { MemoryRouter } from 'react-router-dom'; - -import { TestProviders } from '../../../../mock'; -import { FlowTargetSelectConnectedComponent } from './index'; -import { FlowTarget } from '../../../../graphql/types'; - -describe('Flow Target Select Connected', () => { - test('renders correctly against snapshot flowTarget source', () => { - const wrapper = mount( - - - - - - ); - expect(wrapper.find('Memo(FlowTargetSelectComponent)').prop('selectedTarget')).toEqual( - FlowTarget.source - ); - }); - - test('renders correctly against snapshot flowTarget destination', () => { - const wrapper = mount( - - - - - - ); - - expect(wrapper.find('Memo(FlowTargetSelectComponent)').prop('selectedTarget')).toEqual( - FlowTarget.destination - ); - }); -}); diff --git a/x-pack/plugins/siem/public/components/page/network/flow_target_select_connected/index.tsx b/x-pack/plugins/siem/public/components/page/network/flow_target_select_connected/index.tsx deleted file mode 100644 index 2651c31e0a2c9e..00000000000000 --- a/x-pack/plugins/siem/public/components/page/network/flow_target_select_connected/index.tsx +++ /dev/null @@ -1,65 +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 { Location } from 'history'; -import { EuiFlexItem } from '@elastic/eui'; -import React, { useCallback } from 'react'; -import { useHistory, useLocation } from 'react-router-dom'; -import styled from 'styled-components'; - -import { FlowDirection, FlowTarget } from '../../../../graphql/types'; -import * as i18nIp from '../ip_overview/translations'; - -import { FlowTargetSelect } from '../../../flow_controls/flow_target_select'; -import { IpOverviewId } from '../../../field_renderers/field_renderers'; - -const SelectTypeItem = styled(EuiFlexItem)` - min-width: 180px; -`; - -SelectTypeItem.displayName = 'SelectTypeItem'; - -interface Props { - flowTarget: FlowTarget; -} - -const getUpdatedFlowTargetPath = ( - location: Location, - currentFlowTarget: FlowTarget, - newFlowTarget: FlowTarget -) => { - const newPathame = location.pathname.replace(currentFlowTarget, newFlowTarget); - - return `${newPathame}${location.search}`; -}; - -export const FlowTargetSelectConnectedComponent: React.FC = ({ flowTarget }) => { - const history = useHistory(); - const location = useLocation(); - - const updateIpDetailsFlowTarget = useCallback( - (newFlowTarget: FlowTarget) => { - const newPath = getUpdatedFlowTargetPath(location, flowTarget, newFlowTarget); - history.push(newPath); - }, - [history, location, flowTarget] - ); - - return ( - - - - ); -}; - -export const FlowTargetSelectConnected = React.memo(FlowTargetSelectConnectedComponent); diff --git a/x-pack/plugins/siem/public/components/page/network/index.tsx b/x-pack/plugins/siem/public/components/page/network/index.tsx deleted file mode 100644 index 1f502635a8de4c..00000000000000 --- a/x-pack/plugins/siem/public/components/page/network/index.tsx +++ /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. - */ - -export { IpOverview } from './ip_overview'; -export { KpiNetworkComponent } from './kpi_network'; -export { NetworkDnsTable } from './network_dns_table'; -export { NetworkTopCountriesTable } from './network_top_countries_table'; -export { NetworkTopNFlowTable } from './network_top_n_flow_table'; -export { NetworkHttpTable } from './network_http_table'; diff --git a/x-pack/plugins/siem/public/components/page/network/ip_overview/index.test.tsx b/x-pack/plugins/siem/public/components/page/network/ip_overview/index.test.tsx deleted file mode 100644 index 3038d7f41c632e..00000000000000 --- a/x-pack/plugins/siem/public/components/page/network/ip_overview/index.test.tsx +++ /dev/null @@ -1,57 +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 { shallow } from 'enzyme'; -import React from 'react'; -import { ActionCreator } from 'typescript-fsa'; - -import { FlowTarget } from '../../../../graphql/types'; -import { apolloClientObservable, mockGlobalState, TestProviders } from '../../../../mock'; -import { createStore, networkModel, State } from '../../../../store'; - -import { IpOverview } from './index'; -import { mockData } from './mock'; -import { mockAnomalies } from '../../../ml/mock'; -import { NarrowDateRange } from '../../../ml/types'; - -describe('IP Overview Component', () => { - const state: State = mockGlobalState; - - let store = createStore(state, apolloClientObservable); - - beforeEach(() => { - store = createStore(state, apolloClientObservable); - }); - - describe('rendering', () => { - const mockProps = { - anomaliesData: mockAnomalies, - data: mockData.IpOverview, - endDate: new Date('2019-06-18T06:00:00.000Z').valueOf(), - flowTarget: FlowTarget.source, - loading: false, - id: 'ipOverview', - ip: '10.10.10.10', - isLoadingAnomaliesData: false, - narrowDateRange: (jest.fn() as unknown) as NarrowDateRange, - startDate: new Date('2019-06-15T06:00:00.000Z').valueOf(), - type: networkModel.NetworkType.details, - updateFlowTargetAction: (jest.fn() as unknown) as ActionCreator<{ - flowTarget: FlowTarget; - }>, - }; - - test('it renders the default IP Overview', () => { - const wrapper = shallow( - - - - ); - - expect(wrapper.find('IpOverview')).toMatchSnapshot(); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/page/network/ip_overview/index.tsx b/x-pack/plugins/siem/public/components/page/network/ip_overview/index.tsx deleted file mode 100644 index 56b59ca97156f6..00000000000000 --- a/x-pack/plugins/siem/public/components/page/network/ip_overview/index.tsx +++ /dev/null @@ -1,166 +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 { EuiFlexItem } from '@elastic/eui'; -import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; -import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; -import React from 'react'; - -import { DEFAULT_DARK_MODE } from '../../../../../common/constants'; -import { DescriptionList } from '../../../../../common/utility_types'; -import { useUiSetting$ } from '../../../../lib/kibana'; -import { FlowTarget, IpOverviewData, Overview } from '../../../../graphql/types'; -import { networkModel } from '../../../../store'; -import { getEmptyTagValue } from '../../../empty_value'; - -import { - autonomousSystemRenderer, - dateRenderer, - hostIdRenderer, - hostNameRenderer, - locationRenderer, - reputationRenderer, - whoisRenderer, -} from '../../../field_renderers/field_renderers'; -import * as i18n from './translations'; -import { DescriptionListStyled, OverviewWrapper } from '../../index'; -import { Loader } from '../../../loader'; -import { Anomalies, NarrowDateRange } from '../../../ml/types'; -import { AnomalyScores } from '../../../ml/score/anomaly_scores'; -import { useMlCapabilities } from '../../../ml_popover/hooks/use_ml_capabilities'; -import { hasMlUserPermissions } from '../../../ml/permissions/has_ml_user_permissions'; -import { InspectButton, InspectButtonContainer } from '../../../inspect'; - -interface OwnProps { - data: IpOverviewData; - flowTarget: FlowTarget; - id: string; - ip: string; - loading: boolean; - isLoadingAnomaliesData: boolean; - anomaliesData: Anomalies | null; - startDate: number; - endDate: number; - type: networkModel.NetworkType; - narrowDateRange: NarrowDateRange; -} - -export type IpOverviewProps = OwnProps; - -const getDescriptionList = (descriptionList: DescriptionList[], key: number) => { - return ( - - - - ); -}; - -export const IpOverview = React.memo( - ({ - id, - ip, - data, - loading, - flowTarget, - startDate, - endDate, - isLoadingAnomaliesData, - anomaliesData, - narrowDateRange, - }) => { - const capabilities = useMlCapabilities(); - const userPermissions = hasMlUserPermissions(capabilities); - const [darkMode] = useUiSetting$(DEFAULT_DARK_MODE); - const typeData: Overview = data[flowTarget]!; - const column: DescriptionList[] = [ - { - title: i18n.LOCATION, - description: locationRenderer( - [`${flowTarget}.geo.city_name`, `${flowTarget}.geo.region_name`], - data - ), - }, - { - title: i18n.AUTONOMOUS_SYSTEM, - description: typeData - ? autonomousSystemRenderer(typeData.autonomousSystem, flowTarget) - : getEmptyTagValue(), - }, - ]; - - const firstColumn: DescriptionList[] = userPermissions - ? [ - ...column, - { - title: i18n.MAX_ANOMALY_SCORE_BY_JOB, - description: ( - - ), - }, - ] - : column; - - const descriptionLists: Readonly = [ - firstColumn, - [ - { - title: i18n.FIRST_SEEN, - description: typeData ? dateRenderer(typeData.firstSeen) : getEmptyTagValue(), - }, - { - title: i18n.LAST_SEEN, - description: typeData ? dateRenderer(typeData.lastSeen) : getEmptyTagValue(), - }, - ], - [ - { - title: i18n.HOST_ID, - description: typeData - ? hostIdRenderer({ host: data.host, ipFilter: ip }) - : getEmptyTagValue(), - }, - { - title: i18n.HOST_NAME, - description: typeData ? hostNameRenderer(data.host, ip) : getEmptyTagValue(), - }, - ], - [ - { title: i18n.WHOIS, description: whoisRenderer(ip) }, - { title: i18n.REPUTATION, description: reputationRenderer(ip) }, - ], - ]; - - return ( - - - - - {descriptionLists.map((descriptionList, index) => - getDescriptionList(descriptionList, index) - )} - - {loading && ( - - )} - - - ); - } -); - -IpOverview.displayName = 'IpOverview'; diff --git a/x-pack/plugins/siem/public/components/page/network/ip_overview/mock.ts b/x-pack/plugins/siem/public/components/page/network/ip_overview/mock.ts deleted file mode 100644 index aaacdae70aef79..00000000000000 --- a/x-pack/plugins/siem/public/components/page/network/ip_overview/mock.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 { IpOverviewData } from '../../../../graphql/types'; - -export const mockData: Readonly> = { - complete: { - source: { - firstSeen: '2019-02-07T17:19:41.636Z', - lastSeen: '2019-02-07T17:19:41.636Z', - autonomousSystem: { organization: { name: 'Test Org' }, number: 12345 }, - geo: { - continent_name: ['North America'], - city_name: ['New York'], - country_iso_code: ['US'], - country_name: null, - location: { - lat: [40.7214], - lon: [-74.0052], - }, - region_iso_code: ['US-NY'], - region_name: ['New York'], - }, - }, - destination: { - firstSeen: '2019-02-07T17:19:41.648Z', - lastSeen: '2019-02-07T17:19:41.648Z', - autonomousSystem: { organization: { name: 'Test Org' }, number: 12345 }, - geo: { - continent_name: ['North America'], - city_name: ['New York'], - country_iso_code: ['US'], - country_name: null, - location: { - lat: [40.7214], - lon: [-74.0052], - }, - region_iso_code: ['US-NY'], - region_name: ['New York'], - }, - }, - host: { - os: { - kernel: ['4.14.50-v7+'], - name: ['Raspbian GNU/Linux'], - family: [''], - version: ['9 (stretch)'], - platform: ['raspbian'], - }, - name: ['raspberrypi'], - id: ['b19a781f683541a7a25ee345133aa399'], - ip: ['10.10.10.10'], - architecture: ['armv7l'], - }, - }, -}; diff --git a/x-pack/plugins/siem/public/components/page/network/kpi_network/index.test.tsx b/x-pack/plugins/siem/public/components/page/network/kpi_network/index.test.tsx deleted file mode 100644 index 48d3b25f59e4a2..00000000000000 --- a/x-pack/plugins/siem/public/components/page/network/kpi_network/index.test.tsx +++ /dev/null @@ -1,64 +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 { shallow } from 'enzyme'; -import React from 'react'; -import { Provider as ReduxStoreProvider } from 'react-redux'; - -import { apolloClientObservable, mockGlobalState } from '../../../../mock'; -import { createStore, State } from '../../../../store'; - -import { KpiNetworkComponent } from '.'; -import { mockData } from './mock'; - -describe('KpiNetwork Component', () => { - const state: State = mockGlobalState; - const from = new Date('2019-06-15T06:00:00.000Z').valueOf(); - const to = new Date('2019-06-18T06:00:00.000Z').valueOf(); - const narrowDateRange = jest.fn(); - - let store = createStore(state, apolloClientObservable); - - beforeEach(() => { - store = createStore(state, apolloClientObservable); - }); - - describe('rendering', () => { - test('it renders loading icons', () => { - const wrapper = shallow( - - - - ); - - expect(wrapper.find('KpiNetworkComponent')).toMatchSnapshot(); - }); - - test('it renders the default widget', () => { - const wrapper = shallow( - - - - ); - - expect(wrapper.find('KpiNetworkComponent')).toMatchSnapshot(); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/page/network/kpi_network/index.tsx b/x-pack/plugins/siem/public/components/page/network/kpi_network/index.tsx deleted file mode 100644 index e81c65fbc6afb7..00000000000000 --- a/x-pack/plugins/siem/public/components/page/network/kpi_network/index.tsx +++ /dev/null @@ -1,199 +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 { - EuiFlexItem, - EuiLoadingSpinner, - EuiFlexGroup, - EuiSpacer, - euiPaletteColorBlind, -} from '@elastic/eui'; -import styled from 'styled-components'; -import { chunk as _chunk } from 'lodash/fp'; - -import { - StatItemsComponent, - StatItemsProps, - useKpiMatrixStatus, - StatItems, -} from '../../../../components/stat_items'; -import { KpiNetworkData } from '../../../../graphql/types'; - -import * as i18n from './translations'; -import { UpdateDateRange } from '../../../charts/common'; - -const kipsPerRow = 2; -const kpiWidgetHeight = 228; - -const euiVisColorPalette = euiPaletteColorBlind(); -const euiColorVis1 = euiVisColorPalette[1]; -const euiColorVis2 = euiVisColorPalette[2]; -const euiColorVis3 = euiVisColorPalette[3]; - -interface KpiNetworkProps { - data: KpiNetworkData; - from: number; - id: string; - loading: boolean; - to: number; - narrowDateRange: UpdateDateRange; -} - -export const fieldTitleChartMapping: Readonly = [ - { - key: 'UniqueIps', - index: 2, - fields: [ - { - key: 'uniqueSourcePrivateIps', - value: null, - name: i18n.SOURCE_CHART_LABEL, - description: i18n.SOURCE_UNIT_LABEL, - color: euiColorVis2, - icon: 'visMapCoordinate', - }, - { - key: 'uniqueDestinationPrivateIps', - value: null, - name: i18n.DESTINATION_CHART_LABEL, - description: i18n.DESTINATION_UNIT_LABEL, - color: euiColorVis3, - icon: 'visMapCoordinate', - }, - ], - description: i18n.UNIQUE_PRIVATE_IPS, - enableAreaChart: true, - enableBarChart: true, - grow: 2, - }, -]; - -const fieldTitleMatrixMapping: Readonly = [ - { - key: 'networkEvents', - index: 0, - fields: [ - { - key: 'networkEvents', - value: null, - color: euiColorVis1, - }, - ], - description: i18n.NETWORK_EVENTS, - grow: 1, - }, - { - key: 'dnsQueries', - index: 1, - fields: [ - { - key: 'dnsQueries', - value: null, - }, - ], - description: i18n.DNS_QUERIES, - }, - { - key: 'uniqueFlowId', - index: 3, - fields: [ - { - key: 'uniqueFlowId', - value: null, - }, - ], - description: i18n.UNIQUE_FLOW_IDS, - }, - { - key: 'tlsHandshakes', - index: 4, - fields: [ - { - key: 'tlsHandshakes', - value: null, - }, - ], - description: i18n.TLS_HANDSHAKES, - }, -]; - -const FlexGroup = styled(EuiFlexGroup)` - min-height: ${kpiWidgetHeight}px; -`; - -FlexGroup.displayName = 'FlexGroup'; - -export const KpiNetworkBaseComponent = React.memo<{ - fieldsMapping: Readonly; - data: KpiNetworkData; - id: string; - from: number; - to: number; - narrowDateRange: UpdateDateRange; -}>(({ fieldsMapping, data, id, from, to, narrowDateRange }) => { - const statItemsProps: StatItemsProps[] = useKpiMatrixStatus( - fieldsMapping, - data, - id, - from, - to, - narrowDateRange - ); - - return ( - - {statItemsProps.map((mappedStatItemProps, idx) => { - return ; - })} - - ); -}); - -KpiNetworkBaseComponent.displayName = 'KpiNetworkBaseComponent'; - -export const KpiNetworkComponent = React.memo( - ({ data, from, id, loading, to, narrowDateRange }) => { - return loading ? ( - - - - - - ) : ( - - - {_chunk(kipsPerRow, fieldTitleMatrixMapping).map((mappingsPerLine, idx) => ( - - {idx % kipsPerRow === 1 && } - - - ))} - - - - - - ); - } -); - -KpiNetworkComponent.displayName = 'KpiNetworkComponent'; diff --git a/x-pack/plugins/siem/public/components/page/network/kpi_network/mock.ts b/x-pack/plugins/siem/public/components/page/network/kpi_network/mock.ts deleted file mode 100644 index 4edaf76bb48202..00000000000000 --- a/x-pack/plugins/siem/public/components/page/network/kpi_network/mock.ts +++ /dev/null @@ -1,230 +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 { KpiNetworkData } from '../../../../graphql/types'; -import { StatItems } from '../../../stat_items'; - -export const mockNarrowDateRange = jest.fn(); - -export const mockData: { KpiNetwork: KpiNetworkData } = { - KpiNetwork: { - networkEvents: 16, - uniqueFlowId: 10277307, - uniqueSourcePrivateIps: 383, - uniqueSourcePrivateIpsHistogram: [ - { - x: new Date('2019-02-09T16:00:00.000Z').valueOf(), - y: 8, - }, - { - x: new Date('2019-02-09T19:00:00.000Z').valueOf(), - y: 0, - }, - ], - uniqueDestinationPrivateIps: 18, - uniqueDestinationPrivateIpsHistogram: [ - { - x: new Date('2019-02-09T16:00:00.000Z').valueOf(), - y: 8, - }, - { - x: new Date('2019-02-09T19:00:00.000Z').valueOf(), - y: 0, - }, - ], - dnsQueries: 278, - tlsHandshakes: 10000, - }, -}; - -const mockMappingItems: StatItems = { - key: 'UniqueIps', - index: 0, - fields: [ - { - key: 'uniqueSourcePrivateIps', - value: null, - name: 'Src.', - description: 'source', - color: '#D36086', - icon: 'visMapCoordinate', - }, - { - key: 'uniqueDestinationPrivateIps', - value: null, - name: 'Dest.', - description: 'destination', - color: '#9170B8', - icon: 'visMapCoordinate', - }, - ], - description: 'Unique private IPs', - enableAreaChart: true, - enableBarChart: true, - grow: 2, -}; - -export const mockNoChartMappings: Readonly = [ - { - ...mockMappingItems, - enableAreaChart: false, - enableBarChart: false, - }, -]; - -export const mockDisableChartsInitialData = { - fields: [ - { - key: 'uniqueSourcePrivateIps', - value: undefined, - name: 'Src.', - description: 'source', - color: '#D36086', - icon: 'visMapCoordinate', - }, - { - key: 'uniqueDestinationPrivateIps', - value: undefined, - name: 'Dest.', - description: 'destination', - color: '#9170B8', - icon: 'visMapCoordinate', - }, - ], - description: 'Unique private IPs', - enableAreaChart: false, - enableBarChart: false, - grow: 2, - areaChart: undefined, - barChart: undefined, -}; - -export const mockEnableChartsInitialData = { - fields: [ - { - key: 'uniqueSourcePrivateIps', - value: undefined, - name: 'Src.', - description: 'source', - color: '#D36086', - icon: 'visMapCoordinate', - }, - { - key: 'uniqueDestinationPrivateIps', - value: undefined, - name: 'Dest.', - description: 'destination', - color: '#9170B8', - icon: 'visMapCoordinate', - }, - ], - description: 'Unique private IPs', - enableAreaChart: true, - enableBarChart: true, - grow: 2, - areaChart: [], - barChart: [ - { - color: '#D36086', - key: 'uniqueSourcePrivateIps', - value: [ - { - g: 'uniqueSourcePrivateIps', - x: 'Src.', - y: null, - }, - ], - }, - { - color: '#9170B8', - key: 'uniqueDestinationPrivateIps', - value: [ - { - g: 'uniqueDestinationPrivateIps', - x: 'Dest.', - y: null, - }, - ], - }, - ], -}; - -export const mockEnableChartsData = { - areaChart: [ - { - key: 'uniqueSourcePrivateIpsHistogram', - value: [ - { x: new Date('2019-02-09T16:00:00.000Z').valueOf(), y: 8 }, - { - x: new Date('2019-02-09T19:00:00.000Z').valueOf(), - y: 0, - }, - ], - name: 'Src.', - description: 'source', - color: '#D36086', - icon: 'visMapCoordinate', - }, - { - key: 'uniqueDestinationPrivateIpsHistogram', - value: [ - { x: new Date('2019-02-09T16:00:00.000Z').valueOf(), y: 8 }, - { x: new Date('2019-02-09T19:00:00.000Z').valueOf(), y: 0 }, - ], - name: 'Dest.', - description: 'destination', - color: '#9170B8', - icon: 'visMapCoordinate', - }, - ], - barChart: [ - { - key: 'uniqueSourcePrivateIps', - color: '#D36086', - value: [ - { - x: 'Src.', - y: 383, - g: 'uniqueSourcePrivateIps', - y0: 0, - }, - ], - }, - { - key: 'uniqueDestinationPrivateIps', - color: '#9170B8', - value: [{ x: 'Dest.', y: 18, g: 'uniqueDestinationPrivateIps', y0: 0 }], - }, - ], - description: 'Unique private IPs', - enableAreaChart: true, - enableBarChart: true, - fields: [ - { - key: 'uniqueSourcePrivateIps', - value: 383, - name: 'Src.', - description: 'source', - color: '#D36086', - icon: 'visMapCoordinate', - }, - { - key: 'uniqueDestinationPrivateIps', - value: 18, - name: 'Dest.', - description: 'destination', - color: '#9170B8', - icon: 'visMapCoordinate', - }, - ], - from: 1560578400000, - grow: 2, - id: 'statItem', - index: 2, - statKey: 'UniqueIps', - to: 1560837600000, - narrowDateRange: mockNarrowDateRange, -}; diff --git a/x-pack/plugins/siem/public/components/page/network/network_dns_table/columns.tsx b/x-pack/plugins/siem/public/components/page/network/network_dns_table/columns.tsx deleted file mode 100644 index 83a902d7bbde4a..00000000000000 --- a/x-pack/plugins/siem/public/components/page/network/network_dns_table/columns.tsx +++ /dev/null @@ -1,130 +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 numeral from '@elastic/numeral'; -import React from 'react'; - -import { NetworkDnsFields, NetworkDnsItem } from '../../../../graphql/types'; -import { DragEffects, DraggableWrapper } from '../../../drag_and_drop/draggable_wrapper'; -import { escapeDataProviderId } from '../../../drag_and_drop/helpers'; -import { defaultToEmptyTag, getEmptyTagValue } from '../../../empty_value'; -import { Columns } from '../../../paginated_table'; -import { IS_OPERATOR } from '../../../timeline/data_providers/data_provider'; -import { PreferenceFormattedBytes } from '../../../formatted_bytes'; -import { Provider } from '../../../timeline/data_providers/provider'; - -import * as i18n from './translations'; -export type NetworkDnsColumns = [ - Columns, - Columns, - Columns, - Columns, - Columns -]; - -export const getNetworkDnsColumns = (): NetworkDnsColumns => [ - { - field: `node.${NetworkDnsFields.dnsName}`, - name: i18n.REGISTERED_DOMAIN, - truncateText: false, - hideForMobile: false, - sortable: true, - render: dnsName => { - if (dnsName != null) { - const id = escapeDataProviderId(`networkDns-table--name-${dnsName}`); - return ( - - snapshot.isDragging ? ( - - - - ) : ( - defaultToEmptyTag(dnsName) - ) - } - /> - ); - } else { - return getEmptyTagValue(); - } - }, - }, - { - align: 'right', - field: `node.${NetworkDnsFields.queryCount}`, - name: i18n.TOTAL_QUERIES, - sortable: true, - truncateText: false, - hideForMobile: false, - render: queryCount => { - if (queryCount != null) { - return numeral(queryCount).format('0'); - } else { - return getEmptyTagValue(); - } - }, - }, - { - align: 'right', - field: `node.${NetworkDnsFields.uniqueDomains}`, - name: i18n.UNIQUE_DOMAINS, - sortable: true, - truncateText: false, - hideForMobile: false, - render: uniqueDomains => { - if (uniqueDomains != null) { - return numeral(uniqueDomains).format('0'); - } else { - return getEmptyTagValue(); - } - }, - }, - { - align: 'right', - field: `node.${NetworkDnsFields.dnsBytesIn}`, - name: i18n.DNS_BYTES_IN, - sortable: true, - truncateText: false, - hideForMobile: false, - render: dnsBytesIn => { - if (dnsBytesIn != null) { - return ; - } else { - return getEmptyTagValue(); - } - }, - }, - { - align: 'right', - field: `node.${NetworkDnsFields.dnsBytesOut}`, - name: i18n.DNS_BYTES_OUT, - sortable: true, - truncateText: false, - hideForMobile: false, - render: dnsBytesOut => { - if (dnsBytesOut != null) { - return ; - } else { - return getEmptyTagValue(); - } - }, - }, -]; diff --git a/x-pack/plugins/siem/public/components/page/network/network_dns_table/index.test.tsx b/x-pack/plugins/siem/public/components/page/network/network_dns_table/index.test.tsx deleted file mode 100644 index e425057dd0f75f..00000000000000 --- a/x-pack/plugins/siem/public/components/page/network/network_dns_table/index.test.tsx +++ /dev/null @@ -1,104 +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 { shallow } from 'enzyme'; -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { MockedProvider } from 'react-apollo/test-utils'; -import { Provider as ReduxStoreProvider } from 'react-redux'; - -import { apolloClientObservable, mockGlobalState, TestProviders } from '../../../../mock'; -import { createStore, networkModel, State } from '../../../../store'; -import { useMountAppended } from '../../../../utils/use_mount_appended'; - -import { NetworkDnsTable } from '.'; -import { mockData } from './mock'; - -describe('NetworkTopNFlow Table Component', () => { - const loadPage = jest.fn(); - const state: State = mockGlobalState; - let store = createStore(state, apolloClientObservable); - const mount = useMountAppended(); - - beforeEach(() => { - store = createStore(state, apolloClientObservable); - }); - - describe('rendering', () => { - test('it renders the default NetworkTopNFlow table', () => { - const wrapper = shallow( - - - - ); - - expect(wrapper.find('Connect(NetworkDnsTableComponent)')).toMatchSnapshot(); - }); - }); - - describe('Sorting', () => { - test('when you click on the column header, you should show the sorting icon', () => { - const wrapper = mount( - - - - - - ); - - expect(store.getState().network.page.queries!.dns.sort).toEqual({ - direction: 'desc', - field: 'queryCount', - }); - - wrapper - .find('.euiTable thead tr th button') - .first() - .simulate('click'); - - wrapper.update(); - - expect(store.getState().network.page.queries!.dns.sort).toEqual({ - direction: 'asc', - field: 'dnsName', - }); - expect( - wrapper - .find('.euiTable thead tr th button') - .first() - .find('svg') - ).toBeTruthy(); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/page/network/network_dns_table/index.tsx b/x-pack/plugins/siem/public/components/page/network/network_dns_table/index.tsx deleted file mode 100644 index c1dd96c5c96f91..00000000000000 --- a/x-pack/plugins/siem/public/components/page/network/network_dns_table/index.tsx +++ /dev/null @@ -1,169 +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, { useCallback, useMemo } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import deepEqual from 'fast-deep-equal'; - -import { networkActions } from '../../../../store/actions'; -import { - Direction, - NetworkDnsEdges, - NetworkDnsFields, - NetworkDnsSortField, -} from '../../../../graphql/types'; -import { networkModel, networkSelectors, State } from '../../../../store'; -import { Criteria, ItemsPerRow, PaginatedTable } from '../../../paginated_table'; - -import { getNetworkDnsColumns } from './columns'; -import { IsPtrIncluded } from './is_ptr_included'; -import * as i18n from './translations'; - -const tableType = networkModel.NetworkTableType.dns; - -interface OwnProps { - data: NetworkDnsEdges[]; - fakeTotalCount: number; - id: string; - isInspect: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - showMorePagesIndicator: boolean; - totalCount: number; - type: networkModel.NetworkType; -} - -type NetworkDnsTableProps = OwnProps & PropsFromRedux; - -const rowItems: ItemsPerRow[] = [ - { - text: i18n.ROWS_5, - numberOfRow: 5, - }, - { - text: i18n.ROWS_10, - numberOfRow: 10, - }, -]; - -export const NetworkDnsTableComponent = React.memo( - ({ - activePage, - data, - fakeTotalCount, - id, - isInspect, - isPtrIncluded, - limit, - loading, - loadPage, - showMorePagesIndicator, - sort, - totalCount, - type, - updateNetworkTable, - }) => { - const updateLimitPagination = useCallback( - newLimit => - updateNetworkTable({ - networkType: type, - tableType, - updates: { limit: newLimit }, - }), - [type, updateNetworkTable] - ); - - const updateActivePage = useCallback( - newPage => - updateNetworkTable({ - networkType: type, - tableType, - updates: { activePage: newPage }, - }), - [type, updateNetworkTable] - ); - - const onChange = useCallback( - (criteria: Criteria) => { - if (criteria.sort != null) { - const newDnsSortField: NetworkDnsSortField = { - field: criteria.sort.field.split('.')[1] as NetworkDnsFields, - direction: criteria.sort.direction as Direction, - }; - if (!deepEqual(newDnsSortField, sort)) { - updateNetworkTable({ - networkType: type, - tableType, - updates: { sort: newDnsSortField }, - }); - } - } - }, - [sort, type, updateNetworkTable] - ); - - const onChangePtrIncluded = useCallback( - () => - updateNetworkTable({ - networkType: type, - tableType, - updates: { isPtrIncluded: !isPtrIncluded }, - }), - [type, updateNetworkTable, isPtrIncluded] - ); - - const columns = useMemo(() => getNetworkDnsColumns(), []); - - return ( - - } - headerTitle={i18n.TOP_DNS_DOMAINS} - headerTooltip={i18n.TOOLTIP} - headerUnit={i18n.UNIT(totalCount)} - id={id} - itemsPerRow={rowItems} - isInspect={isInspect} - limit={limit} - loading={loading} - loadPage={loadPage} - onChange={onChange} - pageOfItems={data} - showMorePagesIndicator={showMorePagesIndicator} - sorting={{ - field: `node.${sort.field}`, - direction: sort.direction, - }} - totalCount={fakeTotalCount} - updateActivePage={updateActivePage} - updateLimitPagination={updateLimitPagination} - /> - ); - } -); - -NetworkDnsTableComponent.displayName = 'NetworkDnsTableComponent'; - -const makeMapStateToProps = () => { - const getNetworkDnsSelector = networkSelectors.dnsSelector(); - const mapStateToProps = (state: State) => getNetworkDnsSelector(state); - return mapStateToProps; -}; - -const mapDispatchToProps = { - updateNetworkTable: networkActions.updateNetworkTable, -}; - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const NetworkDnsTable = connector(NetworkDnsTableComponent); diff --git a/x-pack/plugins/siem/public/components/page/network/network_dns_table/mock.ts b/x-pack/plugins/siem/public/components/page/network/network_dns_table/mock.ts deleted file mode 100644 index 281125edb9dc42..00000000000000 --- a/x-pack/plugins/siem/public/components/page/network/network_dns_table/mock.ts +++ /dev/null @@ -1,182 +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 { NetworkDnsData } from '../../../../graphql/types'; - -export const mockData: { NetworkDns: NetworkDnsData } = { - NetworkDns: { - totalCount: 80, - edges: [ - { - node: { - _id: 'nflxvideo.net', - dnsBytesIn: 2964, - dnsBytesOut: 12546, - dnsName: 'nflxvideo.net', - queryCount: 52, - uniqueDomains: 21, - }, - cursor: { value: 'nflxvideo.net' }, - }, - { - node: { - _id: 'apple.com', - dnsBytesIn: 2680, - dnsBytesOut: 31687, - dnsName: 'apple.com', - queryCount: 75, - uniqueDomains: 20, - }, - cursor: { value: 'apple.com' }, - }, - { - node: { - _id: 'googlevideo.com', - dnsBytesIn: 1890, - dnsBytesOut: 16292, - dnsName: 'googlevideo.com', - queryCount: 38, - uniqueDomains: 19, - }, - cursor: { value: 'googlevideo.com' }, - }, - { - node: { - _id: 'netflix.com', - dnsBytesIn: 60525, - dnsBytesOut: 218193, - dnsName: 'netflix.com', - queryCount: 1532, - uniqueDomains: 12, - }, - cursor: { value: 'netflix.com' }, - }, - { - node: { - _id: 'samsungcloudsolution.com', - dnsBytesIn: 1480, - dnsBytesOut: 11702, - dnsName: 'samsungcloudsolution.com', - queryCount: 31, - uniqueDomains: 8, - }, - cursor: { value: 'samsungcloudsolution.com' }, - }, - { - node: { - _id: 'doubleclick.net', - dnsBytesIn: 1505, - dnsBytesOut: 14372, - dnsName: 'doubleclick.net', - queryCount: 35, - uniqueDomains: 7, - }, - cursor: { value: 'doubleclick.net' }, - }, - { - node: { - _id: 'digitalocean.com', - dnsBytesIn: 2035, - dnsBytesOut: 4111, - dnsName: 'digitalocean.com', - queryCount: 35, - uniqueDomains: 6, - }, - cursor: { value: 'digitalocean.com' }, - }, - { - node: { - _id: 'samsungelectronics.com', - dnsBytesIn: 3916, - dnsBytesOut: 36592, - dnsName: 'samsungelectronics.com', - queryCount: 89, - uniqueDomains: 6, - }, - cursor: { value: 'samsungelectronics.com' }, - }, - { - node: { - _id: 'google.com', - dnsBytesIn: 896, - dnsBytesOut: 8072, - dnsName: 'google.com', - queryCount: 23, - uniqueDomains: 5, - }, - cursor: { value: 'google.com' }, - }, - { - node: { - _id: 'samsungcloudsolution.net', - dnsBytesIn: 1490, - dnsBytesOut: 11518, - dnsName: 'samsungcloudsolution.net', - queryCount: 30, - uniqueDomains: 5, - }, - cursor: { value: 'samsungcloudsolution.net' }, - }, - ], - pageInfo: { - activePage: 1, - fakeTotalCount: 50, - showMorePagesIndicator: true, - }, - histogram: [ - { - x: 'nflxvideo.net', - g: 'nflxvideo.net', - y: 12546, - }, - { - x: 'apple.com', - g: 'apple.com', - y: 31687, - }, - { - x: 'googlevideo.com', - g: 'googlevideo.com', - y: 16292, - }, - { - x: 'netflix.com', - g: 'netflix.com', - y: 218193, - }, - { - x: 'samsungcloudsolution.com', - g: 'samsungcloudsolution.com', - y: 11702, - }, - { - x: 'doubleclick.net', - g: 'doubleclick.net', - y: 14372, - }, - { - x: 'digitalocean.com', - g: 'digitalocean.com', - y: 4111, - }, - { - x: 'samsungelectronics.com', - g: 'samsungelectronics.com', - y: 36592, - }, - { - x: 'google.com', - g: 'google.com', - y: 8072, - }, - { - x: 'samsungcloudsolution.net', - g: 'samsungcloudsolution.net', - y: 11518, - }, - ], - }, -}; diff --git a/x-pack/plugins/siem/public/components/page/network/network_http_table/columns.tsx b/x-pack/plugins/siem/public/components/page/network/network_http_table/columns.tsx deleted file mode 100644 index bffc7235b6804c..00000000000000 --- a/x-pack/plugins/siem/public/components/page/network/network_http_table/columns.tsx +++ /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. - */ - -/* eslint-disable react/display-name */ - -import React from 'react'; -import numeral from '@elastic/numeral'; -import { NetworkHttpEdges, NetworkHttpFields, NetworkHttpItem } from '../../../../graphql/types'; -import { escapeDataProviderId } from '../../../drag_and_drop/helpers'; -import { getEmptyTagValue } from '../../../empty_value'; -import { IPDetailsLink } from '../../../links'; -import { Columns } from '../../../paginated_table'; - -import * as i18n from './translations'; -import { getRowItemDraggable, getRowItemDraggables } from '../../../tables/helpers'; -export type NetworkHttpColumns = [ - Columns, - Columns, - Columns, - Columns, - Columns, - Columns, - Columns -]; - -export const getNetworkHttpColumns = (tableId: string): NetworkHttpColumns => [ - { - name: i18n.METHOD, - render: ({ node: { methods, path } }) => { - return Array.isArray(methods) && methods.length > 0 - ? getRowItemDraggables({ - attrName: 'http.request.method', - displayCount: 3, - idPrefix: escapeDataProviderId(`${tableId}-table-methods-${path}`), - rowItems: methods, - }) - : getEmptyTagValue(); - }, - }, - { - name: i18n.DOMAIN, - render: ({ node: { domains, path } }) => - Array.isArray(domains) && domains.length > 0 - ? getRowItemDraggables({ - attrName: 'url.domain', - displayCount: 3, - idPrefix: escapeDataProviderId(`${tableId}-table-domains-${path}`), - rowItems: domains, - }) - : getEmptyTagValue(), - }, - { - field: `node.${NetworkHttpFields.path}`, - name: i18n.PATH, - render: path => - path != null - ? getRowItemDraggable({ - attrName: 'url.path', - idPrefix: escapeDataProviderId(`${tableId}-table-path-${path}`), - rowItem: path, - }) - : getEmptyTagValue(), - }, - { - name: i18n.STATUS, - render: ({ node: { statuses, path } }) => - Array.isArray(statuses) && statuses.length > 0 - ? getRowItemDraggables({ - attrName: 'http.response.status_code', - displayCount: 3, - idPrefix: escapeDataProviderId(`${tableId}-table-statuses-${path}`), - rowItems: statuses, - }) - : getEmptyTagValue(), - }, - { - name: i18n.LAST_HOST, - render: ({ node: { lastHost, path } }) => - lastHost != null - ? getRowItemDraggable({ - attrName: 'host.name', - idPrefix: escapeDataProviderId(`${tableId}-table-lastHost-${path}`), - rowItem: lastHost, - }) - : getEmptyTagValue(), - }, - { - name: i18n.LAST_SOURCE_IP, - render: ({ node: { lastSourceIp, path } }) => - lastSourceIp != null - ? getRowItemDraggable({ - attrName: 'source.ip', - idPrefix: escapeDataProviderId(`${tableId}-table-lastSourceIp-${path}`), - rowItem: lastSourceIp, - render: () => , - }) - : getEmptyTagValue(), - }, - { - align: 'right', - field: `node.${NetworkHttpFields.requestCount}`, - name: i18n.REQUESTS, - sortable: true, - render: requestCount => { - if (requestCount != null) { - return numeral(requestCount).format('0,000'); - } else { - return getEmptyTagValue(); - } - }, - }, -]; diff --git a/x-pack/plugins/siem/public/components/page/network/network_http_table/index.test.tsx b/x-pack/plugins/siem/public/components/page/network/network_http_table/index.test.tsx deleted file mode 100644 index c4596ada5c74db..00000000000000 --- a/x-pack/plugins/siem/public/components/page/network/network_http_table/index.test.tsx +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { shallow } from 'enzyme'; -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { MockedProvider } from 'react-apollo/test-utils'; -import { Provider as ReduxStoreProvider } from 'react-redux'; - -import { apolloClientObservable, mockGlobalState, TestProviders } from '../../../../mock'; -import { useMountAppended } from '../../../../utils/use_mount_appended'; -import { createStore, networkModel, State } from '../../../../store'; - -import { NetworkHttpTable } from '.'; -import { mockData } from './mock'; - -describe('NetworkHttp Table Component', () => { - const loadPage = jest.fn(); - const state: State = mockGlobalState; - - let store = createStore(state, apolloClientObservable); - const mount = useMountAppended(); - - beforeEach(() => { - store = createStore(state, apolloClientObservable); - }); - - describe('rendering', () => { - test('it renders the default NetworkHttp table', () => { - const wrapper = shallow( - - - - ); - - expect(wrapper.find('Connect(Component)')).toMatchSnapshot(); - }); - }); - - describe('Sorting', () => { - test('when you click on the column header, you should show the sorting icon', () => { - const wrapper = mount( - - - - - - ); - - expect(store.getState().network.page.queries!.http.sort).toEqual({ - direction: 'desc', - }); - - wrapper - .find('.euiTable thead tr th button') - .first() - .simulate('click'); - - wrapper.update(); - - expect(store.getState().network.page.queries!.http.sort).toEqual({ - direction: 'asc', - }); - expect( - wrapper - .find('.euiTable thead tr th button') - .first() - .find('svg') - ).toBeTruthy(); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/page/network/network_http_table/index.tsx b/x-pack/plugins/siem/public/components/page/network/network_http_table/index.tsx deleted file mode 100644 index 6a8b1308f1d36a..00000000000000 --- a/x-pack/plugins/siem/public/components/page/network/network_http_table/index.tsx +++ /dev/null @@ -1,145 +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, { useCallback, useMemo } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; - -import { networkActions } from '../../../../store/actions'; -import { Direction, NetworkHttpEdges, NetworkHttpFields } from '../../../../graphql/types'; -import { networkModel, networkSelectors, State } from '../../../../store'; -import { Criteria, ItemsPerRow, PaginatedTable } from '../../../paginated_table'; - -import { getNetworkHttpColumns } from './columns'; -import * as i18n from './translations'; - -interface OwnProps { - data: NetworkHttpEdges[]; - fakeTotalCount: number; - id: string; - isInspect: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - showMorePagesIndicator: boolean; - totalCount: number; - type: networkModel.NetworkType; -} - -type NetworkHttpTableProps = OwnProps & PropsFromRedux; - -const rowItems: ItemsPerRow[] = [ - { - text: i18n.ROWS_5, - numberOfRow: 5, - }, - { - text: i18n.ROWS_10, - numberOfRow: 10, - }, -]; - -const NetworkHttpTableComponent: React.FC = ({ - activePage, - data, - fakeTotalCount, - id, - isInspect, - limit, - loading, - loadPage, - showMorePagesIndicator, - sort, - totalCount, - type, - updateNetworkTable, -}) => { - const tableType = - type === networkModel.NetworkType.page - ? networkModel.NetworkTableType.http - : networkModel.IpDetailsTableType.http; - - const updateLimitPagination = useCallback( - newLimit => - updateNetworkTable({ - networkType: type, - tableType, - updates: { limit: newLimit }, - }), - [type, updateNetworkTable, tableType] - ); - - const updateActivePage = useCallback( - newPage => - updateNetworkTable({ - networkType: type, - tableType, - updates: { activePage: newPage }, - }), - [type, updateNetworkTable, tableType] - ); - - const onChange = useCallback( - (criteria: Criteria) => { - if (criteria.sort != null && criteria.sort.direction !== sort.direction) { - updateNetworkTable({ - networkType: type, - tableType, - updates: { - sort: { - direction: criteria.sort.direction as Direction, - }, - }, - }); - } - }, - [tableType, sort.direction, type, updateNetworkTable] - ); - - const sorting = { field: `node.${NetworkHttpFields.requestCount}`, direction: sort.direction }; - - const columns = useMemo(() => getNetworkHttpColumns(tableType), [tableType]); - - return ( - - ); -}; - -NetworkHttpTableComponent.displayName = 'NetworkHttpTableComponent'; - -const makeMapStateToProps = () => { - const getNetworkHttpSelector = networkSelectors.httpSelector(); - const mapStateToProps = (state: State, { type }: OwnProps) => getNetworkHttpSelector(state, type); - return mapStateToProps; -}; - -const mapDispatchToProps = { - updateNetworkTable: networkActions.updateNetworkTable, -}; - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const NetworkHttpTable = connector(React.memo(NetworkHttpTableComponent)); diff --git a/x-pack/plugins/siem/public/components/page/network/network_http_table/mock.ts b/x-pack/plugins/siem/public/components/page/network/network_http_table/mock.ts deleted file mode 100644 index ed9b00ba8e49e5..00000000000000 --- a/x-pack/plugins/siem/public/components/page/network/network_http_table/mock.ts +++ /dev/null @@ -1,88 +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 { NetworkHttpData } from '../../../../graphql/types'; - -export const mockData: { NetworkHttp: NetworkHttpData } = { - NetworkHttp: { - edges: [ - { - node: { - _id: '/computeMetadata/v1/instance/virtual-clock/drift-token', - domains: ['metadata.google.internal'], - methods: ['get'], - statuses: [], - lastHost: 'suricata-iowa', - lastSourceIp: '10.128.0.21', - path: '/computeMetadata/v1/instance/virtual-clock/drift-token', - requestCount: 1440, - }, - cursor: { - value: '/computeMetadata/v1/instance/virtual-clock/drift-token', - tiebreaker: null, - }, - }, - { - node: { - _id: '/computeMetadata/v1/', - domains: ['metadata.google.internal'], - methods: ['get'], - statuses: ['200'], - lastHost: 'suricata-iowa', - lastSourceIp: '10.128.0.21', - path: '/computeMetadata/v1/', - requestCount: 1020, - }, - cursor: { - value: '/computeMetadata/v1/', - tiebreaker: null, - }, - }, - { - node: { - _id: '/computeMetadata/v1/instance/network-interfaces/', - domains: ['metadata.google.internal'], - methods: ['get'], - statuses: [], - lastHost: 'suricata-iowa', - lastSourceIp: '10.128.0.21', - path: '/computeMetadata/v1/instance/network-interfaces/', - requestCount: 960, - }, - cursor: { - value: '/computeMetadata/v1/instance/network-interfaces/', - tiebreaker: null, - }, - }, - { - node: { - _id: '/downloads/ca_setup.exe', - domains: ['www.oxid.it'], - methods: ['get'], - statuses: ['200'], - lastHost: 'jessie', - lastSourceIp: '10.0.2.15', - path: '/downloads/ca_setup.exe', - requestCount: 3, - }, - cursor: { - value: '/downloads/ca_setup.exe', - tiebreaker: null, - }, - }, - ], - inspect: { - dsl: [''], - response: [''], - }, - pageInfo: { - activePage: 0, - fakeTotalCount: 4, - showMorePagesIndicator: false, - }, - totalCount: 4, - }, -}; diff --git a/x-pack/plugins/siem/public/components/page/network/network_top_countries_table/columns.tsx b/x-pack/plugins/siem/public/components/page/network/network_top_countries_table/columns.tsx deleted file mode 100644 index ae2723e0065091..00000000000000 --- a/x-pack/plugins/siem/public/components/page/network/network_top_countries_table/columns.tsx +++ /dev/null @@ -1,179 +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 { get } from 'lodash/fp'; -import numeral from '@elastic/numeral'; -import React from 'react'; -import { IIndexPattern } from 'src/plugins/data/public'; - -import { CountryFlagAndName } from '../../../source_destination/country_flag'; -import { - FlowTargetSourceDest, - NetworkTopCountriesEdges, - TopNetworkTablesEcsField, -} from '../../../../graphql/types'; -import { networkModel } from '../../../../store'; -import { DragEffects, DraggableWrapper } from '../../../drag_and_drop/draggable_wrapper'; -import { escapeDataProviderId } from '../../../drag_and_drop/helpers'; -import { getEmptyTagValue } from '../../../empty_value'; -import { Columns } from '../../../paginated_table'; -import { IS_OPERATOR } from '../../../timeline/data_providers/data_provider'; -import { Provider } from '../../../timeline/data_providers/provider'; -import * as i18n from './translations'; -import { PreferenceFormattedBytes } from '../../../formatted_bytes'; - -export type NetworkTopCountriesColumns = [ - Columns, - Columns, - Columns, - Columns, - Columns, - Columns -]; - -export type NetworkTopCountriesColumnsIpDetails = [ - Columns, - Columns, - Columns, - Columns, - Columns -]; - -export const getNetworkTopCountriesColumns = ( - indexPattern: IIndexPattern, - flowTarget: FlowTargetSourceDest, - type: networkModel.NetworkType, - tableId: string -): NetworkTopCountriesColumns => [ - { - name: i18n.COUNTRY, - render: ({ node }) => { - const geo = get(`${flowTarget}.country`, node); - const geoAttr = `${flowTarget}.geo.country_iso_code`; - const id = escapeDataProviderId(`${tableId}-table-${flowTarget}-country-${geo}`); - if (geo != null) { - return ( - - snapshot.isDragging ? ( - - - - ) : ( - <> - - - ) - } - /> - ); - } else { - return getEmptyTagValue(); - } - }, - width: '20%', - }, - { - align: 'right', - field: 'node.network.bytes_in', - name: i18n.BYTES_IN, - sortable: true, - render: bytes => { - if (bytes != null) { - return ; - } else { - return getEmptyTagValue(); - } - }, - }, - { - align: 'right', - field: 'node.network.bytes_out', - name: i18n.BYTES_OUT, - sortable: true, - render: bytes => { - if (bytes != null) { - return ; - } else { - return getEmptyTagValue(); - } - }, - }, - { - align: 'right', - field: `node.${flowTarget}.flows`, - name: i18n.FLOWS, - sortable: true, - render: flows => { - if (flows != null) { - return numeral(flows).format('0,000'); - } else { - return getEmptyTagValue(); - } - }, - }, - { - align: 'right', - field: `node.${flowTarget}.${flowTarget}_ips`, - name: flowTarget === FlowTargetSourceDest.source ? i18n.SOURCE_IPS : i18n.DESTINATION_IPS, - sortable: true, - render: ips => { - if (ips != null) { - return numeral(ips).format('0,000'); - } else { - return getEmptyTagValue(); - } - }, - }, - { - align: 'right', - field: `node.${flowTarget}.${getOppositeField(flowTarget)}_ips`, - name: - getOppositeField(flowTarget) === FlowTargetSourceDest.source - ? i18n.SOURCE_IPS - : i18n.DESTINATION_IPS, - sortable: true, - render: ips => { - if (ips != null) { - return numeral(ips).format('0,000'); - } else { - return getEmptyTagValue(); - } - }, - }, -]; - -export const getCountriesColumnsCurated = ( - indexPattern: IIndexPattern, - flowTarget: FlowTargetSourceDest, - type: networkModel.NetworkType, - tableId: string -): NetworkTopCountriesColumns | NetworkTopCountriesColumnsIpDetails => { - const columns = getNetworkTopCountriesColumns(indexPattern, flowTarget, type, tableId); - - // Columns to exclude from host details pages - if (type === networkModel.NetworkType.details) { - columns.pop(); - return columns; - } - - return columns; -}; - -const getOppositeField = (flowTarget: FlowTargetSourceDest): FlowTargetSourceDest => - flowTarget === FlowTargetSourceDest.source - ? FlowTargetSourceDest.destination - : FlowTargetSourceDest.source; diff --git a/x-pack/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx b/x-pack/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx deleted file mode 100644 index 764e440a5a4be8..00000000000000 --- a/x-pack/plugins/siem/public/components/page/network/network_top_countries_table/index.test.tsx +++ /dev/null @@ -1,151 +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 { shallow } from 'enzyme'; -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { MockedProvider } from 'react-apollo/test-utils'; -import { Provider as ReduxStoreProvider } from 'react-redux'; - -import { FlowTargetSourceDest } from '../../../../graphql/types'; -import { - apolloClientObservable, - mockGlobalState, - mockIndexPattern, - TestProviders, -} from '../../../../mock'; -import { useMountAppended } from '../../../../utils/use_mount_appended'; -import { createStore, networkModel, State } from '../../../../store'; - -import { NetworkTopCountriesTable } from '.'; -import { mockData } from './mock'; - -describe('NetworkTopCountries Table Component', () => { - const loadPage = jest.fn(); - const state: State = mockGlobalState; - const mount = useMountAppended(); - - let store = createStore(state, apolloClientObservable); - - beforeEach(() => { - store = createStore(state, apolloClientObservable); - }); - - describe('rendering', () => { - test('it renders the default NetworkTopCountries table', () => { - const wrapper = shallow( - - - - ); - - expect(wrapper.find('Connect(NetworkTopCountriesTableComponent)')).toMatchSnapshot(); - }); - test('it renders the IP Details NetworkTopCountries table', () => { - const wrapper = shallow( - - - - ); - - expect(wrapper.find('Connect(NetworkTopCountriesTableComponent)')).toMatchSnapshot(); - }); - }); - - describe('Sorting on Table', () => { - test('when you click on the column header, you should show the sorting icon', () => { - const wrapper = mount( - - - - - - ); - expect(store.getState().network.page.queries.topCountriesSource.sort).toEqual({ - direction: 'desc', - field: 'bytes_out', - }); - - wrapper - .find('.euiTable thead tr th button') - .at(1) - .simulate('click'); - - wrapper.update(); - - expect(store.getState().network.page.queries.topCountriesSource.sort).toEqual({ - direction: 'asc', - field: 'bytes_out', - }); - expect( - wrapper - .find('.euiTable thead tr th button') - .first() - .text() - ).toEqual('Bytes inClick to sort in ascending order'); - expect( - wrapper - .find('.euiTable thead tr th button') - .at(1) - .text() - ).toEqual('Bytes outClick to sort in descending order'); - expect( - wrapper - .find('.euiTable thead tr th button') - .at(1) - .find('svg') - ).toBeTruthy(); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/page/network/network_top_countries_table/index.tsx b/x-pack/plugins/siem/public/components/page/network/network_top_countries_table/index.tsx deleted file mode 100644 index 30f7d5ad823909..00000000000000 --- a/x-pack/plugins/siem/public/components/page/network/network_top_countries_table/index.tsx +++ /dev/null @@ -1,191 +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 { last } from 'lodash/fp'; -import React, { useCallback, useMemo } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import deepEqual from 'fast-deep-equal'; -import { IIndexPattern } from 'src/plugins/data/public'; - -import { networkActions } from '../../../../store/actions'; -import { - Direction, - FlowTargetSourceDest, - NetworkTopCountriesEdges, - NetworkTopTablesFields, - NetworkTopTablesSortField, -} from '../../../../graphql/types'; -import { networkModel, networkSelectors, State } from '../../../../store'; -import { Criteria, ItemsPerRow, PaginatedTable } from '../../../paginated_table'; - -import { getCountriesColumnsCurated } from './columns'; -import * as i18n from './translations'; - -interface OwnProps { - data: NetworkTopCountriesEdges[]; - fakeTotalCount: number; - flowTargeted: FlowTargetSourceDest; - id: string; - indexPattern: IIndexPattern; - isInspect: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - showMorePagesIndicator: boolean; - totalCount: number; - type: networkModel.NetworkType; -} - -type NetworkTopCountriesTableProps = OwnProps & PropsFromRedux; - -const rowItems: ItemsPerRow[] = [ - { - text: i18n.ROWS_5, - numberOfRow: 5, - }, - { - text: i18n.ROWS_10, - numberOfRow: 10, - }, -]; - -export const NetworkTopCountriesTableId = 'networkTopCountries-top-talkers'; - -const NetworkTopCountriesTableComponent = React.memo( - ({ - activePage, - data, - fakeTotalCount, - flowTargeted, - id, - indexPattern, - isInspect, - limit, - loading, - loadPage, - showMorePagesIndicator, - sort, - totalCount, - type, - updateNetworkTable, - }) => { - let tableType: networkModel.TopCountriesTableType; - const headerTitle: string = - flowTargeted === FlowTargetSourceDest.source - ? i18n.SOURCE_COUNTRIES - : i18n.DESTINATION_COUNTRIES; - - if (type === networkModel.NetworkType.page) { - tableType = - flowTargeted === FlowTargetSourceDest.source - ? networkModel.NetworkTableType.topCountriesSource - : networkModel.NetworkTableType.topCountriesDestination; - } else { - tableType = - flowTargeted === FlowTargetSourceDest.source - ? networkModel.IpDetailsTableType.topCountriesSource - : networkModel.IpDetailsTableType.topCountriesDestination; - } - - const field = - sort.field === NetworkTopTablesFields.bytes_out || - sort.field === NetworkTopTablesFields.bytes_in - ? `node.network.${sort.field}` - : `node.${flowTargeted}.${sort.field}`; - - const updateLimitPagination = useCallback( - newLimit => - updateNetworkTable({ - networkType: type, - tableType, - updates: { limit: newLimit }, - }), - [type, updateNetworkTable, tableType] - ); - - const updateActivePage = useCallback( - newPage => - updateNetworkTable({ - networkType: type, - tableType, - updates: { activePage: newPage }, - }), - [type, updateNetworkTable, tableType] - ); - - const onChange = useCallback( - (criteria: Criteria) => { - if (criteria.sort != null) { - const splitField = criteria.sort.field.split('.'); - const lastField = last(splitField); - const newSortDirection = - lastField !== sort.field ? Direction.desc : criteria.sort.direction; // sort by desc on init click - const newTopCountriesSort: NetworkTopTablesSortField = { - field: lastField as NetworkTopTablesFields, - direction: newSortDirection as Direction, - }; - if (!deepEqual(newTopCountriesSort, sort)) { - updateNetworkTable({ - networkType: type, - tableType, - updates: { - sort: newTopCountriesSort, - }, - }); - } - } - }, - [type, sort, tableType, updateNetworkTable] - ); - - const columns = useMemo( - () => - getCountriesColumnsCurated(indexPattern, flowTargeted, type, NetworkTopCountriesTableId), - [indexPattern, flowTargeted, type] - ); - - return ( - - ); - } -); - -NetworkTopCountriesTableComponent.displayName = 'NetworkTopCountriesTableComponent'; - -const makeMapStateToProps = () => { - const getTopCountriesSelector = networkSelectors.topCountriesSelector(); - return (state: State, { type, flowTargeted }: OwnProps) => - getTopCountriesSelector(state, type, flowTargeted); -}; - -const mapDispatchToProps = { - updateNetworkTable: networkActions.updateNetworkTable, -}; - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const NetworkTopCountriesTable = connector(NetworkTopCountriesTableComponent); diff --git a/x-pack/plugins/siem/public/components/page/network/network_top_countries_table/mock.ts b/x-pack/plugins/siem/public/components/page/network/network_top_countries_table/mock.ts deleted file mode 100644 index 42b933c7fba6d3..00000000000000 --- a/x-pack/plugins/siem/public/components/page/network/network_top_countries_table/mock.ts +++ /dev/null @@ -1,56 +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 { NetworkTopCountriesData } from '../../../../graphql/types'; - -export const mockData: { NetworkTopCountries: NetworkTopCountriesData } = { - NetworkTopCountries: { - totalCount: 524, - edges: [ - { - node: { - source: { - country: 'DE', - destination_ips: 12, - flows: 12345, - source_ips: 55, - }, - destination: null, - network: { - bytes_in: 3826633497, - bytes_out: 1083495734, - }, - }, - cursor: { - value: '8.8.8.8', - }, - }, - { - node: { - source: { - flows: 12345, - destination_ips: 12, - source_ips: 55, - country: 'US', - }, - destination: null, - network: { - bytes_in: 3826633497, - bytes_out: 1083495734, - }, - }, - cursor: { - value: '9.9.9.9', - }, - }, - ], - pageInfo: { - activePage: 1, - fakeTotalCount: 50, - showMorePagesIndicator: true, - }, - }, -}; diff --git a/x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/columns.tsx b/x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/columns.tsx deleted file mode 100644 index 3ed377c7ba4b09..00000000000000 --- a/x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/columns.tsx +++ /dev/null @@ -1,251 +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 { get } from 'lodash/fp'; -import numeral from '@elastic/numeral'; -import React from 'react'; - -import { CountryFlag } from '../../../source_destination/country_flag'; -import { - AutonomousSystemItem, - FlowTargetSourceDest, - NetworkTopNFlowEdges, - TopNetworkTablesEcsField, -} from '../../../../graphql/types'; -import { networkModel } from '../../../../store'; -import { DragEffects, DraggableWrapper } from '../../../drag_and_drop/draggable_wrapper'; -import { escapeDataProviderId } from '../../../drag_and_drop/helpers'; -import { getEmptyTagValue } from '../../../empty_value'; -import { IPDetailsLink } from '../../../links'; -import { Columns } from '../../../paginated_table'; -import { IS_OPERATOR } from '../../../timeline/data_providers/data_provider'; -import { Provider } from '../../../timeline/data_providers/provider'; -import * as i18n from './translations'; -import { getRowItemDraggable, getRowItemDraggables } from '../../../tables/helpers'; -import { PreferenceFormattedBytes } from '../../../formatted_bytes'; - -export type NetworkTopNFlowColumns = [ - Columns, - Columns, - Columns, - Columns, - Columns, - Columns, - Columns -]; - -export type NetworkTopNFlowColumnsIpDetails = [ - Columns, - Columns, - Columns, - Columns, - Columns, - Columns -]; - -export const getNetworkTopNFlowColumns = ( - flowTarget: FlowTargetSourceDest, - tableId: string -): NetworkTopNFlowColumns => [ - { - name: i18n.IP_TITLE, - render: ({ node }) => { - const ipAttr = `${flowTarget}.ip`; - const ip: string | null = get(ipAttr, node); - const geoAttr = `${flowTarget}.location.geo.country_iso_code[0]`; - const geoAttrName = `${flowTarget}.geo.country_iso_code`; - const geo: string | null = get(geoAttr, node); - const id = escapeDataProviderId(`${tableId}-table-${flowTarget}-ip-${ip}`); - - if (ip != null) { - return ( - <> - - snapshot.isDragging ? ( - - - - ) : ( - - ) - } - /> - - {geo && ( - - snapshot.isDragging ? ( - - - - ) : ( - <> - {' '} - {geo} - - ) - } - /> - )} - - ); - } else { - return getEmptyTagValue(); - } - }, - width: '20%', - }, - { - name: i18n.DOMAIN, - render: ({ node }) => { - const domainAttr = `${flowTarget}.domain`; - const ipAttr = `${flowTarget}.ip`; - const domains: string[] = get(domainAttr, node); - const ip: string | null = get(ipAttr, node); - - if (Array.isArray(domains) && domains.length > 0) { - const id = escapeDataProviderId(`${tableId}-table-${ip}`); - return getRowItemDraggables({ - rowItems: domains, - attrName: domainAttr, - idPrefix: id, - displayCount: 1, - }); - } else { - return getEmptyTagValue(); - } - }, - width: '20%', - }, - { - name: i18n.AUTONOMOUS_SYSTEM, - render: ({ node, cursor: { value: ipAddress } }) => { - const asAttr = `${flowTarget}.autonomous_system`; - const as: AutonomousSystemItem | null = get(asAttr, node); - if (as != null) { - const id = escapeDataProviderId(`${tableId}-table-${flowTarget}-ip-${ipAddress}`); - return ( - <> - {as.name && - getRowItemDraggable({ - rowItem: as.name, - attrName: `${flowTarget}.as.organization.name`, - idPrefix: `${id}-name`, - })} - - {as.number && ( - <> - {' '} - {getRowItemDraggable({ - rowItem: `${as.number}`, - attrName: `${flowTarget}.as.number`, - idPrefix: `${id}-number`, - })} - - )} - - ); - } else { - return getEmptyTagValue(); - } - }, - width: '20%', - }, - { - align: 'right', - field: 'node.network.bytes_in', - name: i18n.BYTES_IN, - sortable: true, - render: bytes => { - if (bytes != null) { - return ; - } else { - return getEmptyTagValue(); - } - }, - }, - { - align: 'right', - field: 'node.network.bytes_out', - name: i18n.BYTES_OUT, - sortable: true, - render: bytes => { - if (bytes != null) { - return ; - } else { - return getEmptyTagValue(); - } - }, - }, - { - align: 'right', - field: `node.${flowTarget}.flows`, - name: i18n.FLOWS, - sortable: true, - render: flows => { - if (flows != null) { - return numeral(flows).format('0,000'); - } else { - return getEmptyTagValue(); - } - }, - }, - { - align: 'right', - field: `node.${flowTarget}.${getOppositeField(flowTarget)}_ips`, - name: flowTarget === FlowTargetSourceDest.source ? i18n.DESTINATION_IPS : i18n.SOURCE_IPS, - sortable: true, - render: ips => { - if (ips != null) { - return numeral(ips).format('0,000'); - } else { - return getEmptyTagValue(); - } - }, - }, -]; - -export const getNFlowColumnsCurated = ( - flowTarget: FlowTargetSourceDest, - type: networkModel.NetworkType, - tableId: string -): NetworkTopNFlowColumns | NetworkTopNFlowColumnsIpDetails => { - const columns = getNetworkTopNFlowColumns(flowTarget, tableId); - - // Columns to exclude from host details pages - if (type === networkModel.NetworkType.details) { - columns.pop(); - return columns; - } - - return columns; -}; - -const getOppositeField = (flowTarget: FlowTargetSourceDest): FlowTargetSourceDest => - flowTarget === FlowTargetSourceDest.source - ? FlowTargetSourceDest.destination - : FlowTargetSourceDest.source; diff --git a/x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx b/x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx deleted file mode 100644 index 78e8b15005f431..00000000000000 --- a/x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/index.test.tsx +++ /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. - */ - -import { shallow } from 'enzyme'; -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { MockedProvider } from 'react-apollo/test-utils'; -import { Provider as ReduxStoreProvider } from 'react-redux'; - -import { FlowTargetSourceDest } from '../../../../graphql/types'; -import { apolloClientObservable, mockGlobalState, TestProviders } from '../../../../mock'; -import { useMountAppended } from '../../../../utils/use_mount_appended'; -import { createStore, networkModel, State } from '../../../../store'; - -import { NetworkTopNFlowTable } from '.'; -import { mockData } from './mock'; - -describe('NetworkTopNFlow Table Component', () => { - const loadPage = jest.fn(); - const state: State = mockGlobalState; - - let store = createStore(state, apolloClientObservable); - const mount = useMountAppended(); - - beforeEach(() => { - store = createStore(state, apolloClientObservable); - }); - - describe('rendering', () => { - test('it renders the default NetworkTopNFlow table on the Network page', () => { - const wrapper = shallow( - - - - ); - - expect(wrapper.find('Connect(Component)')).toMatchSnapshot(); - }); - - test('it renders the default NetworkTopNFlow table on the IP Details page', () => { - const wrapper = shallow( - - - - ); - - expect(wrapper.find('Connect(Component)')).toMatchSnapshot(); - }); - }); - - describe('Sorting on Table', () => { - test('when you click on the column header, you should show the sorting icon', () => { - const wrapper = mount( - - - - - - ); - expect(store.getState().network.page.queries.topNFlowSource.sort).toEqual({ - direction: 'desc', - field: 'bytes_out', - }); - - wrapper - .find('.euiTable thead tr th button') - .at(1) - .simulate('click'); - - wrapper.update(); - - expect(store.getState().network.page.queries.topNFlowSource.sort).toEqual({ - direction: 'asc', - field: 'bytes_out', - }); - expect( - wrapper - .find('.euiTable thead tr th button') - .first() - .text() - ).toEqual('Bytes inClick to sort in ascending order'); - expect( - wrapper - .find('.euiTable thead tr th button') - .at(1) - .text() - ).toEqual('Bytes outClick to sort in descending order'); - expect( - wrapper - .find('.euiTable thead tr th button') - .at(1) - .find('svg') - ).toBeTruthy(); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/index.tsx b/x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/index.tsx deleted file mode 100644 index 8e49db04a546cd..00000000000000 --- a/x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/index.tsx +++ /dev/null @@ -1,174 +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 { last } from 'lodash/fp'; -import React, { useCallback, useMemo } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import deepEqual from 'fast-deep-equal'; - -import { networkActions } from '../../../../store/actions'; -import { - Direction, - FlowTargetSourceDest, - NetworkTopNFlowEdges, - NetworkTopTablesFields, - NetworkTopTablesSortField, -} from '../../../../graphql/types'; -import { networkModel, networkSelectors, State } from '../../../../store'; -import { Criteria, ItemsPerRow, PaginatedTable } from '../../../paginated_table'; - -import { getNFlowColumnsCurated } from './columns'; -import * as i18n from './translations'; - -interface OwnProps { - data: NetworkTopNFlowEdges[]; - fakeTotalCount: number; - flowTargeted: FlowTargetSourceDest; - id: string; - isInspect: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - showMorePagesIndicator: boolean; - totalCount: number; - type: networkModel.NetworkType; -} - -type NetworkTopNFlowTableProps = OwnProps & PropsFromRedux; - -const rowItems: ItemsPerRow[] = [ - { - text: i18n.ROWS_5, - numberOfRow: 5, - }, - { - text: i18n.ROWS_10, - numberOfRow: 10, - }, -]; - -export const NetworkTopNFlowTableId = 'networkTopSourceFlow-top-talkers'; - -const NetworkTopNFlowTableComponent: React.FC = ({ - activePage, - data, - fakeTotalCount, - flowTargeted, - id, - isInspect, - limit, - loading, - loadPage, - showMorePagesIndicator, - sort, - totalCount, - type, - updateNetworkTable, -}) => { - const columns = useMemo( - () => getNFlowColumnsCurated(flowTargeted, type, NetworkTopNFlowTableId), - [flowTargeted, type] - ); - - let tableType: networkModel.TopNTableType; - const headerTitle: string = - flowTargeted === FlowTargetSourceDest.source ? i18n.SOURCE_IP : i18n.DESTINATION_IP; - - if (type === networkModel.NetworkType.page) { - tableType = - flowTargeted === FlowTargetSourceDest.source - ? networkModel.NetworkTableType.topNFlowSource - : networkModel.NetworkTableType.topNFlowDestination; - } else { - tableType = - flowTargeted === FlowTargetSourceDest.source - ? networkModel.IpDetailsTableType.topNFlowSource - : networkModel.IpDetailsTableType.topNFlowDestination; - } - - const onChange = useCallback( - (criteria: Criteria) => { - if (criteria.sort != null) { - const splitField = criteria.sort.field.split('.'); - const field = last(splitField); - const newSortDirection = field !== sort.field ? Direction.desc : criteria.sort.direction; // sort by desc on init click - const newTopNFlowSort: NetworkTopTablesSortField = { - field: field as NetworkTopTablesFields, - direction: newSortDirection as Direction, - }; - if (!deepEqual(newTopNFlowSort, sort)) { - updateNetworkTable({ - networkType: type, - tableType, - updates: { - sort: newTopNFlowSort, - }, - }); - } - } - }, - [sort, type, tableType, updateNetworkTable] - ); - - const field = - sort.field === NetworkTopTablesFields.bytes_out || - sort.field === NetworkTopTablesFields.bytes_in - ? `node.network.${sort.field}` - : `node.${flowTargeted}.${sort.field}`; - - const updateActivePage = useCallback( - newPage => - updateNetworkTable({ - networkType: type, - tableType, - updates: { activePage: newPage }, - }), - [updateNetworkTable, type, tableType] - ); - - const updateLimitPagination = useCallback( - newLimit => updateNetworkTable({ networkType: type, tableType, updates: { limit: newLimit } }), - [updateNetworkTable, type, tableType] - ); - - return ( - - ); -}; - -const makeMapStateToProps = () => { - const getTopNFlowSelector = networkSelectors.topNFlowSelector(); - return (state: State, { type, flowTargeted }: OwnProps) => - getTopNFlowSelector(state, type, flowTargeted); -}; - -const mapDispatchToProps = { - updateNetworkTable: networkActions.updateNetworkTable, -}; - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const NetworkTopNFlowTable = connector(React.memo(NetworkTopNFlowTableComponent)); diff --git a/x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/mock.ts b/x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/mock.ts deleted file mode 100644 index 9ef63bf6d31679..00000000000000 --- a/x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/mock.ts +++ /dev/null @@ -1,86 +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 { NetworkTopNFlowData, FlowTargetSourceDest } from '../../../../graphql/types'; - -export const mockData: { NetworkTopNFlow: NetworkTopNFlowData } = { - NetworkTopNFlow: { - totalCount: 524, - edges: [ - { - node: { - source: { - autonomous_system: { - name: 'Google, Inc', - number: 15169, - }, - domain: ['test.domain.com'], - flows: 12345, - destination_ips: 12, - ip: '8.8.8.8', - location: { - geo: { - continent_name: ['North America'], - country_name: null, - country_iso_code: ['US'], - city_name: ['Mountain View'], - region_iso_code: ['US-CA'], - region_name: ['California'], - }, - flowTarget: FlowTargetSourceDest.source, - }, - }, - destination: null, - network: { - bytes_in: 3826633497, - bytes_out: 1083495734, - }, - }, - cursor: { - value: '8.8.8.8', - }, - }, - { - node: { - source: { - autonomous_system: { - name: 'TM Net, Internet Service Provider', - number: 4788, - }, - domain: ['test.domain.net', 'test.old.domain.net'], - flows: 12345, - destination_ips: 12, - ip: '9.9.9.9', - location: { - geo: { - continent_name: ['Asia'], - country_name: null, - country_iso_code: ['MY'], - city_name: ['Petaling Jaya'], - region_iso_code: ['MY-10'], - region_name: ['Selangor'], - }, - flowTarget: FlowTargetSourceDest.source, - }, - }, - destination: null, - network: { - bytes_in: 3826633497, - bytes_out: 1083495734, - }, - }, - cursor: { - value: '9.9.9.9', - }, - }, - ], - pageInfo: { - activePage: 1, - fakeTotalCount: 50, - showMorePagesIndicator: true, - }, - }, -}; diff --git a/x-pack/plugins/siem/public/components/page/network/tls_table/columns.tsx b/x-pack/plugins/siem/public/components/page/network/tls_table/columns.tsx deleted file mode 100644 index f95475819abc94..00000000000000 --- a/x-pack/plugins/siem/public/components/page/network/tls_table/columns.tsx +++ /dev/null @@ -1,99 +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. - */ - -/* eslint-disable react/display-name */ - -import React from 'react'; -import moment from 'moment'; -import { TlsNode } from '../../../../graphql/types'; -import { Columns } from '../../../paginated_table'; - -import { getRowItemDraggables, getRowItemDraggable } from '../../../tables/helpers'; -import { LocalizedDateTooltip } from '../../../localized_date_tooltip'; -import { PreferenceFormattedDate } from '../../../formatted_date'; - -import * as i18n from './translations'; - -export type TlsColumns = [ - Columns, - Columns, - Columns, - Columns, - Columns -]; - -export const getTlsColumns = (tableId: string): TlsColumns => [ - { - field: 'node', - name: i18n.ISSUER, - truncateText: false, - hideForMobile: false, - sortable: false, - render: ({ _id, issuers }) => - getRowItemDraggables({ - rowItems: issuers, - attrName: 'tls.server.issuer', - idPrefix: `${tableId}-${_id}-table-issuers`, - }), - }, - { - field: 'node', - name: i18n.SUBJECT, - truncateText: false, - hideForMobile: false, - sortable: false, - render: ({ _id, subjects }) => - getRowItemDraggables({ - rowItems: subjects, - attrName: 'tls.server.subject', - idPrefix: `${tableId}-${_id}-table-subjects`, - }), - }, - { - field: 'node._id', - name: i18n.SHA1_FINGERPRINT, - truncateText: false, - hideForMobile: false, - sortable: true, - render: sha1 => - getRowItemDraggable({ - rowItem: sha1, - attrName: 'tls.server_certificate.fingerprint.sha1', - idPrefix: `${tableId}-${sha1}-table-sha1`, - }), - }, - { - field: 'node', - name: i18n.JA3_FINGERPRINT, - truncateText: false, - hideForMobile: false, - sortable: false, - render: ({ _id, ja3 }) => - getRowItemDraggables({ - rowItems: ja3, - attrName: 'tls.fingerprints.ja3.hash', - idPrefix: `${tableId}-${_id}-table-ja3`, - }), - }, - { - field: 'node', - name: i18n.VALID_UNTIL, - truncateText: false, - hideForMobile: false, - sortable: false, - render: ({ _id, notAfter }) => - getRowItemDraggables({ - rowItems: notAfter, - attrName: 'tls.server_certificate.not_after', - idPrefix: `${tableId}-${_id}-table-notAfter`, - render: validUntil => ( - - - - ), - }), - }, -]; diff --git a/x-pack/plugins/siem/public/components/page/network/tls_table/index.test.tsx b/x-pack/plugins/siem/public/components/page/network/tls_table/index.test.tsx deleted file mode 100644 index 81a472f3175e51..00000000000000 --- a/x-pack/plugins/siem/public/components/page/network/tls_table/index.test.tsx +++ /dev/null @@ -1,97 +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 { shallow } from 'enzyme'; -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { MockedProvider } from 'react-apollo/test-utils'; -import { Provider as ReduxStoreProvider } from 'react-redux'; - -import { apolloClientObservable, mockGlobalState, TestProviders } from '../../../../mock'; -import { useMountAppended } from '../../../../utils/use_mount_appended'; -import { createStore, networkModel, State } from '../../../../store'; - -import { TlsTable } from '.'; -import { mockTlsData } from './mock'; - -describe('Tls Table Component', () => { - const loadPage = jest.fn(); - const state: State = mockGlobalState; - - let store = createStore(state, apolloClientObservable); - const mount = useMountAppended(); - - beforeEach(() => { - store = createStore(state, apolloClientObservable); - }); - - describe('Rendering', () => { - test('it renders the default Domains table', () => { - const wrapper = shallow( - - - - ); - - expect(wrapper.find('Connect(TlsTableComponent)')).toMatchSnapshot(); - }); - }); - - describe('Sorting on Table', () => { - test('when you click on the column header, you should show the sorting icon', () => { - const wrapper = mount( - - - - - - ); - expect(store.getState().network.details.queries!.tls.sort).toEqual({ - direction: 'desc', - field: '_id', - }); - - wrapper - .find('.euiTable thead tr th button') - .first() - .simulate('click'); - - wrapper.update(); - - expect(store.getState().network.details.queries!.tls.sort).toEqual({ - direction: 'asc', - field: '_id', - }); - - expect( - wrapper - .find('.euiTable thead tr th button') - .first() - .text() - ).toEqual('SHA1 fingerprintClick to sort in descending order'); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/page/network/tls_table/index.tsx b/x-pack/plugins/siem/public/components/page/network/tls_table/index.tsx deleted file mode 100644 index d1512699cc709c..00000000000000 --- a/x-pack/plugins/siem/public/components/page/network/tls_table/index.tsx +++ /dev/null @@ -1,163 +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, { useCallback, useMemo } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import deepEqual from 'fast-deep-equal'; - -import { networkActions } from '../../../../store/network'; -import { TlsEdges, TlsSortField, TlsFields, Direction } from '../../../../graphql/types'; -import { networkModel, networkSelectors, State } from '../../../../store'; -import { Criteria, ItemsPerRow, PaginatedTable, SortingBasicTable } from '../../../paginated_table'; -import { getTlsColumns } from './columns'; -import * as i18n from './translations'; - -interface OwnProps { - data: TlsEdges[]; - fakeTotalCount: number; - id: string; - isInspect: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - showMorePagesIndicator: boolean; - totalCount: number; - type: networkModel.NetworkType; -} - -type TlsTableProps = OwnProps & PropsFromRedux; - -const rowItems: ItemsPerRow[] = [ - { - text: i18n.ROWS_5, - numberOfRow: 5, - }, - { - text: i18n.ROWS_10, - numberOfRow: 10, - }, -]; - -export const tlsTableId = 'tls-table'; - -const TlsTableComponent = React.memo( - ({ - activePage, - data, - fakeTotalCount, - id, - isInspect, - limit, - loading, - loadPage, - showMorePagesIndicator, - sort, - totalCount, - type, - updateNetworkTable, - }) => { - const tableType: networkModel.TopTlsTableType = - type === networkModel.NetworkType.page - ? networkModel.NetworkTableType.tls - : networkModel.IpDetailsTableType.tls; - - const updateLimitPagination = useCallback( - newLimit => - updateNetworkTable({ - networkType: type, - tableType, - updates: { limit: newLimit }, - }), - [type, updateNetworkTable, tableType] - ); - - const updateActivePage = useCallback( - newPage => - updateNetworkTable({ - networkType: type, - tableType, - updates: { activePage: newPage }, - }), - [type, updateNetworkTable, tableType] - ); - - const onChange = useCallback( - (criteria: Criteria) => { - if (criteria.sort != null) { - const splitField = criteria.sort.field.split('.'); - const newTlsSort: TlsSortField = { - field: getSortFromString(splitField[splitField.length - 1]), - direction: criteria.sort.direction as Direction, - }; - if (!deepEqual(newTlsSort, sort)) { - updateNetworkTable({ - networkType: type, - tableType, - updates: { sort: newTlsSort }, - }); - } - } - }, - [sort, type, tableType, updateNetworkTable] - ); - - const columns = useMemo(() => getTlsColumns(tlsTableId), [tlsTableId]); - - return ( - - ); - } -); - -TlsTableComponent.displayName = 'TlsTableComponent'; - -const makeMapStateToProps = () => { - const getTlsSelector = networkSelectors.tlsSelector(); - return (state: State, { type }: OwnProps) => getTlsSelector(state, type); -}; - -const mapDispatchToProps = { - updateNetworkTable: networkActions.updateNetworkTable, -}; - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const TlsTable = connector(TlsTableComponent); - -const getSortField = (sortField: TlsSortField): SortingBasicTable => ({ - field: `node.${sortField.field}`, - direction: sortField.direction, -}); - -const getSortFromString = (sortField: string): TlsFields => { - switch (sortField) { - case '_id': - return TlsFields._id; - default: - return TlsFields._id; - } -}; diff --git a/x-pack/plugins/siem/public/components/page/network/tls_table/mock.ts b/x-pack/plugins/siem/public/components/page/network/tls_table/mock.ts deleted file mode 100644 index 453bd8fc84dfae..00000000000000 --- a/x-pack/plugins/siem/public/components/page/network/tls_table/mock.ts +++ /dev/null @@ -1,54 +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 { TlsData } from '../../../../graphql/types'; - -export const mockTlsData: TlsData = { - totalCount: 2, - edges: [ - { - node: { - _id: '2fe3bdf168af35b9e0ce5dc583bab007c40d47de', - subjects: ['*.elastic.co'], - ja3: ['7851693188210d3b271aa1713d8c68c2', 'fb4726d465c5f28b84cd6d14cedd13a7'], - issuers: ['DigiCert SHA2 Secure Server CA'], - notAfter: ['2021-04-22T12:00:00.000Z'], - }, - cursor: { - value: '2fe3bdf168af35b9e0ce5dc583bab007c40d47de', - }, - }, - { - node: { - _id: '61749734b3246f1584029deb4f5276c64da00ada', - subjects: ['api.snapcraft.io'], - ja3: ['839868ad711dc55bde0d37a87f14740d'], - issuers: ['DigiCert SHA2 Secure Server CA'], - notAfter: ['2019-05-22T12:00:00.000Z'], - }, - cursor: { - value: '61749734b3246f1584029deb4f5276c64da00ada', - }, - }, - { - node: { - _id: '6560d3b7dd001c989b85962fa64beb778cdae47a', - subjects: ['changelogs.ubuntu.com'], - ja3: ['da12c94da8021bbaf502907ad086e7bc'], - issuers: ["Let's Encrypt Authority X3"], - notAfter: ['2019-06-27T01:09:59.000Z'], - }, - cursor: { - value: '6560d3b7dd001c989b85962fa64beb778cdae47a', - }, - }, - ], - pageInfo: { - activePage: 1, - fakeTotalCount: 50, - showMorePagesIndicator: true, - }, -}; diff --git a/x-pack/plugins/siem/public/components/page/network/users_table/columns.tsx b/x-pack/plugins/siem/public/components/page/network/users_table/columns.tsx deleted file mode 100644 index b732ac5bfd5fae..00000000000000 --- a/x-pack/plugins/siem/public/components/page/network/users_table/columns.tsx +++ /dev/null @@ -1,84 +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 { FlowTarget, UsersItem } from '../../../../graphql/types'; -import { defaultToEmptyTag } from '../../../empty_value'; -import { Columns } from '../../../paginated_table'; - -import * as i18n from './translations'; -import { getRowItemDraggables, getRowItemDraggable } from '../../../tables/helpers'; - -export type UsersColumns = [ - Columns, - Columns, - Columns, - Columns, - Columns -]; - -export const getUsersColumns = (flowTarget: FlowTarget, tableId: string): UsersColumns => [ - { - field: 'node.user.name', - name: i18n.USER_NAME, - truncateText: false, - hideForMobile: false, - sortable: true, - render: userName => - getRowItemDraggable({ - rowItem: userName, - attrName: 'user.name', - idPrefix: `${tableId}-table-${flowTarget}-user`, - }), - }, - { - field: 'node.user.id', - name: i18n.USER_ID, - truncateText: false, - hideForMobile: false, - sortable: false, - render: userIds => - getRowItemDraggables({ - rowItems: userIds, - attrName: 'user.id', - idPrefix: `${tableId}-table-${flowTarget}`, - }), - }, - { - field: 'node.user.groupName', - name: i18n.GROUP_NAME, - truncateText: false, - hideForMobile: false, - sortable: false, - render: groupNames => - getRowItemDraggables({ - rowItems: groupNames, - attrName: 'user.group.name', - idPrefix: `${tableId}-table-${flowTarget}`, - }), - }, - { - field: 'node.user.groupId', - name: i18n.GROUP_ID, - truncateText: false, - hideForMobile: false, - sortable: false, - render: groupId => - getRowItemDraggables({ - rowItems: groupId, - attrName: 'user.group.id', - idPrefix: `${tableId}-table-${flowTarget}`, - }), - }, - { - align: 'right', - field: 'node.user.count', - name: i18n.DOCUMENT_COUNT, - truncateText: false, - hideForMobile: false, - sortable: true, - render: docCount => defaultToEmptyTag(docCount), - }, -]; diff --git a/x-pack/plugins/siem/public/components/page/network/users_table/index.test.tsx b/x-pack/plugins/siem/public/components/page/network/users_table/index.test.tsx deleted file mode 100644 index 8dc3704a089ea8..00000000000000 --- a/x-pack/plugins/siem/public/components/page/network/users_table/index.test.tsx +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { shallow } from 'enzyme'; -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { MockedProvider } from 'react-apollo/test-utils'; -import { Provider as ReduxStoreProvider } from 'react-redux'; - -import { FlowTarget } from '../../../../graphql/types'; -import { apolloClientObservable, mockGlobalState, TestProviders } from '../../../../mock'; -import { useMountAppended } from '../../../../utils/use_mount_appended'; -import { createStore, networkModel, State } from '../../../../store'; - -import { UsersTable } from '.'; -import { mockUsersData } from './mock'; - -describe('Users Table Component', () => { - const loadPage = jest.fn(); - const state: State = mockGlobalState; - - let store = createStore(state, apolloClientObservable); - const mount = useMountAppended(); - - beforeEach(() => { - store = createStore(state, apolloClientObservable); - }); - - describe('Rendering', () => { - test('it renders the default Users table', () => { - const wrapper = shallow( - - - - ); - - expect(wrapper.find('Connect(UsersTableComponent)')).toMatchSnapshot(); - }); - }); - - describe('Sorting on Table', () => { - test('when you click on the column header, you should show the sorting icon', () => { - const wrapper = mount( - - - - - - ); - expect(store.getState().network.details.queries!.users.sort).toEqual({ - direction: 'asc', - field: 'name', - }); - - wrapper - .find('.euiTable thead tr th button') - .first() - .simulate('click'); - - wrapper.update(); - - expect(store.getState().network.details.queries!.users.sort).toEqual({ - direction: 'desc', - field: 'name', - }); - expect( - wrapper - .find('.euiTable thead tr th button') - .first() - .text() - ).toEqual('UserClick to sort in ascending order'); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/page/network/users_table/index.tsx b/x-pack/plugins/siem/public/components/page/network/users_table/index.tsx deleted file mode 100644 index b585b835f31cde..00000000000000 --- a/x-pack/plugins/siem/public/components/page/network/users_table/index.tsx +++ /dev/null @@ -1,187 +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, { useCallback, useMemo } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import deepEqual from 'fast-deep-equal'; - -import { networkActions } from '../../../../store/network'; -import { - Direction, - FlowTarget, - UsersEdges, - UsersFields, - UsersSortField, -} from '../../../../graphql/types'; -import { networkModel, networkSelectors, State } from '../../../../store'; -import { Criteria, ItemsPerRow, PaginatedTable, SortingBasicTable } from '../../../paginated_table'; - -import { getUsersColumns } from './columns'; -import * as i18n from './translations'; -import { assertUnreachable } from '../../../../lib/helpers'; -const tableType = networkModel.IpDetailsTableType.users; - -interface OwnProps { - data: UsersEdges[]; - flowTarget: FlowTarget; - fakeTotalCount: number; - id: string; - isInspect: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - showMorePagesIndicator: boolean; - totalCount: number; - type: networkModel.NetworkType; -} - -type UsersTableProps = OwnProps & PropsFromRedux; - -const rowItems: ItemsPerRow[] = [ - { - text: i18n.ROWS_5, - numberOfRow: 5, - }, - { - text: i18n.ROWS_10, - numberOfRow: 10, - }, -]; - -export const usersTableId = 'users-table'; - -const UsersTableComponent = React.memo( - ({ - activePage, - data, - fakeTotalCount, - flowTarget, - id, - isInspect, - limit, - loading, - loadPage, - showMorePagesIndicator, - totalCount, - type, - updateNetworkTable, - sort, - }) => { - const updateLimitPagination = useCallback( - newLimit => - updateNetworkTable({ - networkType: type, - tableType, - updates: { limit: newLimit }, - }), - [type, updateNetworkTable] - ); - - const updateActivePage = useCallback( - newPage => - updateNetworkTable({ - networkType: type, - tableType, - updates: { activePage: newPage }, - }), - [type, updateNetworkTable] - ); - - const onChange = useCallback( - (criteria: Criteria) => { - if (criteria.sort != null) { - const splitField = criteria.sort.field.split('.'); - const newUsersSort: UsersSortField = { - field: getSortFromString(splitField[splitField.length - 1]), - direction: criteria.sort.direction as Direction, - }; - if (!deepEqual(newUsersSort, sort)) { - updateNetworkTable({ - networkType: type, - tableType, - updates: { sort: newUsersSort }, - }); - } - } - }, - [sort, type, updateNetworkTable] - ); - - const columns = useMemo(() => getUsersColumns(flowTarget, usersTableId), [ - flowTarget, - usersTableId, - ]); - - return ( - - ); - } -); - -UsersTableComponent.displayName = 'UsersTableComponent'; - -const makeMapStateToProps = () => { - const getUsersSelector = networkSelectors.usersSelector(); - return (state: State) => ({ - ...getUsersSelector(state), - }); -}; - -const mapDispatchToProps = { - updateNetworkTable: networkActions.updateNetworkTable, -}; - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const UsersTable = connector(UsersTableComponent); - -const getSortField = (sortField: UsersSortField): SortingBasicTable => { - switch (sortField.field) { - case UsersFields.name: - return { - field: `node.user.${sortField.field}`, - direction: sortField.direction, - }; - case UsersFields.count: - return { - field: `node.user.${sortField.field}`, - direction: sortField.direction, - }; - } - return assertUnreachable(sortField.field); -}; - -const getSortFromString = (sortField: string): UsersFields => { - switch (sortField) { - case UsersFields.name.valueOf(): - return UsersFields.name; - case UsersFields.count.valueOf(): - return UsersFields.count; - default: - return UsersFields.name; - } -}; diff --git a/x-pack/plugins/siem/public/components/page/network/users_table/mock.ts b/x-pack/plugins/siem/public/components/page/network/users_table/mock.ts deleted file mode 100644 index 9a5de66a91a3ec..00000000000000 --- a/x-pack/plugins/siem/public/components/page/network/users_table/mock.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { UsersData } from '../../../../graphql/types'; - -export const mockUsersData: UsersData = { - edges: [ - { - node: { - _id: '_apt', - user: { - id: ['104'], - name: '_apt', - groupId: ['65534'], - groupName: ['nogroup'], - count: 10, - }, - }, - cursor: { - value: '_apt', - tiebreaker: null, - }, - }, - { - node: { - _id: 'root', - user: { - id: ['0'], - name: 'root', - groupId: ['116', '0'], - groupName: ['Debian-exim', 'root'], - count: 108, - }, - }, - cursor: { - value: 'root', - tiebreaker: null, - }, - }, - { - node: { - _id: 'systemd-resolve', - user: { - id: ['102'], - name: 'systemd-resolve', - groupId: [], - groupName: [], - count: 4, - }, - }, - cursor: { - value: 'systemd-resolve', - tiebreaker: null, - }, - }, - ], - totalCount: 3, - pageInfo: { - activePage: 1, - fakeTotalCount: 3, - showMorePagesIndicator: true, - }, -}; diff --git a/x-pack/plugins/siem/public/components/page/overview/overview_host/index.test.tsx b/x-pack/plugins/siem/public/components/page/overview/overview_host/index.test.tsx deleted file mode 100644 index 568cf032fb01ce..00000000000000 --- a/x-pack/plugins/siem/public/components/page/overview/overview_host/index.test.tsx +++ /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. - */ - -import { cloneDeep } from 'lodash/fp'; -import { mount } from 'enzyme'; -import React from 'react'; - -import { apolloClientObservable, mockGlobalState, TestProviders } from '../../../../mock'; - -import { OverviewHost } from '.'; -import { createStore, State } from '../../../../store'; -import { overviewHostQuery } from '../../../../containers/overview/overview_host/index.gql_query'; -import { GetOverviewHostQuery } from '../../../../graphql/types'; -import { MockedProvider } from 'react-apollo/test-utils'; -import { wait } from '../../../../lib/helpers'; - -jest.mock('../../../../lib/kibana'); - -const startDate = 1579553397080; -const endDate = 1579639797080; - -interface MockedProvidedQuery { - request: { - query: GetOverviewHostQuery.Query; - fetchPolicy: string; - variables: GetOverviewHostQuery.Variables; - }; - result: { - data: { - source: unknown; - }; - }; -} - -const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ - { - request: { - query: overviewHostQuery, - fetchPolicy: 'cache-and-network', - variables: { - sourceId: 'default', - timerange: { interval: '12h', from: startDate, to: endDate }, - filterQuery: undefined, - defaultIndex: [ - 'apm-*-transaction*', - 'auditbeat-*', - 'endgame-*', - 'filebeat-*', - 'packetbeat-*', - 'winlogbeat-*', - ], - inspect: false, - }, - }, - result: { - data: { - source: { - id: 'default', - OverviewHost: { - auditbeatAuditd: 1, - auditbeatFIM: 1, - auditbeatLogin: 1, - auditbeatPackage: 1, - auditbeatProcess: 1, - auditbeatUser: 1, - endgameDns: 1, - endgameFile: 1, - endgameImageLoad: 1, - endgameNetwork: 1, - endgameProcess: 1, - endgameRegistry: 1, - endgameSecurity: 1, - filebeatSystemModule: 1, - winlogbeatSecurity: 1, - winlogbeatMWSysmonOperational: 1, - }, - }, - }, - }, - }, -]; - -describe('OverviewHost', () => { - const state: State = mockGlobalState; - - let store = createStore(state, apolloClientObservable); - - beforeEach(() => { - const myState = cloneDeep(state); - store = createStore(myState, apolloClientObservable); - }); - - test('it renders the expected widget title', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="header-section-title"]') - .first() - .text() - ).toEqual('Host events'); - }); - - test('it renders an empty subtitle while loading', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="header-panel-subtitle"]') - .first() - .text() - ).toEqual(''); - }); - - test('it renders the expected event count in the subtitle after loading events', async () => { - const wrapper = mount( - - - - - - ); - await wait(); - wrapper.update(); - - expect( - wrapper - .find('[data-test-subj="header-panel-subtitle"]') - .first() - .text() - ).toEqual('Showing: 16 events'); - }); -}); diff --git a/x-pack/plugins/siem/public/components/page/overview/overview_host/index.tsx b/x-pack/plugins/siem/public/components/page/overview/overview_host/index.tsx deleted file mode 100644 index 52c142ceff4805..00000000000000 --- a/x-pack/plugins/siem/public/components/page/overview/overview_host/index.tsx +++ /dev/null @@ -1,129 +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 { isEmpty } from 'lodash/fp'; -import { EuiButton, EuiFlexItem, EuiPanel } from '@elastic/eui'; -import numeral from '@elastic/numeral'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useMemo } from 'react'; - -import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; -import { ESQuery } from '../../../../../common/typed_json'; -import { - ID as OverviewHostQueryId, - OverviewHostQuery, -} from '../../../../containers/overview/overview_host'; -import { HeaderSection } from '../../../header_section'; -import { useUiSetting$ } from '../../../../lib/kibana'; -import { getHostsUrl } from '../../../link_to'; -import { getOverviewHostStats, OverviewHostStats } from '../overview_host_stats'; -import { manageQuery } from '../../../page/manage_query'; -import { inputsModel } from '../../../../store/inputs'; -import { InspectButtonContainer } from '../../../inspect'; -import { useGetUrlSearch } from '../../../navigation/use_get_url_search'; -import { navTabs } from '../../../../pages/home/home_navigations'; - -export interface OwnProps { - startDate: number; - endDate: number; - filterQuery?: ESQuery | string; - setQuery: ({ - id, - inspect, - loading, - refetch, - }: { - id: string; - inspect: inputsModel.InspectQuery | null; - loading: boolean; - refetch: inputsModel.Refetch; - }) => void; -} - -const OverviewHostStatsManage = manageQuery(OverviewHostStats); -export type OverviewHostProps = OwnProps; - -const OverviewHostComponent: React.FC = ({ - endDate, - filterQuery, - startDate, - setQuery, -}) => { - const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); - const urlSearch = useGetUrlSearch(navTabs.hosts); - const hostPageButton = useMemo( - () => ( - - - - ), - [urlSearch] - ); - return ( - - - - - {({ overviewHost, loading, id, inspect, refetch }) => { - const hostEventsCount = getOverviewHostStats(overviewHost).reduce( - (total, stat) => total + stat.count, - 0 - ); - const formattedHostEventsCount = numeral(hostEventsCount).format(defaultNumberFormat); - - return ( - <> - - ) : ( - <>{''} - ) - } - title={ - - } - > - {hostPageButton} - - - - - ); - }} - - - - - ); -}; - -export const OverviewHost = React.memo(OverviewHostComponent); diff --git a/x-pack/plugins/siem/public/components/page/overview/overview_host_stats/index.test.tsx b/x-pack/plugins/siem/public/components/page/overview/overview_host_stats/index.test.tsx deleted file mode 100644 index 4240ea441284cf..00000000000000 --- a/x-pack/plugins/siem/public/components/page/overview/overview_host_stats/index.test.tsx +++ /dev/null @@ -1,69 +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 { mount, shallow } from 'enzyme'; -import React from 'react'; - -import { OverviewHostStats } from '.'; -import { mockData } from './mock'; -import { TestProviders } from '../../../../mock/test_providers'; - -describe('Overview Host Stat Data', () => { - describe('rendering', () => { - test('it renders the default OverviewHostStats', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - }); - describe('loading', () => { - test('it does NOT show loading indicator when loading is false', () => { - const wrapper = mount( - - - - ); - - // click the accordion to expand it - wrapper - .find('button') - .first() - .simulate('click'); - wrapper.update(); - - expect( - wrapper - .find('[data-test-subj="host-stat-auditbeatAuditd"]') - .first() - .find('[data-test-subj="stat-value-loading-spinner"]') - .first() - .exists() - ).toBe(false); - }); - test('it shows loading indicator when loading is true', () => { - const wrapper = mount( - - - - ); - - // click the accordion to expand it - wrapper - .find('button') - .first() - .simulate('click'); - wrapper.update(); - - expect( - wrapper - .find('[data-test-subj="host-stat-auditbeatAuditd"]') - .first() - .find('[data-test-subj="stat-value-loading-spinner"]') - .first() - .exists() - ).toBe(true); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/page/overview/overview_host_stats/index.tsx b/x-pack/plugins/siem/public/components/page/overview/overview_host_stats/index.tsx deleted file mode 100644 index 4756e4c8265746..00000000000000 --- a/x-pack/plugins/siem/public/components/page/overview/overview_host_stats/index.tsx +++ /dev/null @@ -1,269 +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 { EuiAccordion, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiText } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React from 'react'; -import styled from 'styled-components'; - -import { OverviewHostData } from '../../../../graphql/types'; -import { FormattedStat, StatGroup } from '../types'; -import { StatValue } from '../stat_value'; - -interface OverviewHostProps { - data: OverviewHostData; - loading: boolean; -} - -export const getOverviewHostStats = (data: OverviewHostData): FormattedStat[] => [ - { - count: data.auditbeatAuditd ?? 0, - title: , - id: 'auditbeatAuditd', - }, - { - count: data.auditbeatFIM ?? 0, - title: ( - - ), - id: 'auditbeatFIM', - }, - { - count: data.auditbeatLogin ?? 0, - title: , - id: 'auditbeatLogin', - }, - { - count: data.auditbeatPackage ?? 0, - title: ( - - ), - id: 'auditbeatPackage', - }, - { - count: data.auditbeatProcess ?? 0, - title: ( - - ), - id: 'auditbeatProcess', - }, - { - count: data.auditbeatUser ?? 0, - title: , - id: 'auditbeatUser', - }, - { - count: data.endgameDns ?? 0, - title: , - id: 'endgameDns', - }, - { - count: data.endgameFile ?? 0, - title: , - id: 'endgameFile', - }, - { - count: data.endgameImageLoad ?? 0, - title: ( - - ), - id: 'endgameImageLoad', - }, - { - count: data.endgameNetwork ?? 0, - title: ( - - ), - id: 'endgameNetwork', - }, - { - count: data.endgameProcess ?? 0, - title: ( - - ), - id: 'endgameProcess', - }, - { - count: data.endgameRegistry ?? 0, - title: ( - - ), - id: 'endgameRegistry', - }, - { - count: data.endgameSecurity ?? 0, - title: ( - - ), - id: 'endgameSecurity', - }, - { - count: data.filebeatSystemModule ?? 0, - title: ( - - ), - id: 'filebeatSystemModule', - }, - { - count: data.winlogbeatSecurity ?? 0, - title: ( - - ), - id: 'winlogbeatSecurity', - }, - { - count: data.winlogbeatMWSysmonOperational ?? 0, - title: ( - - ), - id: 'winlogbeatMWSysmonOperational', - }, -]; - -const HostStatsContainer = styled.div` - .accordion-button { - width: 100%; - } -`; - -const hostStatGroups: StatGroup[] = [ - { - groupId: 'auditbeat', - name: ( - - ), - statIds: [ - 'auditbeatAuditd', - 'auditbeatFIM', - 'auditbeatLogin', - 'auditbeatPackage', - 'auditbeatProcess', - 'auditbeatUser', - ], - }, - { - groupId: 'endgame', - name: ( - - ), - statIds: [ - 'endgameDns', - 'endgameFile', - 'endgameImageLoad', - 'endgameNetwork', - 'endgameProcess', - 'endgameRegistry', - 'endgameSecurity', - ], - }, - { - groupId: 'filebeat', - name: ( - - ), - statIds: ['filebeatSystemModule'], - }, - { - groupId: 'winlogbeat', - name: ( - - ), - statIds: ['winlogbeatSecurity', 'winlogbeatMWSysmonOperational'], - }, -]; - -const Title = styled.div` - margin-left: 24px; -`; - -const AccordionContent = styled.div` - margin-top: 8px; -`; - -const OverviewHostStatsComponent: React.FC = ({ data, loading }) => { - const allHostStats = getOverviewHostStats(data); - const allHostStatsCount = allHostStats.reduce((total, stat) => total + stat.count, 0); - - return ( - - {hostStatGroups.map((statGroup, i) => { - const statsForGroup = allHostStats.filter(s => statGroup.statIds.includes(s.id)); - const statsForGroupCount = statsForGroup.reduce((total, stat) => total + stat.count, 0); - - return ( - - - - - {statGroup.name} - - - - - - } - buttonContentClassName="accordion-button" - > - - {statsForGroup.map(stat => ( - - - - {stat.title} - - - - - - - ))} - - - - ); - })} - - ); -}; - -export const OverviewHostStats = React.memo(OverviewHostStatsComponent); diff --git a/x-pack/plugins/siem/public/components/page/overview/overview_host_stats/mock.ts b/x-pack/plugins/siem/public/components/page/overview/overview_host_stats/mock.ts deleted file mode 100644 index 60e653caab8c10..00000000000000 --- a/x-pack/plugins/siem/public/components/page/overview/overview_host_stats/mock.ts +++ /dev/null @@ -1,28 +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 { OverviewHostData } from '../../../../graphql/types'; - -export const mockData: { OverviewHost: OverviewHostData } = { - OverviewHost: { - auditbeatAuditd: 73847, - auditbeatFIM: 107307, - auditbeatLogin: 60015, - auditbeatPackage: 2003, - auditbeatProcess: 1200, - auditbeatUser: 1979, - endgameDns: 39123, - endgameFile: 39456, - endgameImageLoad: 39789, - endgameNetwork: 39101112, - endgameProcess: 39131415, - endgameRegistry: 39161718, - endgameSecurity: 39202122, - filebeatSystemModule: 568, - winlogbeatSecurity: 195929, - winlogbeatMWSysmonOperational: 101070, - }, -}; diff --git a/x-pack/plugins/siem/public/components/page/overview/overview_network/index.test.tsx b/x-pack/plugins/siem/public/components/page/overview/overview_network/index.test.tsx deleted file mode 100644 index 151bb444cfe75c..00000000000000 --- a/x-pack/plugins/siem/public/components/page/overview/overview_network/index.test.tsx +++ /dev/null @@ -1,137 +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 { cloneDeep } from 'lodash/fp'; -import { mount } from 'enzyme'; -import React from 'react'; - -import { apolloClientObservable, mockGlobalState, TestProviders } from '../../../../mock'; - -import { OverviewNetwork } from '.'; -import { createStore, State } from '../../../../store'; -import { overviewNetworkQuery } from '../../../../containers/overview/overview_network/index.gql_query'; -import { GetOverviewHostQuery } from '../../../../graphql/types'; -import { MockedProvider } from 'react-apollo/test-utils'; -import { wait } from '../../../../lib/helpers'; - -jest.mock('../../../../lib/kibana'); - -const startDate = 1579553397080; -const endDate = 1579639797080; - -interface MockedProvidedQuery { - request: { - query: GetOverviewHostQuery.Query; - fetchPolicy: string; - variables: GetOverviewHostQuery.Variables; - }; - result: { - data: { - source: unknown; - }; - }; -} - -const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ - { - request: { - query: overviewNetworkQuery, - fetchPolicy: 'cache-and-network', - variables: { - sourceId: 'default', - timerange: { interval: '12h', from: startDate, to: endDate }, - filterQuery: undefined, - defaultIndex: [ - 'apm-*-transaction*', - 'auditbeat-*', - 'endgame-*', - 'filebeat-*', - 'packetbeat-*', - 'winlogbeat-*', - ], - inspect: false, - }, - }, - result: { - data: { - source: { - id: 'default', - OverviewNetwork: { - auditbeatSocket: 1, - filebeatCisco: 1, - filebeatNetflow: 1, - filebeatPanw: 1, - filebeatSuricata: 1, - filebeatZeek: 1, - packetbeatDNS: 1, - packetbeatFlow: 1, - packetbeatTLS: 1, - }, - }, - }, - }, - }, -]; - -describe('OverviewNetwork', () => { - const state: State = mockGlobalState; - - let store = createStore(state, apolloClientObservable); - - beforeEach(() => { - const myState = cloneDeep(state); - store = createStore(myState, apolloClientObservable); - }); - - test('it renders the expected widget title', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="header-section-title"]') - .first() - .text() - ).toEqual('Network events'); - }); - - test('it renders an empty subtitle while loading', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="header-panel-subtitle"]') - .first() - .text() - ).toEqual(''); - }); - - test('it renders the expected event count in the subtitle after loading events', async () => { - const wrapper = mount( - - - - - - ); - await wait(); - wrapper.update(); - - expect( - wrapper - .find('[data-test-subj="header-panel-subtitle"]') - .first() - .text() - ).toEqual('Showing: 9 events'); - }); -}); diff --git a/x-pack/plugins/siem/public/components/page/overview/overview_network/index.tsx b/x-pack/plugins/siem/public/components/page/overview/overview_network/index.tsx deleted file mode 100644 index d649a0dd9e9237..00000000000000 --- a/x-pack/plugins/siem/public/components/page/overview/overview_network/index.tsx +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { isEmpty } from 'lodash/fp'; -import { EuiButton, EuiFlexItem, EuiPanel } from '@elastic/eui'; -import numeral from '@elastic/numeral'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useMemo } from 'react'; - -import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; -import { ESQuery } from '../../../../../common/typed_json'; -import { HeaderSection } from '../../../header_section'; -import { useUiSetting$ } from '../../../../lib/kibana'; -import { manageQuery } from '../../../page/manage_query'; -import { - ID as OverviewNetworkQueryId, - OverviewNetworkQuery, -} from '../../../../containers/overview/overview_network'; -import { inputsModel } from '../../../../store/inputs'; -import { getOverviewNetworkStats, OverviewNetworkStats } from '../overview_network_stats'; -import { getNetworkUrl } from '../../../link_to'; -import { InspectButtonContainer } from '../../../inspect'; -import { useGetUrlSearch } from '../../../navigation/use_get_url_search'; -import { navTabs } from '../../../../pages/home/home_navigations'; - -export interface OverviewNetworkProps { - startDate: number; - endDate: number; - filterQuery?: ESQuery | string; - setQuery: ({ - id, - inspect, - loading, - refetch, - }: { - id: string; - inspect: inputsModel.InspectQuery | null; - loading: boolean; - refetch: inputsModel.Refetch; - }) => void; -} - -const OverviewNetworkStatsManage = manageQuery(OverviewNetworkStats); - -const OverviewNetworkComponent: React.FC = ({ - endDate, - filterQuery, - startDate, - setQuery, -}) => { - const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); - const urlSearch = useGetUrlSearch(navTabs.network); - const networkPageButton = useMemo( - () => ( - - - - ), - [urlSearch] - ); - return ( - - - - - {({ overviewNetwork, loading, id, inspect, refetch }) => { - const networkEventsCount = getOverviewNetworkStats(overviewNetwork).reduce( - (total, stat) => total + stat.count, - 0 - ); - const formattedNetworkEventsCount = numeral(networkEventsCount).format( - defaultNumberFormat - ); - - return ( - <> - - ) : ( - <>{''} - ) - } - title={ - - } - > - {networkPageButton} - - - - - ); - }} - - - - - ); -}; - -OverviewNetworkComponent.displayName = 'OverviewNetworkComponent'; - -export const OverviewNetwork = React.memo(OverviewNetworkComponent); diff --git a/x-pack/plugins/siem/public/components/page/overview/overview_network_stats/index.test.tsx b/x-pack/plugins/siem/public/components/page/overview/overview_network_stats/index.test.tsx deleted file mode 100644 index cf1a7d20b73eca..00000000000000 --- a/x-pack/plugins/siem/public/components/page/overview/overview_network_stats/index.test.tsx +++ /dev/null @@ -1,72 +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 { mount, shallow } from 'enzyme'; -import React from 'react'; - -import { OverviewNetworkStats } from '.'; -import { mockData } from './mock'; -import { TestProviders } from '../../../../mock/test_providers'; - -describe('Overview Network Stat Data', () => { - describe('rendering', () => { - test('it renders the default OverviewNetworkStats', () => { - const wrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); - }); - }); - describe('loading', () => { - test('it does NOT show loading indicator when loading is false', () => { - const wrapper = mount( - - - - ); - - // click the accordion to expand it - wrapper - .find('button') - .first() - .simulate('click'); - wrapper.update(); - - expect( - wrapper - .find('[data-test-subj="network-stat-auditbeatSocket"]') - .first() - .find('[data-test-subj="stat-value-loading-spinner"]') - .first() - .exists() - ).toBe(false); - }); - - test('it shows the loading indicator when loading is true', () => { - const wrapper = mount( - - - - ); - - // click the accordion to expand it - wrapper - .find('button') - .first() - .simulate('click'); - wrapper.update(); - - expect( - wrapper - .find('[data-test-subj="network-stat-auditbeatSocket"]') - .first() - .find('[data-test-subj="stat-value-loading-spinner"]') - .first() - .exists() - ).toBe(true); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/page/overview/overview_network_stats/index.tsx b/x-pack/plugins/siem/public/components/page/overview/overview_network_stats/index.tsx deleted file mode 100644 index ca947c29bc3822..00000000000000 --- a/x-pack/plugins/siem/public/components/page/overview/overview_network_stats/index.tsx +++ /dev/null @@ -1,195 +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 { EuiAccordion, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiText } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React from 'react'; -import styled from 'styled-components'; - -import { OverviewNetworkData } from '../../../../graphql/types'; -import { FormattedStat, StatGroup } from '../types'; -import { StatValue } from '../stat_value'; - -interface OverviewNetworkProps { - data: OverviewNetworkData; - loading: boolean; -} - -export const getOverviewNetworkStats = (data: OverviewNetworkData): FormattedStat[] => [ - { - count: data.auditbeatSocket ?? 0, - title: ( - - ), - id: 'auditbeatSocket', - }, - { - count: data.filebeatCisco ?? 0, - title: , - id: 'filebeatCisco', - }, - { - count: data.filebeatNetflow ?? 0, - title: ( - - ), - id: 'filebeatNetflow', - }, - { - count: data.filebeatPanw ?? 0, - title: ( - - ), - id: 'filebeatPanw', - }, - { - count: data.filebeatSuricata ?? 0, - title: ( - - ), - id: 'filebeatSuricata', - }, - { - count: data.filebeatZeek ?? 0, - title: , - id: 'filebeatZeek', - }, - { - count: data.packetbeatDNS ?? 0, - title: , - id: 'packetbeatDNS', - }, - { - count: data.packetbeatFlow ?? 0, - title: , - id: 'packetbeatFlow', - }, - { - count: data.packetbeatTLS ?? 0, - title: , - id: 'packetbeatTLS', - }, -]; - -const networkStatGroups: StatGroup[] = [ - { - groupId: 'auditbeat', - name: ( - - ), - statIds: ['auditbeatSocket'], - }, - { - groupId: 'filebeat', - name: ( - - ), - statIds: [ - 'filebeatCisco', - 'filebeatNetflow', - 'filebeatPanw', - 'filebeatSuricata', - 'filebeatZeek', - ], - }, - { - groupId: 'packetbeat', - name: ( - - ), - statIds: ['packetbeatDNS', 'packetbeatFlow', 'packetbeatTLS'], - }, -]; - -const NetworkStatsContainer = styled.div` - .accordion-button { - width: 100%; - } -`; - -const Title = styled.div` - margin-left: 24px; -`; - -const AccordionContent = styled.div` - margin-top: 8px; -`; - -const OverviewNetworkStatsComponent: React.FC = ({ data, loading }) => { - const allNetworkStats = getOverviewNetworkStats(data); - const allNetworkStatsCount = allNetworkStats.reduce((total, stat) => total + stat.count, 0); - - return ( - - {networkStatGroups.map((statGroup, i) => { - const statsForGroup = allNetworkStats.filter(s => statGroup.statIds.includes(s.id)); - const statsForGroupCount = statsForGroup.reduce((total, stat) => total + stat.count, 0); - - return ( - - - - - {statGroup.name} - - - - - - } - buttonContentClassName="accordion-button" - > - - {statsForGroup.map(stat => ( - - - - {stat.title} - - - - - - - ))} - - - - ); - })} - - ); -}; - -export const OverviewNetworkStats = React.memo(OverviewNetworkStatsComponent); diff --git a/x-pack/plugins/siem/public/components/page/overview/overview_network_stats/mock.ts b/x-pack/plugins/siem/public/components/page/overview/overview_network_stats/mock.ts deleted file mode 100644 index cc4c639f85deb8..00000000000000 --- a/x-pack/plugins/siem/public/components/page/overview/overview_network_stats/mock.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. - */ - -import { OverviewNetworkData } from '../../../../graphql/types'; - -export const mockData: { OverviewNetwork: OverviewNetworkData } = { - OverviewNetwork: { - auditbeatSocket: 12, - filebeatCisco: 999, - filebeatNetflow: 7777, - filebeatPanw: 66, - filebeatSuricata: 60015, - filebeatZeek: 2003, - packetbeatDNS: 10277307, - packetbeatFlow: 16, - packetbeatTLS: 3400000, - }, -}; diff --git a/x-pack/plugins/siem/public/components/paginated_table/helpers.ts b/x-pack/plugins/siem/public/components/paginated_table/helpers.ts deleted file mode 100644 index c63b8699e79eef..00000000000000 --- a/x-pack/plugins/siem/public/components/paginated_table/helpers.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. - */ - -import { PaginationInputPaginated } from '../../graphql/types'; - -export const generateTablePaginationOptions = ( - activePage: number, - limit: number -): PaginationInputPaginated => { - const cursorStart = activePage * limit; - return { - activePage, - cursorStart, - fakePossibleCount: 4 <= activePage && activePage > 0 ? limit * (activePage + 2) : limit * 5, - querySize: limit + cursorStart, - }; -}; diff --git a/x-pack/plugins/siem/public/components/paginated_table/index.test.tsx b/x-pack/plugins/siem/public/components/paginated_table/index.test.tsx deleted file mode 100644 index 94dac6607ce217..00000000000000 --- a/x-pack/plugins/siem/public/components/paginated_table/index.test.tsx +++ /dev/null @@ -1,522 +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 { mount, shallow } from 'enzyme'; -import React from 'react'; - -import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../common/constants'; -import { Direction } from '../../graphql/types'; - -import { BasicTableProps, PaginatedTable } from './index'; -import { getHostsColumns, mockData, rowItems, sortedHosts } from './index.mock'; -import { ThemeProvider } from 'styled-components'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; - -jest.mock('react', () => { - const r = jest.requireActual('react'); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return { ...r, memo: (x: any) => x }; -}); - -describe('Paginated Table Component', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); - let loadPage: jest.Mock; - let updateLimitPagination: jest.Mock; - let updateActivePage: jest.Mock; - beforeEach(() => { - loadPage = jest.fn(); - updateLimitPagination = jest.fn(); - updateActivePage = jest.fn(); - }); - - describe('rendering', () => { - test('it renders the default load more table', () => { - const wrapper = shallow( - - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={1} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> -
- ); - - expect(wrapper).toMatchSnapshot(); - }); - - test('it renders the loading panel at the beginning ', () => { - const wrapper = mount( - - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={1} - loading={true} - loadPage={loadPage} - pageOfItems={[]} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> -
- ); - - expect( - wrapper.find('[data-test-subj="initialLoadingPanelPaginatedTable"]').exists() - ).toBeTruthy(); - }); - - test('it renders the over loading panel after data has been in the table ', () => { - const wrapper = mount( - - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={1} - loading={true} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> -
- ); - - expect(wrapper.find('[data-test-subj="loadingPanelPaginatedTable"]').exists()).toBeTruthy(); - }); - - test('it renders the correct amount of pages and starts at activePage: 0', () => { - const wrapper = mount( - - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={1} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> -
- ); - - const paginiationProps = wrapper - .find('[data-test-subj="numberedPagination"]') - .first() - .props(); - - const expectedPaginationProps = { - 'data-test-subj': 'numberedPagination', - pageCount: 10, - activePage: 0, - }; - expect(JSON.stringify(paginiationProps)).toEqual(JSON.stringify(expectedPaginationProps)); - }); - - test('it render popover to select new limit in table', () => { - const wrapper = mount( - - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={2} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> -
- ); - - wrapper - .find('[data-test-subj="loadingMoreSizeRowPopover"] button') - .first() - .simulate('click'); - expect(wrapper.find('[data-test-subj="loadingMorePickSizeRow"]').exists()).toBeTruthy(); - }); - - test('it will NOT render popover to select new limit in table if props itemsPerRow is empty', () => { - const wrapper = mount( - - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={[]} - limit={2} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> -
- ); - - expect(wrapper.find('[data-test-subj="loadingMoreSizeRowPopover"]').exists()).toBeFalsy(); - }); - - test('It should render a sort icon if sorting is defined', () => { - const mockOnChange = jest.fn(); - const wrapper = mount( - - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={2} - loading={false} - loadPage={jest.fn()} - onChange={mockOnChange} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - sorting={{ direction: Direction.asc, field: 'node.host.name' }} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> -
- ); - - expect(wrapper.find('.euiTable thead tr th button svg')).toBeTruthy(); - }); - - test('Should display toast when user reaches end of results max', () => { - const wrapper = mount( - - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={DEFAULT_MAX_TABLE_QUERY_SIZE} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={DEFAULT_MAX_TABLE_QUERY_SIZE * 3} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> -
- ); - wrapper - .find('[data-test-subj="pagination-button-next"]') - .first() - .simulate('click'); - expect(updateActivePage.mock.calls.length).toEqual(0); - }); - - test('Should show items per row if totalCount is greater than items', () => { - const wrapper = mount( - - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={DEFAULT_MAX_TABLE_QUERY_SIZE} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={30} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> -
- ); - expect(wrapper.find('[data-test-subj="loadingMoreSizeRowPopover"]').exists()).toBeTruthy(); - }); - - test('Should hide items per row if totalCount is less than items', () => { - const wrapper = mount( - - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={DEFAULT_MAX_TABLE_QUERY_SIZE} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={1} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> -
- ); - expect(wrapper.find('[data-test-subj="loadingMoreSizeRowPopover"]').exists()).toBeFalsy(); - }); - }); - - describe('Events', () => { - test('should call updateActivePage with 1 when clicking to the first page', () => { - const wrapper = mount( - - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={1} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> -
- ); - wrapper - .find('[data-test-subj="pagination-button-next"]') - .first() - .simulate('click'); - expect(updateActivePage.mock.calls[0][0]).toEqual(1); - }); - - test('Should call updateActivePage with 0 when you pick a new limit', () => { - const wrapper = mount( - - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={2} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> -
- ); - wrapper - .find('[data-test-subj="pagination-button-next"]') - .first() - .simulate('click'); - - wrapper - .find('[data-test-subj="loadingMoreSizeRowPopover"] button') - .first() - .simulate('click'); - - wrapper - .find('[data-test-subj="loadingMorePickSizeRow"] button') - .first() - .simulate('click'); - expect(updateActivePage.mock.calls[1][0]).toEqual(0); - }); - - test('should update the page when the activePage is changed from redux', () => { - const ourProps: BasicTableProps = { - activePage: 3, - columns: getHostsColumns(), - headerCount: 1, - headerSupplement:

{'My test supplement.'}

, - headerTitle: 'Hosts', - headerTooltip: 'My test tooltip', - headerUnit: 'Test Unit', - itemsPerRow: rowItems, - limit: 1, - loading: false, - loadPage, - pageOfItems: mockData.Hosts.edges, - showMorePagesIndicator: true, - totalCount: 10, - updateActivePage, - updateLimitPagination: limit => updateLimitPagination({ limit }), - }; - - // enzyme does not allow us to pass props to child of HOC - // so we make a component to pass it the props context - // ComponentWithContext will pass the changed props to Component - // https://github.com/airbnb/enzyme/issues/1853#issuecomment-443475903 - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const ComponentWithContext = (props: BasicTableProps) => { - return ( - - - - ); - }; - - const wrapper = mount(); - expect( - wrapper - .find('[data-test-subj="numberedPagination"]') - .first() - .prop('activePage') - ).toEqual(3); - wrapper.setProps({ activePage: 0 }); - wrapper.update(); - expect( - wrapper - .find('[data-test-subj="numberedPagination"]') - .first() - .prop('activePage') - ).toEqual(0); - }); - - test('Should call updateLimitPagination when you pick a new limit', () => { - const wrapper = mount( - - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={2} - loading={false} - loadPage={loadPage} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> -
- ); - - wrapper - .find('[data-test-subj="loadingMoreSizeRowPopover"] button') - .first() - .simulate('click'); - - wrapper - .find('[data-test-subj="loadingMorePickSizeRow"] button') - .first() - .simulate('click'); - expect(updateLimitPagination).toBeCalled(); - }); - - test('Should call onChange when you choose a new sort in the table', () => { - const mockOnChange = jest.fn(); - const wrapper = mount( - - {'My test supplement.'}

} - headerTitle="Hosts" - headerTooltip="My test tooltip" - headerUnit="Test Unit" - itemsPerRow={rowItems} - limit={2} - loading={false} - loadPage={jest.fn()} - onChange={mockOnChange} - pageOfItems={mockData.Hosts.edges} - showMorePagesIndicator={true} - sorting={{ direction: Direction.asc, field: 'node.host.name' }} - totalCount={10} - updateActivePage={updateActivePage} - updateLimitPagination={limit => updateLimitPagination({ limit })} - /> -
- ); - - wrapper - .find('.euiTable thead tr th button') - .first() - .simulate('click'); - - expect(mockOnChange).toBeCalled(); - expect(mockOnChange.mock.calls[0]).toEqual([ - { page: undefined, sort: { direction: 'desc', field: 'node.host.name' } }, - ]); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/paginated_table/index.tsx b/x-pack/plugins/siem/public/components/paginated_table/index.tsx deleted file mode 100644 index a815ecd1005180..00000000000000 --- a/x-pack/plugins/siem/public/components/paginated_table/index.tsx +++ /dev/null @@ -1,350 +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 { - EuiBasicTable, - EuiBasicTableProps, - EuiButtonEmpty, - EuiContextMenuItem, - EuiContextMenuPanel, - EuiFlexGroup, - EuiFlexItem, - EuiGlobalToastListToast as Toast, - EuiLoadingContent, - EuiPagination, - EuiPopover, - Direction, -} from '@elastic/eui'; -import { noop } from 'lodash/fp'; -import React, { FC, memo, useState, useEffect, ComponentType } from 'react'; -import styled from 'styled-components'; - -import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../common/constants'; -import { AuthTableColumns } from '../page/hosts/authentications_table'; -import { HostsTableColumns } from '../page/hosts/hosts_table'; -import { NetworkDnsColumns } from '../page/network/network_dns_table/columns'; -import { NetworkHttpColumns } from '../page/network/network_http_table/columns'; -import { - NetworkTopNFlowColumns, - NetworkTopNFlowColumnsIpDetails, -} from '../page/network/network_top_n_flow_table/columns'; -import { - NetworkTopCountriesColumns, - NetworkTopCountriesColumnsIpDetails, -} from '../page/network/network_top_countries_table/columns'; -import { TlsColumns } from '../page/network/tls_table/columns'; -import { UncommonProcessTableColumns } from '../page/hosts/uncommon_process_table'; -import { UsersColumns } from '../page/network/users_table/columns'; -import { HeaderSection } from '../header_section'; -import { Loader } from '../loader'; -import { useStateToaster } from '../toasters'; - -import * as i18n from './translations'; -import { Panel } from '../panel'; -import { InspectButtonContainer } from '../inspect'; - -const DEFAULT_DATA_TEST_SUBJ = 'paginated-table'; - -export interface ItemsPerRow { - text: string; - numberOfRow: number; -} - -export interface SortingBasicTable { - field: string; - direction: Direction; - allowNeutralSort?: boolean; -} - -export interface Criteria { - page?: { index: number; size: number }; - sort?: SortingBasicTable; -} - -declare type HostsTableColumnsTest = [ - Columns, - Columns, - Columns, - Columns -]; - -declare type BasicTableColumns = - | AuthTableColumns - | HostsTableColumns - | HostsTableColumnsTest - | NetworkDnsColumns - | NetworkHttpColumns - | NetworkTopCountriesColumns - | NetworkTopCountriesColumnsIpDetails - | NetworkTopNFlowColumns - | NetworkTopNFlowColumnsIpDetails - | TlsColumns - | UncommonProcessTableColumns - | UsersColumns; - -declare type SiemTables = BasicTableProps; - -// Using telescoping templates to remove 'any' that was polluting downstream column type checks -export interface BasicTableProps { - activePage: number; - columns: T; - dataTestSubj?: string; - headerCount: number; - headerSupplement?: React.ReactElement; - headerTitle: string | React.ReactElement; - headerTooltip?: string; - headerUnit: string | React.ReactElement; - id?: string; - itemsPerRow?: ItemsPerRow[]; - isInspect?: boolean; - limit: number; - loading: boolean; - loadPage: (activePage: number) => void; - onChange?: (criteria: Criteria) => void; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - pageOfItems: any[]; - showMorePagesIndicator: boolean; - sorting?: SortingBasicTable; - totalCount: number; - updateActivePage: (activePage: number) => void; - updateLimitPagination: (limit: number) => void; -} -type Func = (arg: T) => string | number; - -export interface Columns { - align?: string; - field?: string; - hideForMobile?: boolean; - isMobileHeader?: boolean; - name: string | React.ReactNode; - render?: (item: T, node: U) => React.ReactNode; - sortable?: boolean | Func; - truncateText?: boolean; - width?: string; -} - -const PaginatedTableComponent: FC = ({ - activePage, - columns, - dataTestSubj = DEFAULT_DATA_TEST_SUBJ, - headerCount, - headerSupplement, - headerTitle, - headerTooltip, - headerUnit, - id, - isInspect, - itemsPerRow, - limit, - loading, - loadPage, - onChange = noop, - pageOfItems, - showMorePagesIndicator, - sorting = null, - totalCount, - updateActivePage, - updateLimitPagination, -}) => { - const [myLoading, setMyLoading] = useState(loading); - const [myActivePage, setActivePage] = useState(activePage); - const [loadingInitial, setLoadingInitial] = useState(headerCount === -1); - const [isPopoverOpen, setPopoverOpen] = useState(false); - - const pageCount = Math.ceil(totalCount / limit); - const dispatchToaster = useStateToaster()[1]; - - useEffect(() => { - setActivePage(activePage); - }, [activePage]); - - useEffect(() => { - if (headerCount >= 0 && loadingInitial) { - setLoadingInitial(false); - } - }, [loadingInitial, headerCount]); - - useEffect(() => { - setMyLoading(loading); - }, [loading]); - - const onButtonClick = () => { - setPopoverOpen(!isPopoverOpen); - }; - - const closePopover = () => { - setPopoverOpen(false); - }; - - const goToPage = (newActivePage: number) => { - if ((newActivePage + 1) * limit >= DEFAULT_MAX_TABLE_QUERY_SIZE) { - const toast: Toast = { - id: 'PaginationWarningMsg', - title: headerTitle + i18n.TOAST_TITLE, - color: 'warning', - iconType: 'alert', - toastLifeTimeMs: 10000, - text: i18n.TOAST_TEXT, - }; - return dispatchToaster({ - type: 'addToaster', - toast, - }); - } - setActivePage(newActivePage); - loadPage(newActivePage); - updateActivePage(newActivePage); - }; - - const button = ( - - {`${i18n.ROWS}: ${limit}`} - - ); - - const rowItems = - itemsPerRow && - itemsPerRow.map((item: ItemsPerRow) => ( - { - closePopover(); - updateLimitPagination(item.numberOfRow); - updateActivePage(0); // reset results to first page - }} - > - {item.text} - - )); - const PaginationWrapper = showMorePagesIndicator ? PaginationEuiFlexItem : EuiFlexItem; - - return ( - - - = 0 ? headerCount.toLocaleString() : 0} ${headerUnit}` - } - title={headerTitle} - tooltip={headerTooltip} - > - {!loadingInitial && headerSupplement} - - - {loadingInitial ? ( - - ) : ( - <> - - - - {itemsPerRow && itemsPerRow.length > 0 && totalCount >= itemsPerRow[0].numberOfRow && ( - - - - )} - - - - - - - {(isInspect || myLoading) && ( - - )} - - )} - - - ); -}; - -export const PaginatedTable = memo(PaginatedTableComponent); - -type BasicTableType = ComponentType>; // eslint-disable-line @typescript-eslint/no-explicit-any -const BasicTable = styled(EuiBasicTable as BasicTableType)` - tbody { - th, - td { - vertical-align: top; - } - - .euiTableCellContent { - display: block; - } - } -` as any; // eslint-disable-line @typescript-eslint/no-explicit-any - -BasicTable.displayName = 'BasicTable'; - -const FooterAction = styled(EuiFlexGroup).attrs(() => ({ - alignItems: 'center', - responsive: false, -}))` - margin-top: ${({ theme }) => theme.eui.euiSizeXS}; -`; - -FooterAction.displayName = 'FooterAction'; - -const PaginationEuiFlexItem = styled(EuiFlexItem)` - @media only screen and (min-width: ${({ theme }) => theme.eui.euiBreakpoints.m}) { - .euiButtonIcon:last-child { - margin-left: 28px; - } - - .euiPagination { - position: relative; - } - - .euiPagination::before { - bottom: 0; - color: ${({ theme }) => theme.eui.euiButtonColorDisabled}; - content: '\\2026'; - font-size: ${({ theme }) => theme.eui.euiFontSizeS}; - padding: 5px ${({ theme }) => theme.eui.euiSizeS}; - position: absolute; - right: ${({ theme }) => theme.eui.euiSizeL}; - } - } -`; - -PaginationEuiFlexItem.displayName = 'PaginationEuiFlexItem'; diff --git a/x-pack/plugins/siem/public/components/pin/index.tsx b/x-pack/plugins/siem/public/components/pin/index.tsx deleted file mode 100644 index 9f898f9acaf2e9..00000000000000 --- a/x-pack/plugins/siem/public/components/pin/index.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 { EuiButtonIcon, IconSize } from '@elastic/eui'; -import { noop } from 'lodash/fp'; -import React from 'react'; - -import * as i18n from '../../components/timeline/body/translations'; - -export type PinIcon = 'pin' | 'pinFilled'; - -export const getPinIcon = (pinned: boolean): PinIcon => (pinned ? 'pinFilled' : 'pin'); - -interface Props { - allowUnpinning: boolean; - iconSize?: IconSize; - onClick?: () => void; - pinned: boolean; -} - -export const Pin = React.memo( - ({ allowUnpinning, iconSize = 'm', onClick = noop, pinned }) => ( - - ) -); - -Pin.displayName = 'Pin'; diff --git a/x-pack/plugins/siem/public/components/port/index.test.tsx b/x-pack/plugins/siem/public/components/port/index.test.tsx deleted file mode 100644 index 6ab587f266a8a4..00000000000000 --- a/x-pack/plugins/siem/public/components/port/index.test.tsx +++ /dev/null @@ -1,71 +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 { shallow } from 'enzyme'; -import React from 'react'; - -import { TestProviders } from '../../mock/test_providers'; -import { useMountAppended } from '../../utils/use_mount_appended'; - -import { Port } from '.'; - -describe('Port', () => { - const mount = useMountAppended(); - - test('renders correctly against snapshot', () => { - const wrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); - }); - - test('it renders the port', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="port"]') - .first() - .text() - ).toEqual('443'); - }); - - test('it hyperlinks links destination.port to an external service that describes the purpose of the port', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="port-or-service-name-link"]') - .first() - .props().href - ).toEqual( - 'https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=443' - ); - }); - - test('it renders an external link', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="external-link-icon"]') - .first() - .exists() - ).toBe(true); - }); -}); diff --git a/x-pack/plugins/siem/public/components/port/index.tsx b/x-pack/plugins/siem/public/components/port/index.tsx deleted file mode 100644 index bd6289547d0dc8..00000000000000 --- a/x-pack/plugins/siem/public/components/port/index.tsx +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; - -import { DefaultDraggable } from '../draggables'; -import { getEmptyValue } from '../empty_value'; -import { ExternalLinkIcon } from '../external_link_icon'; -import { PortOrServiceNameLink } from '../links'; - -export const CLIENT_PORT_FIELD_NAME = 'client.port'; -export const DESTINATION_PORT_FIELD_NAME = 'destination.port'; -export const SERVER_PORT_FIELD_NAME = 'server.port'; -export const SOURCE_PORT_FIELD_NAME = 'source.port'; -export const URL_PORT_FIELD_NAME = 'url.port'; - -export const PORT_NAMES = [ - CLIENT_PORT_FIELD_NAME, - DESTINATION_PORT_FIELD_NAME, - SERVER_PORT_FIELD_NAME, - SOURCE_PORT_FIELD_NAME, - URL_PORT_FIELD_NAME, -]; - -export const Port = React.memo<{ - contextId: string; - eventId: string; - fieldName: string; - value: string | undefined | null; -}>(({ contextId, eventId, fieldName, value }) => ( - - - - -)); - -Port.displayName = 'Port'; diff --git a/x-pack/plugins/siem/public/components/query_bar/index.test.tsx b/x-pack/plugins/siem/public/components/query_bar/index.test.tsx deleted file mode 100644 index e27669b2b15be2..00000000000000 --- a/x-pack/plugins/siem/public/components/query_bar/index.test.tsx +++ /dev/null @@ -1,338 +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 { mount } from 'enzyme'; -import React from 'react'; - -import { DEFAULT_FROM, DEFAULT_TO } from '../../../common/constants'; -import { TestProviders, mockIndexPattern } from '../../mock'; -import { createKibanaCoreStartMock } from '../../mock/kibana_core'; -import { FilterManager, SearchBar } from '../../../../../../src/plugins/data/public'; -import { QueryBar, QueryBarComponentProps } from '.'; -import { createKibanaContextProviderMock } from '../../mock/kibana_react'; - -const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; - -describe('QueryBar ', () => { - // We are doing that because we need to wrapped this component with redux - // and redux does not like to be updated and since we need to update our - // child component (BODY) and we do not want to scare anyone with this error - // we are hiding it!!! - // eslint-disable-next-line no-console - const originalError = console.error; - beforeAll(() => { - // eslint-disable-next-line no-console - console.error = (...args: string[]) => { - if (/ does not support changing `store` on the fly/.test(args[0])) { - return; - } - originalError.call(console, ...args); - }; - }); - - const mockOnChangeQuery = jest.fn(); - const mockOnSubmitQuery = jest.fn(); - const mockOnSavedQuery = jest.fn(); - - beforeEach(() => { - mockOnChangeQuery.mockClear(); - mockOnSubmitQuery.mockClear(); - mockOnSavedQuery.mockClear(); - }); - - test('check if we format the appropriate props to QueryBar', () => { - const wrapper = mount( - - - - ); - const { - customSubmitButton, - timeHistory, - onClearSavedQuery, - onFiltersUpdated, - onQueryChange, - onQuerySubmit, - onSaved, - onSavedQueryUpdated, - ...searchBarProps - } = wrapper.find(SearchBar).props(); - - expect(searchBarProps).toEqual({ - dataTestSubj: undefined, - dateRangeFrom: 'now-24h', - dateRangeTo: 'now', - filters: [], - indexPatterns: [ - { - fields: [ - { - aggregatable: true, - name: '@timestamp', - searchable: true, - type: 'date', - }, - { - aggregatable: true, - name: '@version', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'agent.ephemeral_id', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'agent.hostname', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'agent.id', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'agent.test1', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'agent.test2', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'agent.test3', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'agent.test4', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'agent.test5', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'agent.test6', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'agent.test7', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'agent.test8', - searchable: true, - type: 'string', - }, - { - aggregatable: true, - name: 'host.name', - searchable: true, - type: 'string', - }, - ], - title: 'filebeat-*,auditbeat-*,packetbeat-*', - }, - ], - isLoading: false, - isRefreshPaused: true, - query: { - language: 'kuery', - query: 'here: query', - }, - refreshInterval: undefined, - showAutoRefreshOnly: false, - showDatePicker: false, - showFilterBar: true, - showQueryBar: true, - showQueryInput: true, - showSaveQuery: true, - }); - }); - - describe('#onQueryChange', () => { - test(' is the only reference that changed when filterQueryDraft props get updated', () => { - const KibanaWithStorageProvider = createKibanaContextProviderMock(); - - const Proxy = (props: QueryBarComponentProps) => ( - - - - - - ); - - const wrapper = mount( - - ); - const searchBarProps = wrapper.find(SearchBar).props(); - const onChangedQueryRef = searchBarProps.onQueryChange; - const onSubmitQueryRef = searchBarProps.onQuerySubmit; - const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; - - const queryInput = wrapper.find(QueryBar).find('input[data-test-subj="queryInput"]'); - queryInput.simulate('change', { target: { value: 'hello: world' } }); - wrapper.update(); - - expect(onChangedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQueryChange); - expect(onSubmitQueryRef).toEqual(wrapper.find(SearchBar).props().onQuerySubmit); - expect(onSavedQueryRef).toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated); - }); - }); - - describe('#onQuerySubmit', () => { - test(' is the only reference that changed when filterQuery props get updated', () => { - const Proxy = (props: QueryBarComponentProps) => ( - - - - ); - - const wrapper = mount( - - ); - const searchBarProps = wrapper.find(SearchBar).props(); - const onChangedQueryRef = searchBarProps.onQueryChange; - const onSubmitQueryRef = searchBarProps.onQuerySubmit; - const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; - - wrapper.setProps({ filterQuery: { expression: 'new: one', kind: 'kuery' } }); - wrapper.update(); - - expect(onSubmitQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQuerySubmit); - expect(onChangedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQueryChange); - expect(onSavedQueryRef).toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated); - }); - - test(' is only reference that changed when timelineId props get updated', () => { - const Proxy = (props: QueryBarComponentProps) => ( - - - - ); - - const wrapper = mount( - - ); - const searchBarProps = wrapper.find(SearchBar).props(); - const onChangedQueryRef = searchBarProps.onQueryChange; - const onSubmitQueryRef = searchBarProps.onQuerySubmit; - const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; - - wrapper.setProps({ onSubmitQuery: jest.fn() }); - wrapper.update(); - - expect(onSubmitQueryRef).not.toEqual(wrapper.find(SearchBar).props().onQuerySubmit); - expect(onChangedQueryRef).toEqual(wrapper.find(SearchBar).props().onQueryChange); - expect(onSavedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated); - }); - }); - - describe('#onSavedQueryUpdated', () => { - test('is only reference that changed when dataProviders props get updated', () => { - const Proxy = (props: QueryBarComponentProps) => ( - - - - ); - - const wrapper = mount( - - ); - const searchBarProps = wrapper.find(SearchBar).props(); - const onChangedQueryRef = searchBarProps.onQueryChange; - const onSubmitQueryRef = searchBarProps.onQuerySubmit; - const onSavedQueryRef = searchBarProps.onSavedQueryUpdated; - - wrapper.setProps({ onSavedQuery: jest.fn() }); - wrapper.update(); - - expect(onSavedQueryRef).not.toEqual(wrapper.find(SearchBar).props().onSavedQueryUpdated); - expect(onChangedQueryRef).toEqual(wrapper.find(SearchBar).props().onQueryChange); - expect(onSubmitQueryRef).toEqual(wrapper.find(SearchBar).props().onQuerySubmit); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/query_bar/index.tsx b/x-pack/plugins/siem/public/components/query_bar/index.tsx deleted file mode 100644 index 1ad7bc16b901ee..00000000000000 --- a/x-pack/plugins/siem/public/components/query_bar/index.tsx +++ /dev/null @@ -1,153 +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, { memo, useState, useEffect, useMemo, useCallback } from 'react'; -import deepEqual from 'fast-deep-equal'; - -import { - Filter, - IIndexPattern, - FilterManager, - Query, - TimeHistory, - TimeRange, - SavedQuery, - SearchBar, - SavedQueryTimeFilter, -} from '../../../../../../src/plugins/data/public'; -import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; - -export interface QueryBarComponentProps { - dataTestSubj?: string; - dateRangeFrom?: string; - dateRangeTo?: string; - hideSavedQuery?: boolean; - indexPattern: IIndexPattern; - isLoading?: boolean; - isRefreshPaused?: boolean; - filterQuery: Query; - filterManager: FilterManager; - filters: Filter[]; - onChangedQuery: (query: Query) => void; - onSubmitQuery: (query: Query, timefilter?: SavedQueryTimeFilter) => void; - refreshInterval?: number; - savedQuery?: SavedQuery | null; - onSavedQuery: (savedQuery: SavedQuery | null) => void; -} - -export const QueryBar = memo( - ({ - dateRangeFrom, - dateRangeTo, - hideSavedQuery = false, - indexPattern, - isLoading = false, - isRefreshPaused, - filterQuery, - filterManager, - filters, - onChangedQuery, - onSubmitQuery, - refreshInterval, - savedQuery, - onSavedQuery, - dataTestSubj, - }) => { - const [draftQuery, setDraftQuery] = useState(filterQuery); - - useEffect(() => { - setDraftQuery(filterQuery); - }, [filterQuery]); - - const onQuerySubmit = useCallback( - (payload: { dateRange: TimeRange; query?: Query }) => { - if (payload.query != null && !deepEqual(payload.query, filterQuery)) { - onSubmitQuery(payload.query); - } - }, - [filterQuery, onSubmitQuery] - ); - - const onQueryChange = useCallback( - (payload: { dateRange: TimeRange; query?: Query }) => { - if (payload.query != null && !deepEqual(payload.query, draftQuery)) { - setDraftQuery(payload.query); - onChangedQuery(payload.query); - } - }, - [draftQuery, onChangedQuery, setDraftQuery] - ); - - const onSaved = useCallback( - (newSavedQuery: SavedQuery) => { - onSavedQuery(newSavedQuery); - }, - [onSavedQuery] - ); - - const onSavedQueryUpdated = useCallback( - (savedQueryUpdated: SavedQuery) => { - const { query: newQuery, filters: newFilters, timefilter } = savedQueryUpdated.attributes; - onSubmitQuery(newQuery, timefilter); - filterManager.setFilters(newFilters || []); - onSavedQuery(savedQueryUpdated); - }, - [filterManager, onSubmitQuery, onSavedQuery] - ); - - const onClearSavedQuery = useCallback(() => { - if (savedQuery != null) { - onSubmitQuery({ - query: '', - language: savedQuery.attributes.query.language, - }); - filterManager.setFilters([]); - onSavedQuery(null); - } - }, [filterManager, onSubmitQuery, onSavedQuery, savedQuery]); - - const onFiltersUpdated = useCallback( - (newFilters: Filter[]) => { - filterManager.setFilters(newFilters); - }, - [filterManager] - ); - - const CustomButton = <>{null}; - const indexPatterns = useMemo(() => [indexPattern], [indexPattern]); - - const searchBarProps = savedQuery != null ? { savedQuery } : {}; - - return ( - - ); - } -); diff --git a/x-pack/plugins/siem/public/components/recent_cases/index.tsx b/x-pack/plugins/siem/public/components/recent_cases/index.tsx deleted file mode 100644 index 07246c6c6ec885..00000000000000 --- a/x-pack/plugins/siem/public/components/recent_cases/index.tsx +++ /dev/null @@ -1,80 +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 { EuiHorizontalRule, EuiLink, EuiText } from '@elastic/eui'; -import React, { useEffect, useMemo, useRef } from 'react'; - -import { FilterOptions, QueryParams } from '../../containers/case/types'; -import { DEFAULT_QUERY_PARAMS, useGetCases } from '../../containers/case/use_get_cases'; -import { getCaseUrl } from '../link_to/redirect_to_case'; -import { useGetUrlSearch } from '../navigation/use_get_url_search'; -import { LoadingPlaceholders } from '../page/overview/loading_placeholders'; -import { navTabs } from '../../pages/home/home_navigations'; - -import { NoCases } from './no_cases'; -import { RecentCases } from './recent_cases'; -import * as i18n from './translations'; - -const usePrevious = (value: FilterOptions) => { - const ref = useRef(); - useEffect(() => { - (ref.current as unknown) = value; - }); - return ref.current; -}; - -const MAX_CASES_TO_SHOW = 3; - -const queryParams: QueryParams = { - ...DEFAULT_QUERY_PARAMS, - perPage: MAX_CASES_TO_SHOW, -}; - -const StatefulRecentCasesComponent = React.memo( - ({ filterOptions }: { filterOptions: FilterOptions }) => { - const previousFilterOptions = usePrevious(filterOptions); - const { data, loading, setFilters } = useGetCases(queryParams); - const isLoadingCases = useMemo( - () => loading.indexOf('cases') > -1 || loading.indexOf('caseUpdate') > -1, - [loading] - ); - const search = useGetUrlSearch(navTabs.case); - const allCasesLink = useMemo( - () => {i18n.VIEW_ALL_CASES}, - [search] - ); - - useEffect(() => { - if (previousFilterOptions !== undefined && previousFilterOptions !== filterOptions) { - setFilters(filterOptions); - } - }, [previousFilterOptions, filterOptions, setFilters]); - - const content = useMemo( - () => - isLoadingCases ? ( - - ) : !isLoadingCases && data.cases.length === 0 ? ( - - ) : ( - - ), - [isLoadingCases, data] - ); - - return ( - - {content} - - {allCasesLink} - - ); - } -); - -StatefulRecentCasesComponent.displayName = 'StatefulRecentCasesComponent'; - -export const StatefulRecentCases = React.memo(StatefulRecentCasesComponent); diff --git a/x-pack/plugins/siem/public/components/recent_cases/no_cases/index.tsx b/x-pack/plugins/siem/public/components/recent_cases/no_cases/index.tsx deleted file mode 100644 index 9f0361311b7b6d..00000000000000 --- a/x-pack/plugins/siem/public/components/recent_cases/no_cases/index.tsx +++ /dev/null @@ -1,34 +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 { EuiLink } from '@elastic/eui'; -import React, { useMemo } from 'react'; - -import { getCreateCaseUrl } from '../../link_to/redirect_to_case'; -import { useGetUrlSearch } from '../../navigation/use_get_url_search'; -import { navTabs } from '../../../pages/home/home_navigations'; - -import * as i18n from '../translations'; - -const NoCasesComponent = () => { - const urlSearch = useGetUrlSearch(navTabs.case); - const newCaseLink = useMemo( - () => {` ${i18n.START_A_NEW_CASE}`}, - [urlSearch] - ); - - return ( - <> - {i18n.NO_CASES} - {newCaseLink} - {'!'} - - ); -}; - -NoCasesComponent.displayName = 'NoCasesComponent'; - -export const NoCases = React.memo(NoCasesComponent); diff --git a/x-pack/plugins/siem/public/components/recent_timelines/counts/index.tsx b/x-pack/plugins/siem/public/components/recent_timelines/counts/index.tsx deleted file mode 100644 index c80530b245cf35..00000000000000 --- a/x-pack/plugins/siem/public/components/recent_timelines/counts/index.tsx +++ /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 { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, EuiToolTip } from '@elastic/eui'; -import React from 'react'; -import styled from 'styled-components'; - -import { getPinnedEventCount, getNotesCount } from '../../open_timeline/helpers'; -import { OpenTimelineResult } from '../../open_timeline/types'; - -import * as i18n from '../translations'; - -const Icon = styled(EuiIcon)` - margin-right: 8px; -`; - -const FlexGroup = styled(EuiFlexGroup)` - margin-right: 16px; -`; - -export const IconWithCount = React.memo<{ count: number; icon: string; tooltip: string }>( - ({ count, icon, tooltip }) => ( - - - - - - - - - {count} - - - - - ) -); - -IconWithCount.displayName = 'IconWithCount'; - -export const RecentTimelineCounts = React.memo<{ - timeline: OpenTimelineResult; -}>(({ timeline }) => { - return ( -
- - -
- ); -}); - -RecentTimelineCounts.displayName = 'RecentTimelineCounts'; diff --git a/x-pack/plugins/siem/public/components/recent_timelines/header/index.tsx b/x-pack/plugins/siem/public/components/recent_timelines/header/index.tsx deleted file mode 100644 index 89c7ae6f1eed96..00000000000000 --- a/x-pack/plugins/siem/public/components/recent_timelines/header/index.tsx +++ /dev/null @@ -1,30 +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 { EuiText, EuiLink } from '@elastic/eui'; -import React, { useCallback } from 'react'; - -import { isUntitled } from '../../open_timeline/helpers'; -import { OnOpenTimeline, OpenTimelineResult } from '../../open_timeline/types'; -import * as i18n from '../translations'; - -export const RecentTimelineHeader = React.memo<{ - onOpenTimeline: OnOpenTimeline; - timeline: OpenTimelineResult; -}>(({ onOpenTimeline, timeline, timeline: { title, savedObjectId } }) => { - const onClick = useCallback( - () => onOpenTimeline({ duplicate: false, timelineId: `${savedObjectId}` }), - [onOpenTimeline, savedObjectId] - ); - - return ( - - {isUntitled(timeline) ? i18n.UNTITLED_TIMELINE : title} - - ); -}); - -RecentTimelineHeader.displayName = 'RecentTimelineHeader'; diff --git a/x-pack/plugins/siem/public/components/recent_timelines/index.tsx b/x-pack/plugins/siem/public/components/recent_timelines/index.tsx deleted file mode 100644 index d3532d9fd1025f..00000000000000 --- a/x-pack/plugins/siem/public/components/recent_timelines/index.tsx +++ /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 ApolloClient from 'apollo-client'; -import { EuiHorizontalRule, EuiLink, EuiText } from '@elastic/eui'; -import React, { useCallback, useMemo, useEffect } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import { Dispatch } from 'redux'; - -import { TimelineType } from '../../../common/types/timeline'; - -import { useGetAllTimeline } from '../../containers/timeline/all'; -import { SortFieldTimeline, Direction } from '../../graphql/types'; -import { queryTimelineById, dispatchUpdateTimeline } from '../open_timeline/helpers'; -import { OnOpenTimeline } from '../open_timeline/types'; -import { LoadingPlaceholders } from '../page/overview/loading_placeholders'; -import { updateIsLoading as dispatchUpdateIsLoading } from '../../store/timeline/actions'; - -import { RecentTimelines } from './recent_timelines'; -import * as i18n from './translations'; -import { FilterMode } from './types'; -import { useGetUrlSearch } from '../navigation/use_get_url_search'; -import { navTabs } from '../../pages/home/home_navigations'; -import { getTimelinesUrl } from '../link_to/redirect_to_timelines'; - -interface OwnProps { - apolloClient: ApolloClient<{}>; - filterBy: FilterMode; -} - -export type Props = OwnProps & PropsFromRedux; - -const PAGE_SIZE = 3; - -const StatefulRecentTimelinesComponent = React.memo( - ({ apolloClient, filterBy, updateIsLoading, updateTimeline }) => { - const onOpenTimeline: OnOpenTimeline = useCallback( - ({ duplicate, timelineId }: { duplicate: boolean; timelineId: string }) => { - queryTimelineById({ - apolloClient, - duplicate, - timelineId, - updateIsLoading, - updateTimeline, - }); - }, - [apolloClient, updateIsLoading, updateTimeline] - ); - - const noTimelinesMessage = - filterBy === 'favorites' ? i18n.NO_FAVORITE_TIMELINES : i18n.NO_TIMELINES; - const urlSearch = useGetUrlSearch(navTabs.timelines); - const linkAllTimelines = useMemo( - () => {i18n.VIEW_ALL_TIMELINES}, - [urlSearch] - ); - const loadingPlaceholders = useMemo( - () => ( - - ), - [filterBy] - ); - - const { fetchAllTimeline, timelines, loading } = useGetAllTimeline(); - - useEffect(() => { - fetchAllTimeline({ - pageInfo: { - pageIndex: 1, - pageSize: PAGE_SIZE, - }, - search: '', - sort: { - sortField: SortFieldTimeline.updated, - sortOrder: Direction.desc, - }, - onlyUserFavorite: filterBy === 'favorites', - timelineType: TimelineType.default, - }); - }, [filterBy]); - - return ( - <> - {loading ? ( - loadingPlaceholders - ) : ( - - )} - - {linkAllTimelines} - - ); - } -); - -StatefulRecentTimelinesComponent.displayName = 'StatefulRecentTimelinesComponent'; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - updateIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => - dispatch(dispatchUpdateIsLoading({ id, isLoading })), - updateTimeline: dispatchUpdateTimeline(dispatch), -}); - -const connector = connect(null, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const StatefulRecentTimelines = connector(StatefulRecentTimelinesComponent); diff --git a/x-pack/plugins/siem/public/components/search_bar/index.tsx b/x-pack/plugins/siem/public/components/search_bar/index.tsx deleted file mode 100644 index 4dd1b114ccff39..00000000000000 --- a/x-pack/plugins/siem/public/components/search_bar/index.tsx +++ /dev/null @@ -1,385 +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 { getOr, set } from 'lodash/fp'; -import React, { memo, useEffect, useCallback, useMemo } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import { Dispatch } from 'redux'; -import { Subscription } from 'rxjs'; -import styled from 'styled-components'; -import deepEqual from 'fast-deep-equal'; -import { - FilterManager, - IIndexPattern, - TimeRange, - Query, - Filter, - SavedQuery, -} from 'src/plugins/data/public'; - -import { OnTimeChangeProps } from '@elastic/eui'; - -import { inputsActions } from '../../store/inputs'; -import { InputsRange } from '../../store/inputs/model'; -import { InputsModelId } from '../../store/inputs/constants'; -import { State, inputsModel } from '../../store'; -import { formatDate } from '../super_date_picker'; -import { - endSelector, - filterQuerySelector, - fromStrSelector, - isLoadingSelector, - kindSelector, - queriesSelector, - savedQuerySelector, - startSelector, - toStrSelector, -} from './selectors'; -import { timelineActions, hostsActions, networkActions } from '../../store/actions'; -import { useKibana } from '../../lib/kibana'; - -interface SiemSearchBarProps { - id: InputsModelId; - indexPattern: IIndexPattern; - timelineId?: string; - dataTestSubj?: string; -} - -const SearchBarContainer = styled.div` - .globalQueryBar { - padding: 0px; - } -`; - -const SearchBarComponent = memo( - ({ - end, - filterQuery, - fromStr, - id, - indexPattern, - isLoading = false, - queries, - savedQuery, - setSavedQuery, - setSearchBarFilter, - start, - toStr, - updateSearch, - dataTestSubj, - }) => { - const { data } = useKibana().services; - const { - timefilter: { timefilter }, - filterManager, - } = data.query; - - if (fromStr != null && toStr != null) { - timefilter.setTime({ from: fromStr, to: toStr }); - } else if (start != null && end != null) { - timefilter.setTime({ - from: new Date(start).toISOString(), - to: new Date(end).toISOString(), - }); - } - - const onQuerySubmit = useCallback( - (payload: { dateRange: TimeRange; query?: Query }) => { - const isQuickSelection = - payload.dateRange.from.includes('now') || payload.dateRange.to.includes('now'); - let updateSearchBar: UpdateReduxSearchBar = { - id, - end: toStr != null ? toStr : new Date(end).toISOString(), - start: fromStr != null ? fromStr : new Date(start).toISOString(), - isInvalid: false, - isQuickSelection, - updateTime: false, - filterManager, - }; - let isStateUpdated = false; - - if ( - (isQuickSelection && - (fromStr !== payload.dateRange.from || toStr !== payload.dateRange.to)) || - (!isQuickSelection && - (start !== formatDate(payload.dateRange.from) || - end !== formatDate(payload.dateRange.to))) - ) { - isStateUpdated = true; - updateSearchBar.updateTime = true; - updateSearchBar.end = payload.dateRange.to; - updateSearchBar.start = payload.dateRange.from; - } - - if (payload.query != null && !deepEqual(payload.query, filterQuery)) { - isStateUpdated = true; - updateSearchBar = set('query', payload.query, updateSearchBar); - } - - if (!isStateUpdated) { - // That mean we are doing a refresh! - if (isQuickSelection) { - updateSearchBar.updateTime = true; - updateSearchBar.end = payload.dateRange.to; - updateSearchBar.start = payload.dateRange.from; - } else { - queries.forEach(q => q.refetch && (q.refetch as inputsModel.Refetch)()); - } - } - - window.setTimeout(() => updateSearch(updateSearchBar), 0); - }, - [id, end, filterQuery, fromStr, queries, start, toStr] - ); - - const onRefresh = useCallback( - (payload: { dateRange: TimeRange }) => { - if (payload.dateRange.from.includes('now') || payload.dateRange.to.includes('now')) { - updateSearch({ - id, - end: payload.dateRange.to, - start: payload.dateRange.from, - isInvalid: false, - isQuickSelection: true, - updateTime: true, - filterManager, - }); - } else { - queries.forEach(q => q.refetch && (q.refetch as inputsModel.Refetch)()); - } - }, - [id, queries, filterManager] - ); - - const onSaved = useCallback( - (newSavedQuery: SavedQuery) => { - setSavedQuery({ id, savedQuery: newSavedQuery }); - }, - [id] - ); - - const onSavedQueryUpdated = useCallback( - (savedQueryUpdated: SavedQuery) => { - const isQuickSelection = savedQueryUpdated.attributes.timefilter - ? savedQueryUpdated.attributes.timefilter.from.includes('now') || - savedQueryUpdated.attributes.timefilter.to.includes('now') - : false; - - let updateSearchBar: UpdateReduxSearchBar = { - id, - filters: savedQueryUpdated.attributes.filters || [], - end: toStr != null ? toStr : new Date(end).toISOString(), - start: fromStr != null ? fromStr : new Date(start).toISOString(), - isInvalid: false, - isQuickSelection, - updateTime: false, - filterManager, - }; - - if (savedQueryUpdated.attributes.timefilter) { - updateSearchBar.end = savedQueryUpdated.attributes.timefilter - ? savedQueryUpdated.attributes.timefilter.to - : updateSearchBar.end; - updateSearchBar.start = savedQueryUpdated.attributes.timefilter - ? savedQueryUpdated.attributes.timefilter.from - : updateSearchBar.start; - updateSearchBar.updateTime = true; - } - - updateSearchBar = set('query', savedQueryUpdated.attributes.query, updateSearchBar); - updateSearchBar = set('savedQuery', savedQueryUpdated, updateSearchBar); - - updateSearch(updateSearchBar); - }, - [id, end, fromStr, start, toStr] - ); - - const onClearSavedQuery = useCallback(() => { - if (savedQuery != null) { - updateSearch({ - id, - filters: [], - end: toStr != null ? toStr : new Date(end).toISOString(), - start: fromStr != null ? fromStr : new Date(start).toISOString(), - isInvalid: false, - isQuickSelection: false, - updateTime: false, - query: { - query: '', - language: savedQuery.attributes.query.language, - }, - resetSavedQuery: true, - savedQuery: undefined, - filterManager, - }); - } - }, [id, end, filterManager, fromStr, start, toStr, savedQuery]); - - useEffect(() => { - let isSubscribed = true; - const subscriptions = new Subscription(); - - subscriptions.add( - filterManager.getUpdates$().subscribe({ - next: () => { - if (isSubscribed) { - setSearchBarFilter({ - id, - filters: filterManager.getFilters(), - }); - } - }, - }) - ); - - return () => { - isSubscribed = false; - subscriptions.unsubscribe(); - }; - }, []); - const indexPatterns = useMemo(() => [indexPattern], [indexPattern]); - return ( - - - - ); - } -); - -const makeMapStateToProps = () => { - const getEndSelector = endSelector(); - const getFromStrSelector = fromStrSelector(); - const getIsLoadingSelector = isLoadingSelector(); - const getKindSelector = kindSelector(); - const getQueriesSelector = queriesSelector(); - const getStartSelector = startSelector(); - const getToStrSelector = toStrSelector(); - const getFilterQuerySelector = filterQuerySelector(); - const getSavedQuerySelector = savedQuerySelector(); - return (state: State, { id }: SiemSearchBarProps) => { - const inputsRange: InputsRange = getOr({}, `inputs.${id}`, state); - return { - end: getEndSelector(inputsRange), - fromStr: getFromStrSelector(inputsRange), - filterQuery: getFilterQuerySelector(inputsRange), - isLoading: getIsLoadingSelector(inputsRange), - kind: getKindSelector(inputsRange), - queries: getQueriesSelector(inputsRange), - savedQuery: getSavedQuerySelector(inputsRange), - start: getStartSelector(inputsRange), - toStr: getToStrSelector(inputsRange), - }; - }; -}; - -SearchBarComponent.displayName = 'SiemSearchBar'; - -interface UpdateReduxSearchBar extends OnTimeChangeProps { - id: InputsModelId; - filters?: Filter[]; - filterManager: FilterManager; - query?: Query; - savedQuery?: SavedQuery; - resetSavedQuery?: boolean; - timelineId?: string; - updateTime: boolean; -} - -export const dispatchUpdateSearch = (dispatch: Dispatch) => ({ - end, - filters, - id, - isQuickSelection, - query, - resetSavedQuery, - savedQuery, - start, - timelineId, - filterManager, - updateTime = false, -}: UpdateReduxSearchBar): void => { - if (updateTime) { - const fromDate = formatDate(start); - let toDate = formatDate(end, { roundUp: true }); - if (isQuickSelection) { - dispatch( - inputsActions.setRelativeRangeDatePicker({ - id, - fromStr: start, - toStr: end, - from: fromDate, - to: toDate, - }) - ); - } else { - toDate = formatDate(end); - dispatch( - inputsActions.setAbsoluteRangeDatePicker({ - id, - from: formatDate(start), - to: formatDate(end), - }) - ); - } - if (timelineId != null) { - dispatch( - timelineActions.updateRange({ - id: timelineId, - start: fromDate, - end: toDate, - }) - ); - } - } - if (query != null) { - dispatch( - inputsActions.setFilterQuery({ - id, - ...query, - }) - ); - } - if (filters != null) { - filterManager.setFilters(filters); - } - if (savedQuery != null || resetSavedQuery) { - dispatch(inputsActions.setSavedQuery({ id, savedQuery })); - } - - dispatch(hostsActions.setHostTablesActivePageToZero()); - dispatch(networkActions.setNetworkTablesActivePageToZero()); -}; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - updateSearch: dispatchUpdateSearch(dispatch), - setSavedQuery: ({ id, savedQuery }: { id: InputsModelId; savedQuery: SavedQuery | undefined }) => - dispatch(inputsActions.setSavedQuery({ id, savedQuery })), - setSearchBarFilter: ({ id, filters }: { id: InputsModelId; filters: Filter[] }) => - dispatch(inputsActions.setSearchBarFilter({ id, filters })), -}); - -export const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const SiemSearchBar = connector(SearchBarComponent); diff --git a/x-pack/plugins/siem/public/components/search_bar/selectors.ts b/x-pack/plugins/siem/public/components/search_bar/selectors.ts deleted file mode 100644 index 4e700a46ca0e2d..00000000000000 --- a/x-pack/plugins/siem/public/components/search_bar/selectors.ts +++ /dev/null @@ -1,28 +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 { createSelector } from 'reselect'; -import { InputsRange } from '../../store/inputs/model'; -import { Query, SavedQuery } from '../../../../../../src/plugins/data/public'; - -export { - endSelector, - fromStrSelector, - isLoadingSelector, - kindSelector, - queriesSelector, - startSelector, - toStrSelector, -} from '../super_date_picker/selectors'; - -export const getFilterQuery = (inputState: InputsRange): Query => inputState.query; - -export const getSavedQuery = (inputState: InputsRange): SavedQuery | undefined => - inputState.savedQuery; - -export const filterQuerySelector = () => createSelector(getFilterQuery, filterQuery => filterQuery); - -export const savedQuerySelector = () => createSelector(getSavedQuery, savedQuery => savedQuery); diff --git a/x-pack/plugins/siem/public/components/skeleton_row/index.test.tsx b/x-pack/plugins/siem/public/components/skeleton_row/index.test.tsx deleted file mode 100644 index 0ee54a1a20003a..00000000000000 --- a/x-pack/plugins/siem/public/components/skeleton_row/index.test.tsx +++ /dev/null @@ -1,45 +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 { mount, shallow } from 'enzyme'; -import React from 'react'; - -import { TestProviders } from '../../mock'; -import { SkeletonRow } from './index'; - -describe('SkeletonRow', () => { - test('it renders', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - - test('it renders the correct number of cells if cellCount is specified', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('.siemSkeletonRow__cell')).toHaveLength(10); - }); - - test('it applies row and cell styles when cellColor/cellMargin/rowHeight/rowPadding provided', () => { - const wrapper = mount( - - - - ); - const siemSkeletonRow = wrapper.find('.siemSkeletonRow').first(); - const siemSkeletonRowCell = wrapper.find('.siemSkeletonRow__cell').last(); - - expect(siemSkeletonRow).toHaveStyleRule('height', '100px'); - expect(siemSkeletonRow).toHaveStyleRule('padding', '10px'); - expect(siemSkeletonRowCell).toHaveStyleRule('background-color', 'red'); - expect(siemSkeletonRowCell).toHaveStyleRule('margin-left', '10px', { - modifier: '& + &', - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/source_destination/index.test.tsx b/x-pack/plugins/siem/public/components/source_destination/index.test.tsx deleted file mode 100644 index 3dee668d66a707..00000000000000 --- a/x-pack/plugins/siem/public/components/source_destination/index.test.tsx +++ /dev/null @@ -1,419 +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 numeral from '@elastic/numeral'; -import { shallow } from 'enzyme'; -import { get } from 'lodash/fp'; -import React from 'react'; - -import { asArrayIfExists } from '../../lib/helpers'; -import { getMockNetflowData } from '../../mock'; -import { TestProviders } from '../../mock/test_providers'; -import { ID_FIELD_NAME } from '../event_details/event_id'; -import { useMountAppended } from '../../utils/use_mount_appended'; -import { DESTINATION_IP_FIELD_NAME, SOURCE_IP_FIELD_NAME } from '../ip'; -import { DESTINATION_PORT_FIELD_NAME, SOURCE_PORT_FIELD_NAME } from '../port'; -import { - DESTINATION_BYTES_FIELD_NAME, - DESTINATION_PACKETS_FIELD_NAME, - SOURCE_BYTES_FIELD_NAME, - SOURCE_PACKETS_FIELD_NAME, -} from '../source_destination/source_destination_arrows'; -import * as i18n from '../timeline/body/renderers/translations'; - -import { SourceDestination } from '.'; -import { - DESTINATION_GEO_CITY_NAME_FIELD_NAME, - DESTINATION_GEO_CONTINENT_NAME_FIELD_NAME, - DESTINATION_GEO_COUNTRY_ISO_CODE_FIELD_NAME, - DESTINATION_GEO_COUNTRY_NAME_FIELD_NAME, - DESTINATION_GEO_REGION_NAME_FIELD_NAME, - SOURCE_GEO_CITY_NAME_FIELD_NAME, - SOURCE_GEO_CONTINENT_NAME_FIELD_NAME, - SOURCE_GEO_COUNTRY_ISO_CODE_FIELD_NAME, - SOURCE_GEO_COUNTRY_NAME_FIELD_NAME, - SOURCE_GEO_REGION_NAME_FIELD_NAME, -} from './geo_fields'; -import { - NETWORK_BYTES_FIELD_NAME, - NETWORK_COMMUNITY_ID_FIELD_NAME, - NETWORK_DIRECTION_FIELD_NAME, - NETWORK_PACKETS_FIELD_NAME, - NETWORK_PROTOCOL_FIELD_NAME, - NETWORK_TRANSPORT_FIELD_NAME, -} from './field_names'; - -const getSourceDestinationInstance = () => ( - -); - -describe('SourceDestination', () => { - const mount = useMountAppended(); - - test('renders correctly against snapshot', () => { - const wrapper = shallow(
{getSourceDestinationInstance()}
); - expect(wrapper).toMatchSnapshot(); - }); - - test('it renders a destination label', () => { - const wrapper = mount({getSourceDestinationInstance()}); - - expect( - wrapper - .find('[data-test-subj="destination-label"]') - .first() - .text() - ).toEqual(i18n.DESTINATION); - }); - - test('it renders destination.bytes', () => { - const wrapper = mount({getSourceDestinationInstance()}); - - expect( - wrapper - .find('[data-test-subj="destination-bytes"]') - .first() - .text() - ).toEqual('40B'); - }); - - test('it renders percent destination.bytes', () => { - const wrapper = mount({getSourceDestinationInstance()}); - const destinationBytes = asArrayIfExists( - get(DESTINATION_BYTES_FIELD_NAME, getMockNetflowData()) - ); - const sumBytes = asArrayIfExists(get(NETWORK_BYTES_FIELD_NAME, getMockNetflowData())); - let percent = ''; - if (destinationBytes != null && sumBytes != null) { - percent = `(${numeral((destinationBytes[0] / sumBytes[0]) * 100).format('0.00')}%)`; - } - - expect( - wrapper - .find('[data-test-subj="destination-bytes-percent"]') - .first() - .text() - ).toEqual(percent); - }); - - test('it renders destination.geo.continent_name', () => { - const wrapper = mount({getSourceDestinationInstance()}); - - expect( - wrapper - .find('[data-test-subj="destination.geo.continent_name"]') - .first() - .text() - ).toEqual('North America'); - }); - - test('it renders destination.geo.country_name', () => { - const wrapper = mount({getSourceDestinationInstance()}); - - expect( - wrapper - .find('[data-test-subj="destination.geo.country_name"]') - .first() - .text() - ).toEqual('United States'); - }); - - test('it renders destination.geo.country_iso_code', () => { - const wrapper = mount({getSourceDestinationInstance()}); - - expect( - wrapper - .find('[data-test-subj="destination.geo.country_iso_code"]') - .first() - .text() - ).toEqual('US'); - }); - - test('it renders destination.geo.region_name', () => { - const wrapper = mount({getSourceDestinationInstance()}); - - expect( - wrapper - .find('[data-test-subj="destination.geo.region_name"]') - .first() - .text() - ).toEqual('New York'); - }); - - test('it renders destination.geo.city_name', () => { - const wrapper = mount({getSourceDestinationInstance()}); - - expect( - wrapper - .find('[data-test-subj="destination.geo.city_name"]') - .first() - .text() - ).toEqual('New York'); - }); - - test('it renders the destination ip and port, separated with a colon', () => { - const wrapper = mount({getSourceDestinationInstance()}); - - expect( - wrapper - .find('[data-test-subj="destination-ip-and-port"]') - .first() - .text() - ).toEqual('10.1.2.3:80'); - }); - - test('it renders destination.packets', () => { - const wrapper = mount({getSourceDestinationInstance()}); - - expect( - wrapper - .find('[data-test-subj="destination-packets"]') - .first() - .text() - ).toEqual('1 pkts'); - }); - - test('it hyperlinks links destination.port to an external service that describes the purpose of the port', () => { - const wrapper = mount({getSourceDestinationInstance()}); - - expect( - wrapper - .find('[data-test-subj="destination-ip-and-port"]') - .find('[data-test-subj="port-or-service-name-link"]') - .first() - .props().href - ).toEqual( - 'https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=80' - ); - }); - - test('it renders network.bytes', () => { - const wrapper = mount({getSourceDestinationInstance()}); - - expect( - wrapper - .find('[data-test-subj="network-bytes"]') - .first() - .text() - ).toEqual('100B'); - }); - - test('it renders network.community_id', () => { - const wrapper = mount({getSourceDestinationInstance()}); - - expect( - wrapper - .find('[data-test-subj="network-community-id"]') - .first() - .text() - ).toEqual('we.live.in.a'); - }); - - test('it renders network.direction', () => { - const wrapper = mount({getSourceDestinationInstance()}); - - expect( - wrapper - .find('[data-test-subj="network-direction"]') - .first() - .text() - ).toEqual('outgoing'); - }); - - test('it renders network.packets', () => { - const wrapper = mount({getSourceDestinationInstance()}); - - expect( - wrapper - .find('[data-test-subj="network-packets"]') - .first() - .text() - ).toEqual('3 pkts'); - }); - - test('it renders network.protocol', () => { - const wrapper = mount({getSourceDestinationInstance()}); - - expect( - wrapper - .find('[data-test-subj="network-protocol"]') - .first() - .text() - ).toEqual('http'); - }); - - test('it renders a source label', () => { - const wrapper = mount({getSourceDestinationInstance()}); - - expect( - wrapper - .find('[data-test-subj="source-label"]') - .first() - .text() - ).toEqual(i18n.SOURCE); - }); - - test('it renders source.bytes', () => { - const wrapper = mount({getSourceDestinationInstance()}); - - expect( - wrapper - .find('[data-test-subj="source-bytes"]') - .first() - .text() - ).toEqual('60B'); - }); - - test('it renders percent source.bytes', () => { - const wrapper = mount({getSourceDestinationInstance()}); - const sourceBytes = asArrayIfExists(get(SOURCE_BYTES_FIELD_NAME, getMockNetflowData())); - const sumBytes = asArrayIfExists(get(NETWORK_BYTES_FIELD_NAME, getMockNetflowData())); - let percent = ''; - if (sourceBytes != null && sumBytes != null) { - percent = `(${numeral((sourceBytes[0] / sumBytes[0]) * 100).format('0.00')}%)`; - } - - expect( - wrapper - .find('[data-test-subj="source-bytes-percent"]') - .first() - .text() - ).toEqual(percent); - }); - - test('it renders source.geo.continent_name', () => { - const wrapper = mount({getSourceDestinationInstance()}); - - expect( - wrapper - .find('[data-test-subj="source.geo.continent_name"]') - .first() - .text() - ).toEqual('North America'); - }); - - test('it renders source.geo.country_name', () => { - const wrapper = mount({getSourceDestinationInstance()}); - - expect( - wrapper - .find('[data-test-subj="source.geo.country_name"]') - .first() - .text() - ).toEqual('United States'); - }); - - test('it renders source.geo.country_iso_code', () => { - const wrapper = mount({getSourceDestinationInstance()}); - - expect( - wrapper - .find('[data-test-subj="source.geo.country_iso_code"]') - .first() - .text() - ).toEqual('US'); - }); - - test('it renders source.geo.region_name', () => { - const wrapper = mount({getSourceDestinationInstance()}); - - expect( - wrapper - .find('[data-test-subj="source.geo.region_name"]') - .first() - .text() - ).toEqual('Georgia'); - }); - - test('it renders source.geo.city_name', () => { - const wrapper = mount({getSourceDestinationInstance()}); - - expect( - wrapper - .find('[data-test-subj="source.geo.city_name"]') - .first() - .text() - ).toEqual('Atlanta'); - }); - - test('it renders the source ip and port, separated with a colon', () => { - const wrapper = mount({getSourceDestinationInstance()}); - - expect( - wrapper - .find('[data-test-subj="source-ip-and-port"]') - .first() - .text() - ).toEqual('192.168.1.2:9987'); - }); - - test('it renders source.packets', () => { - const wrapper = mount({getSourceDestinationInstance()}); - - expect( - wrapper - .find('[data-test-subj="source-packets"]') - .first() - .text() - ).toEqual('2 pkts'); - }); - - test('it renders network.transport', () => { - const wrapper = mount({getSourceDestinationInstance()}); - - expect( - wrapper - .find('[data-test-subj="network-transport"]') - .first() - .text() - ).toEqual('tcp'); - }); -}); diff --git a/x-pack/plugins/siem/public/components/source_destination/network.tsx b/x-pack/plugins/siem/public/components/source_destination/network.tsx deleted file mode 100644 index a0b86b3e9a1338..00000000000000 --- a/x-pack/plugins/siem/public/components/source_destination/network.tsx +++ /dev/null @@ -1,140 +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 { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; -import { uniq } from 'lodash/fp'; -import React from 'react'; -import styled from 'styled-components'; - -import { DirectionBadge } from '../direction'; -import { DefaultDraggable, DraggableBadge } from '../draggables'; - -import * as i18n from './translations'; -import { - NETWORK_BYTES_FIELD_NAME, - NETWORK_COMMUNITY_ID_FIELD_NAME, - NETWORK_PACKETS_FIELD_NAME, - NETWORK_PROTOCOL_FIELD_NAME, - NETWORK_TRANSPORT_FIELD_NAME, -} from './field_names'; -import { PreferenceFormattedBytes } from '../formatted_bytes'; - -const EuiFlexItemMarginRight = styled(EuiFlexItem)` - margin-right: 3px; -`; - -EuiFlexItemMarginRight.displayName = 'EuiFlexItemMarginRight'; - -const Stats = styled(EuiText)` - margin: 0 5px; -`; - -Stats.displayName = 'Stats'; - -/** - * Renders a row of draggable badges containing fields from the - * `Network` category of fields - */ -export const Network = React.memo<{ - bytes?: string[] | null; - communityId?: string[] | null; - contextId: string; - direction?: string[] | null; - eventId: string; - packets?: string[] | null; - protocol?: string[] | null; - transport?: string[] | null; -}>(({ bytes, communityId, contextId, direction, eventId, packets, protocol, transport }) => ( - - {direction != null - ? uniq(direction).map(dir => ( - - - - )) - : null} - - {protocol != null - ? uniq(protocol).map(proto => ( - - - - )) - : null} - - {bytes != null - ? uniq(bytes).map(b => - !isNaN(Number(b)) ? ( - - - - - - - - - - ) : null - ) - : null} - - {packets != null - ? uniq(packets).map(p => ( - - - - {`${p} ${i18n.PACKETS}`} - - - - )) - : null} - - {transport != null - ? uniq(transport).map(trans => ( - - - - )) - : null} - - {communityId != null - ? uniq(communityId).map(trans => ( - - - - )) - : null} - -)); - -Network.displayName = 'Network'; diff --git a/x-pack/plugins/siem/public/components/stat_items/index.test.tsx b/x-pack/plugins/siem/public/components/stat_items/index.test.tsx deleted file mode 100644 index 95ef747bc429ac..00000000000000 --- a/x-pack/plugins/siem/public/components/stat_items/index.test.tsx +++ /dev/null @@ -1,294 +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. - */ - -/* eslint-disable react/display-name */ - -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import { mount, ReactWrapper } from 'enzyme'; -import React from 'react'; -import { ThemeProvider } from 'styled-components'; - -import { - StatItemsComponent, - StatItemsProps, - addValueToFields, - addValueToAreaChart, - addValueToBarChart, - useKpiMatrixStatus, - StatItems, -} from '.'; -import { BarChart } from '../charts/barchart'; -import { AreaChart } from '../charts/areachart'; -import { EuiHorizontalRule } from '@elastic/eui'; -import { fieldTitleChartMapping } from '../page/network/kpi_network'; -import { - mockData, - mockEnableChartsData, - mockNoChartMappings, - mockNarrowDateRange, -} from '../page/network/kpi_network/mock'; -import { mockGlobalState, apolloClientObservable } from '../../mock'; -import { State, createStore } from '../../store'; -import { Provider as ReduxStoreProvider } from 'react-redux'; -import { KpiNetworkData, KpiHostsData } from '../../graphql/types'; - -const from = new Date('2019-06-15T06:00:00.000Z').valueOf(); -const to = new Date('2019-06-18T06:00:00.000Z').valueOf(); - -jest.mock('../charts/areachart', () => { - return { AreaChart: () =>
}; -}); - -jest.mock('../charts/barchart', () => { - return { BarChart: () =>
}; -}); - -describe('Stat Items Component', () => { - const theme = () => ({ eui: euiDarkVars, darkMode: true }); - const state: State = mockGlobalState; - const store = createStore(state, apolloClientObservable); - - describe.each([ - [ - mount( - - - - - - ), - ], - [ - mount( - - - - - - ), - ], - ])('disable charts', wrapper => { - test('it renders the default widget', () => { - expect(wrapper).toMatchSnapshot(); - }); - - test('should render titles', () => { - expect(wrapper.find('[data-test-subj="stat-title"]')).toBeTruthy(); - }); - - test('should not render icons', () => { - expect(wrapper.find('[data-test-subj="stat-icon"]').filter('EuiIcon')).toHaveLength(0); - }); - - test('should not render barChart', () => { - expect(wrapper.find(BarChart)).toHaveLength(0); - }); - - test('should not render areaChart', () => { - expect(wrapper.find(AreaChart)).toHaveLength(0); - }); - - test('should not render spliter', () => { - expect(wrapper.find(EuiHorizontalRule)).toHaveLength(0); - }); - }); - - describe('rendering kpis with charts', () => { - const mockStatItemsData: StatItemsProps = { - areaChart: [ - { - key: 'uniqueSourceIpsHistogram', - value: [ - { x: new Date('2019-05-03T13:00:00.000Z').valueOf(), y: 565975 }, - { x: new Date('2019-05-04T01:00:00.000Z').valueOf(), y: 1084366 }, - { x: new Date('2019-05-04T13:00:00.000Z').valueOf(), y: 12280 }, - ], - color: '#D36086', - }, - { - key: 'uniqueDestinationIpsHistogram', - value: [ - { x: new Date('2019-05-03T13:00:00.000Z').valueOf(), y: 565975 }, - { x: new Date('2019-05-04T01:00:00.000Z').valueOf(), y: 1084366 }, - { x: new Date('2019-05-04T13:00:00.000Z').valueOf(), y: 12280 }, - ], - color: '#9170B8', - }, - ], - barChart: [ - { key: 'uniqueSourceIps', value: [{ x: 'uniqueSourceIps', y: '1714' }], color: '#D36086' }, - { - key: 'uniqueDestinationIps', - value: [{ x: 'uniqueDestinationIps', y: 2354 }], - color: '#9170B8', - }, - ], - description: 'UNIQUE_PRIVATE_IPS', - enableAreaChart: true, - enableBarChart: true, - fields: [ - { - key: 'uniqueSourceIps', - description: 'Source', - value: 1714, - color: '#D36086', - icon: 'cross', - }, - { - key: 'uniqueDestinationIps', - description: 'Dest.', - value: 2359, - color: '#9170B8', - icon: 'cross', - }, - ], - from, - id: 'statItems', - index: 0, - key: 'mock-keys', - to, - narrowDateRange: mockNarrowDateRange, - }; - let wrapper: ReactWrapper; - beforeAll(() => { - wrapper = mount( - - - - ); - }); - test('it renders the default widget', () => { - expect(wrapper).toMatchSnapshot(); - }); - - test('should handle multiple titles', () => { - expect(wrapper.find('[data-test-subj="stat-title"]')).toHaveLength(2); - }); - - test('should render kpi icons', () => { - expect(wrapper.find('[data-test-subj="stat-icon"]').filter('EuiIcon')).toHaveLength(2); - }); - - test('should render barChart', () => { - expect(wrapper.find(BarChart)).toHaveLength(1); - }); - - test('should render areaChart', () => { - expect(wrapper.find(AreaChart)).toHaveLength(1); - }); - - test('should render separator', () => { - expect(wrapper.find(EuiHorizontalRule)).toHaveLength(1); - }); - }); -}); - -describe('addValueToFields', () => { - const mockNetworkMappings = fieldTitleChartMapping[0]; - const mockKpiNetworkData = mockData.KpiNetwork; - test('should update value from data', () => { - const result = addValueToFields(mockNetworkMappings.fields, mockKpiNetworkData); - expect(result).toEqual(mockEnableChartsData.fields); - }); -}); - -describe('addValueToAreaChart', () => { - const mockNetworkMappings = fieldTitleChartMapping[0]; - const mockKpiNetworkData = mockData.KpiNetwork; - test('should add areaChart from data', () => { - const result = addValueToAreaChart(mockNetworkMappings.fields, mockKpiNetworkData); - expect(result).toEqual(mockEnableChartsData.areaChart); - }); -}); - -describe('addValueToBarChart', () => { - const mockNetworkMappings = fieldTitleChartMapping[0]; - const mockKpiNetworkData = mockData.KpiNetwork; - test('should add areaChart from data', () => { - const result = addValueToBarChart(mockNetworkMappings.fields, mockKpiNetworkData); - expect(result).toEqual(mockEnableChartsData.barChart); - }); -}); - -describe('useKpiMatrixStatus', () => { - const mockNetworkMappings = fieldTitleChartMapping; - const mockKpiNetworkData = mockData.KpiNetwork; - const MockChildComponent = (mappedStatItemProps: StatItemsProps) => ; - const MockHookWrapperComponent = ({ - fieldsMapping, - data, - }: { - fieldsMapping: Readonly; - data: KpiNetworkData | KpiHostsData; - }) => { - const statItemsProps: StatItemsProps[] = useKpiMatrixStatus( - fieldsMapping, - data, - 'statItem', - from, - to, - mockNarrowDateRange - ); - - return ( -
- {statItemsProps.map(mappedStatItemProps => { - return ; - })} -
- ); - }; - - test('it updates status correctly', () => { - const wrapper = mount( - <> - - - ); - - expect(wrapper.find('MockChildComponent').get(0).props).toEqual(mockEnableChartsData); - }); - - test('it should not append areaChart if enableAreaChart is off', () => { - const wrapper = mount( - <> - - - ); - - expect(wrapper.find('MockChildComponent').get(0).props.areaChart).toBeUndefined(); - }); - - test('it should not append barChart if enableBarChart is off', () => { - const wrapper = mount( - <> - - - ); - - expect(wrapper.find('MockChildComponent').get(0).props.barChart).toBeUndefined(); - }); -}); diff --git a/x-pack/plugins/siem/public/components/stat_items/index.tsx b/x-pack/plugins/siem/public/components/stat_items/index.tsx deleted file mode 100644 index 3ebcba0a85a40d..00000000000000 --- a/x-pack/plugins/siem/public/components/stat_items/index.tsx +++ /dev/null @@ -1,286 +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 { ScaleType, Rotation, BrushEndListener, ElementClickListener } from '@elastic/charts'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiPanel, - EuiHorizontalRule, - EuiIcon, - EuiTitle, - IconType, -} from '@elastic/eui'; -import { get, getOr } from 'lodash/fp'; -import React, { useState, useEffect } from 'react'; -import styled from 'styled-components'; - -import { KpiHostsData, KpiNetworkData } from '../../graphql/types'; -import { AreaChart } from '../charts/areachart'; -import { BarChart } from '../charts/barchart'; -import { ChartSeriesData, ChartData, ChartSeriesConfigs, UpdateDateRange } from '../charts/common'; -import { histogramDateTimeFormatter } from '../utils'; -import { getEmptyTagValue } from '../empty_value'; - -import { InspectButton, InspectButtonContainer } from '../inspect'; - -const FlexItem = styled(EuiFlexItem)` - min-width: 0; -`; - -FlexItem.displayName = 'FlexItem'; - -const StatValue = styled(EuiTitle)` - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -`; - -StatValue.displayName = 'StatValue'; - -interface StatItem { - color?: string; - description?: string; - icon?: IconType; - key: string; - name?: string; - value: number | undefined | null; -} - -export interface StatItems { - areachartConfigs?: ChartSeriesConfigs; - barchartConfigs?: ChartSeriesConfigs; - description?: string; - enableAreaChart?: boolean; - enableBarChart?: boolean; - fields: StatItem[]; - grow?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | true | false | null; - index: number; - key: string; - statKey?: string; -} - -export interface StatItemsProps extends StatItems { - areaChart?: ChartSeriesData[]; - barChart?: ChartSeriesData[]; - from: number; - id: string; - narrowDateRange: UpdateDateRange; - to: number; -} - -export const numberFormatter = (value: string | number): string => value.toLocaleString(); -const statItemBarchartRotation: Rotation = 90; -const statItemChartCustomHeight = 74; - -export const areachartConfigs = (config?: { - xTickFormatter: (value: number) => string; - onBrushEnd?: BrushEndListener; -}) => ({ - series: { - xScaleType: ScaleType.Time, - yScaleType: ScaleType.Linear, - }, - axis: { - xTickFormatter: get('xTickFormatter', config), - yTickFormatter: numberFormatter, - }, - settings: { - onBrushEnd: getOr(() => {}, 'onBrushEnd', config), - }, - customHeight: statItemChartCustomHeight, -}); - -export const barchartConfigs = (config?: { onElementClick?: ElementClickListener }) => ({ - series: { - xScaleType: ScaleType.Ordinal, - yScaleType: ScaleType.Linear, - stackAccessors: ['y0'], - }, - axis: { - xTickFormatter: numberFormatter, - }, - settings: { - onElementClick: getOr(() => {}, 'onElementClick', config), - rotation: statItemBarchartRotation, - }, - customHeight: statItemChartCustomHeight, -}); - -export const addValueToFields = ( - fields: StatItem[], - data: KpiHostsData | KpiNetworkData -): StatItem[] => fields.map(field => ({ ...field, value: get(field.key, data) })); - -export const addValueToAreaChart = ( - fields: StatItem[], - data: KpiHostsData | KpiNetworkData -): ChartSeriesData[] => - fields - .filter(field => get(`${field.key}Histogram`, data) != null) - .map(field => ({ - ...field, - value: get(`${field.key}Histogram`, data), - key: `${field.key}Histogram`, - })); - -export const addValueToBarChart = ( - fields: StatItem[], - data: KpiHostsData | KpiNetworkData -): ChartSeriesData[] => { - if (fields.length === 0) return []; - return fields.reduce((acc: ChartSeriesData[], field: StatItem, idx: number) => { - const { key, color } = field; - const y: number | null = getOr(null, key, data); - const x: string = get(`${idx}.name`, fields) || getOr('', `${idx}.description`, fields); - const value: [ChartData] = [ - { - x, - y, - g: key, - y0: 0, - }, - ]; - - return [ - ...acc, - { - key, - color, - value, - }, - ]; - }, []); -}; - -export const useKpiMatrixStatus = ( - mappings: Readonly, - data: KpiHostsData | KpiNetworkData, - id: string, - from: number, - to: number, - narrowDateRange: UpdateDateRange -): StatItemsProps[] => { - const [statItemsProps, setStatItemsProps] = useState(mappings as StatItemsProps[]); - - useEffect(() => { - setStatItemsProps( - mappings.map(stat => { - return { - ...stat, - areaChart: stat.enableAreaChart ? addValueToAreaChart(stat.fields, data) : undefined, - barChart: stat.enableBarChart ? addValueToBarChart(stat.fields, data) : undefined, - fields: addValueToFields(stat.fields, data), - id, - key: `kpi-summary-${stat.key}`, - statKey: `${stat.key}`, - from, - to, - narrowDateRange, - }; - }) - ); - }, [data]); - - return statItemsProps; -}; - -export const StatItemsComponent = React.memo( - ({ - areaChart, - barChart, - description, - enableAreaChart, - enableBarChart, - fields, - from, - grow, - id, - index, - narrowDateRange, - statKey = 'item', - to, - }) => { - const isBarChartDataAvailable = - barChart && - barChart.length && - barChart.every(item => item.value != null && item.value.length > 0); - const isAreaChartDataAvailable = - areaChart && - areaChart.length && - areaChart.every(item => item.value != null && item.value.length > 0); - - return ( - - - - - - -
{description}
-
-
- - - -
- - - {fields.map(field => ( - - - {(isAreaChartDataAvailable || isBarChartDataAvailable) && field.icon && ( - - - - )} - - - -

- {field.value != null ? field.value.toLocaleString() : getEmptyTagValue()}{' '} - {field.description} -

-
-
-
-
- ))} -
- - {(enableAreaChart || enableBarChart) && } - - {enableBarChart && ( - - - - )} - - {enableAreaChart && from != null && to != null && ( - - - - )} - -
-
-
- ); - } -); - -StatItemsComponent.displayName = 'StatItemsComponent'; diff --git a/x-pack/plugins/siem/public/components/super_date_picker/index.test.tsx b/x-pack/plugins/siem/public/components/super_date_picker/index.test.tsx deleted file mode 100644 index b6b515ceeffa61..00000000000000 --- a/x-pack/plugins/siem/public/components/super_date_picker/index.test.tsx +++ /dev/null @@ -1,443 +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 { mount } from 'enzyme'; -import React from 'react'; -import { Provider as ReduxStoreProvider } from 'react-redux'; - -import { DEFAULT_TIMEPICKER_QUICK_RANGES } from '../../../common/constants'; -import { useUiSetting$ } from '../../lib/kibana'; -import { apolloClientObservable, mockGlobalState } from '../../mock'; -import { createUseUiSetting$Mock } from '../../mock/kibana_react'; -import { createStore, State } from '../../store'; - -import { SuperDatePicker, makeMapStateToProps } from '.'; -import { cloneDeep } from 'lodash/fp'; - -jest.mock('../../lib/kibana'); -const mockUseUiSetting$ = useUiSetting$ as jest.Mock; -const timepickerRanges = [ - { - from: 'now/d', - to: 'now/d', - display: 'Today', - }, - { - from: 'now/w', - to: 'now/w', - display: 'This week', - }, - { - from: 'now-15m', - to: 'now', - display: 'Last 15 minutes', - }, - { - from: 'now-30m', - to: 'now', - display: 'Last 30 minutes', - }, - { - from: 'now-1h', - to: 'now', - display: 'Last 1 hour', - }, - { - from: 'now-24h', - to: 'now', - display: 'Last 24 hours', - }, - { - from: 'now-7d', - to: 'now', - display: 'Last 7 days', - }, - { - from: 'now-30d', - to: 'now', - display: 'Last 30 days', - }, - { - from: 'now-90d', - to: 'now', - display: 'Last 90 days', - }, - { - from: 'now-1y', - to: 'now', - display: 'Last 1 year', - }, -]; - -describe('SIEM Super Date Picker', () => { - describe('#SuperDatePicker', () => { - const state: State = mockGlobalState; - let store = createStore(state, apolloClientObservable); - - beforeEach(() => { - jest.clearAllMocks(); - store = createStore(state, apolloClientObservable); - mockUseUiSetting$.mockImplementation((key, defaultValue) => { - const useUiSetting$Mock = createUseUiSetting$Mock(); - - return key === DEFAULT_TIMEPICKER_QUICK_RANGES - ? [timepickerRanges, jest.fn()] - : useUiSetting$Mock(key, defaultValue); - }); - }); - - describe('Pick Relative Date', () => { - let wrapper = mount( - - - - ); - beforeEach(() => { - wrapper = mount( - - - - ); - wrapper - .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') - .first() - .simulate('click'); - wrapper.update(); - - wrapper - .find('button.euiQuickSelect__applyButton') - .first() - .simulate('click'); - wrapper.update(); - }); - - test('Make Sure it is relative date', () => { - expect(store.getState().inputs.global.timerange.kind).toBe('relative'); - }); - - test('Make Sure it is last 24 hours date', () => { - expect(store.getState().inputs.global.timerange.fromStr).toBe('now-24h'); - expect(store.getState().inputs.global.timerange.toStr).toBe('now'); - }); - - test('Make Sure it is Today date', () => { - wrapper - .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') - .first() - .simulate('click'); - wrapper.update(); - - wrapper - .find('[data-test-subj="superDatePickerCommonlyUsed_Today"]') - .first() - .simulate('click'); - wrapper.update(); - expect(store.getState().inputs.global.timerange.fromStr).toBe('now/d'); - expect(store.getState().inputs.global.timerange.toStr).toBe('now/d'); - }); - - test('Make Sure to (end date) is superior than from (start date)', () => { - expect(store.getState().inputs.global.timerange.to).toBeGreaterThan( - store.getState().inputs.global.timerange.from - ); - }); - }); - - describe('Recently used date ranges', () => { - let wrapper = mount( - - - - ); - beforeEach(() => { - wrapper = mount( - - - - ); - wrapper - .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') - .first() - .simulate('click'); - wrapper.update(); - - wrapper - .find('[data-test-subj="superDatePickerCommonlyUsed_Today"]') - .first() - .simulate('click'); - wrapper.update(); - }); - - test('Today is in Recently used date ranges', () => { - expect( - wrapper - .find('div.euiQuickSelectPopover__section') - .at(1) - .text() - ).toBe('Today'); - }); - - test('Today and Last 24 hours are in Recently used date ranges', () => { - wrapper - .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') - .first() - .simulate('click'); - wrapper.update(); - - wrapper - .find('button.euiQuickSelect__applyButton') - .first() - .simulate('click'); - wrapper.update(); - - expect( - wrapper - .find('div.euiQuickSelectPopover__section') - .at(1) - .text() - ).toBe('Last 24 hoursToday'); - }); - - test('Make sure that it does not add any duplicate if you click again on today', () => { - wrapper - .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') - .first() - .simulate('click'); - wrapper.update(); - - wrapper - .find('[data-test-subj="superDatePickerCommonlyUsed_Today"]') - .first() - .simulate('click'); - wrapper.update(); - - expect( - wrapper - .find('div.euiQuickSelectPopover__section') - .at(1) - .text() - ).toBe('Today'); - }); - }); - - describe('Refresh Every', () => { - let wrapper = mount( - - - - ); - beforeEach(() => { - wrapper = mount( - - - - ); - wrapper - .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') - .first() - .simulate('click'); - wrapper.update(); - - const wrapperFixedEuiFieldSearch = wrapper.find( - 'input[data-test-subj="superDatePickerRefreshIntervalInput"]' - ); - - wrapperFixedEuiFieldSearch.simulate('change', { target: { value: '2' } }); - wrapper.update(); - - wrapper - .find('[data-test-subj="superDatePickerToggleRefreshButton"]') - .first() - .simulate('click'); - wrapper.update(); - }); - - test('Make sure the duration get updated to 2 minutes === 120000ms', () => { - expect(store.getState().inputs.global.policy.duration).toEqual(120000); - }); - - test('Make sure the stream live started', () => { - expect(store.getState().inputs.global.policy.kind).toBe('interval'); - }); - - test('Make sure we can stop the stream live', () => { - wrapper - .find('[data-test-subj="superDatePickerToggleQuickMenuButton"]') - .first() - .simulate('click'); - wrapper.update(); - - wrapper - .find('[data-test-subj="superDatePickerToggleRefreshButton"]') - .first() - .simulate('click'); - wrapper.update(); - - expect(store.getState().inputs.global.policy.kind).toBe('manual'); - }); - }); - - describe('Pick Absolute Date', () => { - let wrapper = mount( - - - - ); - beforeEach(() => { - wrapper = mount( - - - - ); - wrapper - .find('[data-test-subj="superDatePickerShowDatesButton"]') - .first() - .simulate('click'); - wrapper.update(); - - wrapper - .find('[data-test-subj="superDatePickerstartDatePopoverButton"]') - .first() - .simulate('click'); - wrapper.update(); - - wrapper - .find('[data-test-subj="superDatePickerAbsoluteTab"]') - .first() - .simulate('click'); - wrapper.update(); - - wrapper - .find('button.react-datepicker__navigation--previous') - .first() - .simulate('click'); - wrapper.update(); - - wrapper - .find('div.react-datepicker__day') - .at(1) - .simulate('click'); - wrapper.update(); - - wrapper - .find('button[data-test-subj="superDatePickerApplyTimeButton"]') - .first() - .simulate('click'); - wrapper.update(); - }); - }); - - describe('#makeMapStateToProps', () => { - test('it should return the same shallow references given the same input twice', () => { - const mapStateToProps = makeMapStateToProps(); - const props1 = mapStateToProps(state, { id: 'global' }); - const props2 = mapStateToProps(state, { id: 'global' }); - Object.keys(props1).forEach(key => { - expect((props1 as Record)[key]).toBe((props2 as Record)[key]); - }); - }); - - test('it should not return the same reference if policy kind is different', () => { - const mapStateToProps = makeMapStateToProps(); - const props1 = mapStateToProps(state, { id: 'global' }); - const clone = cloneDeep(state); - clone.inputs.global.policy.kind = 'interval'; - const props2 = mapStateToProps(clone, { id: 'global' }); - expect(props1.policy).not.toBe(props2.policy); - }); - - test('it should not return the same reference if duration is different', () => { - const mapStateToProps = makeMapStateToProps(); - const props1 = mapStateToProps(state, { id: 'global' }); - const clone = cloneDeep(state); - clone.inputs.global.policy.duration = 99999; - const props2 = mapStateToProps(clone, { id: 'global' }); - expect(props1.duration).not.toBe(props2.duration); - }); - - test('it should not return the same reference if timerange kind is different', () => { - const mapStateToProps = makeMapStateToProps(); - const props1 = mapStateToProps(state, { id: 'global' }); - const clone = cloneDeep(state); - clone.inputs.global.timerange.kind = 'absolute'; - const props2 = mapStateToProps(clone, { id: 'global' }); - expect(props1.kind).not.toBe(props2.kind); - }); - - test('it should not return the same reference if timerange from is different', () => { - const mapStateToProps = makeMapStateToProps(); - const props1 = mapStateToProps(state, { id: 'global' }); - const clone = cloneDeep(state); - clone.inputs.global.timerange.from = 999; - const props2 = mapStateToProps(clone, { id: 'global' }); - expect(props1.start).not.toBe(props2.start); - }); - - test('it should not return the same reference if timerange to is different', () => { - const mapStateToProps = makeMapStateToProps(); - const props1 = mapStateToProps(state, { id: 'global' }); - const clone = cloneDeep(state); - clone.inputs.global.timerange.to = 999; - const props2 = mapStateToProps(clone, { id: 'global' }); - expect(props1.end).not.toBe(props2.end); - }); - - test('it should not return the same reference of toStr if toStr different', () => { - const mapStateToProps = makeMapStateToProps(); - const props1 = mapStateToProps(state, { id: 'global' }); - const clone = cloneDeep(state); - clone.inputs.global.timerange.toStr = 'some other string'; - const props2 = mapStateToProps(clone, { id: 'global' }); - expect(props1.toStr).not.toBe(props2.toStr); - }); - - test('it should not return the same reference of fromStr if fromStr different', () => { - const mapStateToProps = makeMapStateToProps(); - const props1 = mapStateToProps(state, { id: 'global' }); - const clone = cloneDeep(state); - clone.inputs.global.timerange.fromStr = 'some other string'; - const props2 = mapStateToProps(clone, { id: 'global' }); - expect(props1.fromStr).not.toBe(props2.fromStr); - }); - - test('it should not return the same reference of isLoadingSelector if the query different', () => { - const mapStateToProps = makeMapStateToProps(); - const props1 = mapStateToProps(state, { id: 'global' }); - const clone = cloneDeep(state); - clone.inputs.global.queries = [ - { - loading: true, - id: '1', - inspect: { dsl: [], response: [] }, - isInspected: false, - refetch: null, - selectedInspectIndex: 0, - }, - ]; - const props2 = mapStateToProps(clone, { id: 'global' }); - expect(props1.isLoading).not.toBe(props2.isLoading); - }); - - test('it should not return the same reference of refetchSelector if the query different', () => { - const mapStateToProps = makeMapStateToProps(); - const props1 = mapStateToProps(state, { id: 'global' }); - const clone = cloneDeep(state); - clone.inputs.global.queries = [ - { - loading: true, - id: '1', - inspect: { dsl: [], response: [] }, - isInspected: false, - refetch: null, - selectedInspectIndex: 0, - }, - ]; - const props2 = mapStateToProps(clone, { id: 'global' }); - expect(props1.queries).not.toBe(props2.queries); - }); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/super_date_picker/index.tsx b/x-pack/plugins/siem/public/components/super_date_picker/index.tsx deleted file mode 100644 index ad38a7d61bcba6..00000000000000 --- a/x-pack/plugins/siem/public/components/super_date_picker/index.tsx +++ /dev/null @@ -1,313 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import dateMath from '@elastic/datemath'; -import { - EuiSuperDatePicker, - OnRefreshChangeProps, - EuiSuperDatePickerRecentRange, - OnRefreshProps, - OnTimeChangeProps, -} from '@elastic/eui'; -import { getOr, take, isEmpty } from 'lodash/fp'; -import React, { useState, useCallback } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import { Dispatch } from 'redux'; - -import { DEFAULT_TIMEPICKER_QUICK_RANGES } from '../../../common/constants'; -import { useUiSetting$ } from '../../lib/kibana'; -import { inputsModel, State } from '../../store'; -import { inputsActions, timelineActions } from '../../store/actions'; -import { InputsModelId } from '../../store/inputs/constants'; -import { - policySelector, - durationSelector, - kindSelector, - startSelector, - endSelector, - fromStrSelector, - toStrSelector, - isLoadingSelector, - queriesSelector, - kqlQuerySelector, -} from './selectors'; -import { InputsRange } from '../../store/inputs/model'; - -const MAX_RECENTLY_USED_RANGES = 9; - -interface Range { - from: string; - to: string; - display: string; -} - -interface UpdateReduxTime extends OnTimeChangeProps { - id: InputsModelId; - kql?: inputsModel.GlobalKqlQuery | undefined; - timelineId?: string; -} - -interface ReturnUpdateReduxTime { - kqlHaveBeenUpdated: boolean; -} - -export type DispatchUpdateReduxTime = ({ - end, - id, - isQuickSelection, - kql, - start, - timelineId, -}: UpdateReduxTime) => ReturnUpdateReduxTime; - -interface OwnProps { - disabled?: boolean; - id: InputsModelId; - timelineId?: string; -} - -export type SuperDatePickerProps = OwnProps & PropsFromRedux; - -export const SuperDatePickerComponent = React.memo( - ({ - duration, - end, - fromStr, - id, - isLoading, - kind, - kqlQuery, - policy, - queries, - setDuration, - start, - startAutoReload, - stopAutoReload, - timelineId, - toStr, - updateReduxTime, - }) => { - const [isQuickSelection, setIsQuickSelection] = useState(true); - const [recentlyUsedRanges, setRecentlyUsedRanges] = useState( - [] - ); - const onRefresh = useCallback( - ({ start: newStart, end: newEnd }: OnRefreshProps): void => { - const { kqlHaveBeenUpdated } = updateReduxTime({ - end: newEnd, - id, - isInvalid: false, - isQuickSelection, - kql: kqlQuery, - start: newStart, - timelineId, - }); - const currentStart = formatDate(newStart); - const currentEnd = isQuickSelection - ? formatDate(newEnd, { roundUp: true }) - : formatDate(newEnd); - if ( - !kqlHaveBeenUpdated && - (!isQuickSelection || (start === currentStart && end === currentEnd)) - ) { - refetchQuery(queries); - } - }, - [end, id, isQuickSelection, kqlQuery, start, timelineId] - ); - - const onRefreshChange = useCallback( - ({ isPaused, refreshInterval }: OnRefreshChangeProps): void => { - if (duration !== refreshInterval) { - setDuration({ id, duration: refreshInterval }); - } - - if (isPaused && policy === 'interval') { - stopAutoReload({ id }); - } else if (!isPaused && policy === 'manual') { - startAutoReload({ id }); - } - - if (!isPaused && (!isQuickSelection || (isQuickSelection && toStr !== 'now'))) { - refetchQuery(queries); - } - }, - [id, isQuickSelection, duration, policy, toStr] - ); - - const refetchQuery = (newQueries: inputsModel.GlobalGraphqlQuery[]) => { - newQueries.forEach(q => q.refetch && (q.refetch as inputsModel.Refetch)()); - }; - - const onTimeChange = useCallback( - ({ - start: newStart, - end: newEnd, - isQuickSelection: newIsQuickSelection, - isInvalid, - }: OnTimeChangeProps) => { - if (!isInvalid) { - updateReduxTime({ - end: newEnd, - id, - isInvalid, - isQuickSelection: newIsQuickSelection, - kql: kqlQuery, - start: newStart, - timelineId, - }); - const newRecentlyUsedRanges = [ - { start: newStart, end: newEnd }, - ...take( - MAX_RECENTLY_USED_RANGES, - recentlyUsedRanges.filter( - recentlyUsedRange => - !(recentlyUsedRange.start === newStart && recentlyUsedRange.end === newEnd) - ) - ), - ]; - - setRecentlyUsedRanges(newRecentlyUsedRanges); - setIsQuickSelection(newIsQuickSelection); - } - }, - [recentlyUsedRanges, kqlQuery] - ); - - const endDate = kind === 'relative' ? toStr : new Date(end).toISOString(); - const startDate = kind === 'relative' ? fromStr : new Date(start).toISOString(); - - const [quickRanges] = useUiSetting$(DEFAULT_TIMEPICKER_QUICK_RANGES); - const commonlyUsedRanges = isEmpty(quickRanges) - ? [] - : quickRanges.map(({ from, to, display }) => ({ - start: from, - end: to, - label: display, - })); - - return ( - - ); - } -); - -export const formatDate = ( - date: string, - options?: { - roundUp?: boolean; - } -) => { - const momentDate = dateMath.parse(date, options); - return momentDate != null && momentDate.isValid() ? momentDate.valueOf() : 0; -}; - -export const dispatchUpdateReduxTime = (dispatch: Dispatch) => ({ - end, - id, - isQuickSelection, - kql, - start, - timelineId, -}: UpdateReduxTime): ReturnUpdateReduxTime => { - const fromDate = formatDate(start); - let toDate = formatDate(end, { roundUp: true }); - if (isQuickSelection) { - dispatch( - inputsActions.setRelativeRangeDatePicker({ - id, - fromStr: start, - toStr: end, - from: fromDate, - to: toDate, - }) - ); - } else { - toDate = formatDate(end); - dispatch( - inputsActions.setAbsoluteRangeDatePicker({ - id, - from: formatDate(start), - to: formatDate(end), - }) - ); - } - if (timelineId != null) { - dispatch( - timelineActions.updateRange({ - id: timelineId, - start: fromDate, - end: toDate, - }) - ); - } - if (kql) { - return { - kqlHaveBeenUpdated: kql.refetch(dispatch), - }; - } - - return { - kqlHaveBeenUpdated: false, - }; -}; - -export const makeMapStateToProps = () => { - const getDurationSelector = durationSelector(); - const getEndSelector = endSelector(); - const getFromStrSelector = fromStrSelector(); - const getIsLoadingSelector = isLoadingSelector(); - const getKindSelector = kindSelector(); - const getKqlQuerySelector = kqlQuerySelector(); - const getPolicySelector = policySelector(); - const getQueriesSelector = queriesSelector(); - const getStartSelector = startSelector(); - const getToStrSelector = toStrSelector(); - return (state: State, { id }: OwnProps) => { - const inputsRange: InputsRange = getOr({}, `inputs.${id}`, state); - return { - duration: getDurationSelector(inputsRange), - end: getEndSelector(inputsRange), - fromStr: getFromStrSelector(inputsRange), - isLoading: getIsLoadingSelector(inputsRange), - kind: getKindSelector(inputsRange), - kqlQuery: getKqlQuerySelector(inputsRange) as inputsModel.GlobalKqlQuery, - policy: getPolicySelector(inputsRange), - queries: getQueriesSelector(inputsRange) as inputsModel.GlobalGraphqlQuery[], - start: getStartSelector(inputsRange), - toStr: getToStrSelector(inputsRange), - }; - }; -}; - -SuperDatePickerComponent.displayName = 'SuperDatePickerComponent'; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - startAutoReload: ({ id }: { id: InputsModelId }) => - dispatch(inputsActions.startAutoReload({ id })), - stopAutoReload: ({ id }: { id: InputsModelId }) => dispatch(inputsActions.stopAutoReload({ id })), - setDuration: ({ id, duration }: { id: InputsModelId; duration: number }) => - dispatch(inputsActions.setDuration({ id, duration })), - updateReduxTime: dispatchUpdateReduxTime(dispatch), -}); - -export const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const SuperDatePicker = connector(SuperDatePickerComponent); diff --git a/x-pack/plugins/siem/public/components/tables/helpers.tsx b/x-pack/plugins/siem/public/components/tables/helpers.tsx deleted file mode 100644 index f4f7375c26d14c..00000000000000 --- a/x-pack/plugins/siem/public/components/tables/helpers.tsx +++ /dev/null @@ -1,240 +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 { EuiLink, EuiPopover, EuiToolTip, EuiText, EuiTextColor } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useState } from 'react'; -import styled from 'styled-components'; - -import { DragEffects, DraggableWrapper } from '../drag_and_drop/draggable_wrapper'; -import { escapeDataProviderId } from '../drag_and_drop/helpers'; -import { defaultToEmptyTag, getEmptyTagValue } from '../empty_value'; -import { MoreRowItems, Spacer } from '../page'; -import { IS_OPERATOR } from '../timeline/data_providers/data_provider'; -import { Provider } from '../timeline/data_providers/provider'; - -const Subtext = styled.div` - font-size: ${props => props.theme.eui.euiFontSizeXS}; -`; - -export const getRowItemDraggable = ({ - rowItem, - attrName, - idPrefix, - render, - dragDisplayValue, -}: { - rowItem: string | null | undefined; - attrName: string; - idPrefix: string; - render?: (item: string) => JSX.Element; - displayCount?: number; - dragDisplayValue?: string; - maxOverflow?: number; -}): JSX.Element => { - if (rowItem != null) { - const id = escapeDataProviderId(`${idPrefix}-${attrName}-${rowItem}`); - return ( - - snapshot.isDragging ? ( - - - - ) : ( - <>{render ? render(rowItem) : defaultToEmptyTag(rowItem)} - ) - } - /> - ); - } else { - return getEmptyTagValue(); - } -}; - -export const getRowItemDraggables = ({ - rowItems, - attrName, - idPrefix, - render, - dragDisplayValue, - displayCount = 5, - maxOverflow = 5, -}: { - rowItems: string[] | null | undefined; - attrName: string; - idPrefix: string; - render?: (item: string) => JSX.Element; - displayCount?: number; - dragDisplayValue?: string; - maxOverflow?: number; -}): JSX.Element => { - if (rowItems != null && rowItems.length > 0) { - const draggables = rowItems.slice(0, displayCount).map((rowItem, index) => { - const id = escapeDataProviderId(`${idPrefix}-${attrName}-${rowItem}-${index}`); - return ( - - {index !== 0 && ( - <> - {','} - - - )} - - snapshot.isDragging ? ( - - - - ) : ( - <>{render ? render(rowItem) : defaultToEmptyTag(rowItem)} - ) - } - /> - - ); - }); - - return draggables.length > 0 ? ( - <> - {draggables} {getRowItemOverflow(rowItems, idPrefix, displayCount, maxOverflow)} - - ) : ( - getEmptyTagValue() - ); - } else { - return getEmptyTagValue(); - } -}; - -export const getRowItemOverflow = ( - rowItems: string[], - idPrefix: string, - overflowIndexStart = 5, - maxOverflowItems = 5 -): JSX.Element => { - return ( - <> - {rowItems.length > overflowIndexStart && ( - - -
    - {rowItems - .slice(overflowIndexStart, overflowIndexStart + maxOverflowItems) - .map(rowItem => ( -
  • {defaultToEmptyTag(rowItem)}
  • - ))} -
- - {rowItems.length > overflowIndexStart + maxOverflowItems && ( -

- - {rowItems.length - overflowIndexStart - maxOverflowItems}{' '} - - -

- )} -
-
- )} - - ); -}; - -export const PopoverComponent = ({ - children, - count, - idPrefix, -}: { - children: React.ReactNode; - count: number; - idPrefix: string; -}) => { - const [isOpen, setIsOpen] = useState(false); - - return ( - - setIsOpen(!isOpen)}>{`+${count} More`}} - closePopover={() => setIsOpen(!isOpen)} - id={`${idPrefix}-popover`} - isOpen={isOpen} - > - {children} - - - ); -}; - -PopoverComponent.displayName = 'PopoverComponent'; - -export const Popover = React.memo(PopoverComponent); - -Popover.displayName = 'Popover'; - -export const OverflowFieldComponent = ({ - value, - showToolTip = true, - overflowLength = 50, -}: { - value: string; - showToolTip?: boolean; - overflowLength?: number; -}) => ( - - {showToolTip ? ( - - <>{value.substring(0, overflowLength)} - - ) : ( - <>{value.substring(0, overflowLength)} - )} - {value.length > overflowLength && ( - - - - )} - -); - -OverflowFieldComponent.displayName = 'OverflowFieldComponent'; - -export const OverflowField = React.memo(OverflowFieldComponent); - -OverflowField.displayName = 'OverflowField'; diff --git a/x-pack/plugins/siem/public/components/timeline/auto_save_warning/index.tsx b/x-pack/plugins/siem/public/components/timeline/auto_save_warning/index.tsx deleted file mode 100644 index 90d0738aba72f2..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/auto_save_warning/index.tsx +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - EuiButton, - EuiFlexGroup, - EuiFlexItem, - EuiGlobalToastListToast as Toast, -} from '@elastic/eui'; -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { connect, ConnectedProps } from 'react-redux'; - -import { State, timelineSelectors } from '../../../store'; -import { setTimelineRangeDatePicker as dispatchSetTimelineRangeDatePicker } from '../../../store/inputs/actions'; - -import * as i18n from './translations'; -import { timelineActions } from '../../../store/timeline'; -import { AutoSavedWarningMsg } from '../../../store/timeline/types'; -import { useStateToaster } from '../../toasters'; - -const AutoSaveWarningMsgComponent = React.memo( - ({ - newTimelineModel, - setTimelineRangeDatePicker, - timelineId, - updateAutoSaveMsg, - updateTimeline, - }) => { - const dispatchToaster = useStateToaster()[1]; - if (timelineId != null && newTimelineModel != null) { - const toast: Toast = { - id: 'AutoSaveWarningMsg', - title: i18n.TITLE, - color: 'warning', - iconType: 'alert', - toastLifeTimeMs: 10000, - text: ( - <> -

{i18n.DESCRIPTION}

- - - { - updateTimeline({ id: timelineId, timeline: newTimelineModel }); - updateAutoSaveMsg({ timelineId: null, newTimelineModel: null }); - setTimelineRangeDatePicker({ - from: getOr(0, 'dateRange.start', newTimelineModel), - to: getOr(0, 'dateRange.end', newTimelineModel), - }); - }} - > - {i18n.REFRESH_TIMELINE} - - - - - ), - }; - dispatchToaster({ - type: 'addToaster', - toast, - }); - } - - return null; - } -); - -AutoSaveWarningMsgComponent.displayName = 'AutoSaveWarningMsgComponent'; - -const mapStateToProps = (state: State) => { - const autoSaveMessage: AutoSavedWarningMsg = timelineSelectors.autoSaveMsgSelector(state); - - return { - timelineId: autoSaveMessage.timelineId, - newTimelineModel: autoSaveMessage.newTimelineModel, - }; -}; - -const mapDispatchToProps = { - setTimelineRangeDatePicker: dispatchSetTimelineRangeDatePicker, - updateAutoSaveMsg: timelineActions.updateAutoSaveMsg, - updateTimeline: timelineActions.updateTimeline, -}; - -const connector = connect(mapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const AutoSaveWarningMsg = connector(AutoSaveWarningMsgComponent); diff --git a/x-pack/plugins/siem/public/components/timeline/body/actions/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/actions/index.test.tsx deleted file mode 100644 index 6055745e9378ee..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/body/actions/index.test.tsx +++ /dev/null @@ -1,265 +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 { mount } from 'enzyme'; -import React from 'react'; - -import { TestProviders } from '../../../../mock'; -import { DEFAULT_ACTIONS_COLUMN_WIDTH } from '../constants'; - -import { Actions } from '.'; - -describe('Actions', () => { - test('it renders a checkbox for selecting the event when `showCheckboxes` is `true`', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="select-event"]').exists()).toEqual(true); - }); - - test('it does NOT render a checkbox for selecting the event when `showCheckboxes` is `false`', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="select-event"]').exists()).toBe(false); - }); - - test('it renders a button for expanding the event', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="expand-event"]').exists()).toEqual(true); - }); - - test('it invokes onEventToggled when the button to expand an event is clicked', () => { - const onEventToggled = jest.fn(); - - const wrapper = mount( - - - - ); - - wrapper - .find('[data-test-subj="expand-event"]') - .first() - .simulate('click'); - - expect(onEventToggled).toBeCalled(); - }); - - test('it does NOT render a notes button when isEventsViewer is true', () => { - const toggleShowNotes = jest.fn(); - - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="timeline-notes-button-small"]').exists()).toBe(false); - }); - - test('it invokes toggleShowNotes when the button for adding notes is clicked', () => { - const toggleShowNotes = jest.fn(); - - const wrapper = mount( - - - - ); - - wrapper - .find('[data-test-subj="timeline-notes-button-small"]') - .first() - .simulate('click'); - - expect(toggleShowNotes).toBeCalled(); - }); - - test('it does NOT render a pin button when isEventViewer is true', () => { - const onPinClicked = jest.fn(); - - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="pin"]').exists()).toBe(false); - }); - - test('it invokes onPinClicked when the button for pinning events is clicked', () => { - const onPinClicked = jest.fn(); - - const wrapper = mount( - - - - ); - - wrapper - .find('[data-test-subj="pin"]') - .first() - .simulate('click'); - - expect(onPinClicked).toHaveBeenCalled(); - }); -}); diff --git a/x-pack/plugins/siem/public/components/timeline/body/actions/index.tsx b/x-pack/plugins/siem/public/components/timeline/body/actions/index.tsx deleted file mode 100644 index 030e9be7703ed5..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/body/actions/index.tsx +++ /dev/null @@ -1,177 +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 { EuiButtonIcon, EuiCheckbox, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui'; -import React from 'react'; - -import { Note } from '../../../../lib/note'; -import { AssociateNote, UpdateNote } from '../../../notes/helpers'; -import { Pin } from '../../../pin'; -import { NotesButton } from '../../properties/helpers'; -import { EventsLoading, EventsTd, EventsTdContent, EventsTdGroupActions } from '../../styles'; -import { eventHasNotes, getPinTooltip } from '../helpers'; -import * as i18n from '../translations'; -import { OnRowSelected } from '../../events'; -import { Ecs } from '../../../../graphql/types'; - -export interface TimelineActionProps { - eventId: string; - ecsData: Ecs; -} - -export interface TimelineAction { - getAction: ({ eventId, ecsData }: TimelineActionProps) => JSX.Element; - width: number; - id: string; -} - -interface Props { - actionsColumnWidth: number; - additionalActions?: JSX.Element[]; - associateNote: AssociateNote; - checked: boolean; - onRowSelected: OnRowSelected; - expanded: boolean; - eventId: string; - eventIsPinned: boolean; - getNotesByIds: (noteIds: string[]) => Note[]; - isEventViewer?: boolean; - loading: boolean; - loadingEventIds: Readonly; - noteIds: string[]; - onEventToggled: () => void; - onPinClicked: () => void; - showNotes: boolean; - showCheckboxes: boolean; - toggleShowNotes: () => void; - updateNote: UpdateNote; -} - -const emptyNotes: string[] = []; - -export const Actions = React.memo( - ({ - actionsColumnWidth, - additionalActions, - associateNote, - checked, - expanded, - eventId, - eventIsPinned, - getNotesByIds, - isEventViewer = false, - loading = false, - loadingEventIds, - noteIds, - onEventToggled, - onPinClicked, - onRowSelected, - showCheckboxes, - showNotes, - toggleShowNotes, - updateNote, - }) => ( - - {showCheckboxes && ( - - - {loadingEventIds.includes(eventId) ? ( - - ) : ( - ) => { - onRowSelected({ - eventIds: [eventId], - isSelected: event.currentTarget.checked, - }); - }} - /> - )} - - - )} - - <>{additionalActions} - - - - {loading && } - - {!loading && ( - - )} - - - - {!isEventViewer && ( - <> - - - - - - - - - - - - - - - )} - - ), - (nextProps, prevProps) => { - return ( - prevProps.actionsColumnWidth === nextProps.actionsColumnWidth && - prevProps.checked === nextProps.checked && - prevProps.expanded === nextProps.expanded && - prevProps.eventId === nextProps.eventId && - prevProps.eventIsPinned === nextProps.eventIsPinned && - prevProps.loading === nextProps.loading && - prevProps.loadingEventIds === nextProps.loadingEventIds && - prevProps.noteIds === nextProps.noteIds && - prevProps.onRowSelected === nextProps.onRowSelected && - prevProps.showCheckboxes === nextProps.showCheckboxes && - prevProps.showNotes === nextProps.showNotes - ); - } -); -Actions.displayName = 'Actions'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/column_headers/actions/index.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/actions/index.tsx deleted file mode 100644 index 7a2898d465b225..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/body/column_headers/actions/index.tsx +++ /dev/null @@ -1,63 +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 { EuiButtonIcon } from '@elastic/eui'; -import React from 'react'; - -import { ColumnHeaderOptions } from '../../../../../store/timeline/model'; -import { OnColumnRemoved } from '../../../events'; -import { EventsHeadingExtra, EventsLoading } from '../../../styles'; -import { useTimelineContext } from '../../../timeline_context'; -import { Sort } from '../../sort'; - -import * as i18n from '../translations'; - -interface Props { - header: ColumnHeaderOptions; - onColumnRemoved: OnColumnRemoved; - sort: Sort; -} - -/** Given a `header`, returns the `SortDirection` applicable to it */ - -export const CloseButton = React.memo<{ - columnId: string; - onColumnRemoved: OnColumnRemoved; -}>(({ columnId, onColumnRemoved }) => ( - ) => { - // To avoid a re-sorting when you delete a column - event.preventDefault(); - event.stopPropagation(); - onColumnRemoved(columnId); - }} - /> -)); - -CloseButton.displayName = 'CloseButton'; - -export const Actions = React.memo(({ header, onColumnRemoved, sort }) => { - const { isLoading } = useTimelineContext(); - return ( - <> - {sort.columnId === header.id && isLoading ? ( - - - - ) : ( - - - - )} - - ); -}); - -Actions.displayName = 'Actions'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/column_headers/events_select/helpers.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/events_select/helpers.tsx deleted file mode 100644 index 853c1ec24b7031..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/body/column_headers/events_select/helpers.tsx +++ /dev/null @@ -1,108 +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 { EuiText } from '@elastic/eui'; -import React from 'react'; -import styled from 'styled-components'; - -import { Pin } from '../../../../pin'; - -import * as i18n from './translations'; - -const InputDisplay = styled.div` - width: 5px; -`; - -InputDisplay.displayName = 'InputDisplay'; - -const PinIconContainer = styled.div` - margin-right: 5px; -`; - -PinIconContainer.displayName = 'PinIconContainer'; - -const PinActionItem = styled.div` - display: flex; - flex-direction: row; -`; - -PinActionItem.displayName = 'PinActionItem'; - -export type EventsSelectAction = - | 'select-all' - | 'select-none' - | 'select-pinned' - | 'select-unpinned' - | 'pin-selected' - | 'unpin-selected'; - -export interface EventsSelectOption { - value: EventsSelectAction; - inputDisplay: JSX.Element | string; - disabled?: boolean; - dropdownDisplay: JSX.Element | string; -} - -export const DropdownDisplay = React.memo<{ text: string }>(({ text }) => ( - - {text} - -)); - -DropdownDisplay.displayName = 'DropdownDisplay'; - -export const getEventsSelectOptions = (): EventsSelectOption[] => [ - { - inputDisplay: , - disabled: true, - dropdownDisplay: , - value: 'select-all', - }, - { - inputDisplay: , - disabled: true, - dropdownDisplay: , - value: 'select-none', - }, - { - inputDisplay: , - disabled: true, - dropdownDisplay: , - value: 'select-pinned', - }, - { - inputDisplay: , - disabled: true, - dropdownDisplay: , - value: 'select-unpinned', - }, - { - inputDisplay: , - disabled: true, - dropdownDisplay: ( - - - - - - - ), - value: 'pin-selected', - }, - { - inputDisplay: , - disabled: true, - dropdownDisplay: ( - - - - - - - ), - value: 'unpin-selected', - }, -]; diff --git a/x-pack/plugins/siem/public/components/timeline/body/column_headers/filter/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/filter/index.test.tsx deleted file mode 100644 index f0f6ce8d0ed6ff..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/body/column_headers/filter/index.test.tsx +++ /dev/null @@ -1,57 +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 { mount, shallow } from 'enzyme'; -import React from 'react'; - -import { ColumnHeaderType } from '../../../../../store/timeline/model'; -import { defaultHeaders } from '../default_headers'; - -import { Filter } from '.'; - -const textFilter: ColumnHeaderType = 'text-filter'; -const notFiltered: ColumnHeaderType = 'not-filtered'; - -describe('Filter', () => { - test('renders correctly against snapshot', () => { - const textFilterColumnHeader = { - ...defaultHeaders[0], - columnHeaderType: textFilter, - }; - - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - - describe('rendering', () => { - test('it renders a text filter when the columnHeaderType is "text-filter"', () => { - const textFilterColumnHeader = { - ...defaultHeaders[0], - columnHeaderType: textFilter, - }; - - const wrapper = mount(); - - expect( - wrapper - .find('[data-test-subj="textFilter"]') - .first() - .props() - ).toHaveProperty('placeholder'); - }); - - test('it does NOT render a filter when the columnHeaderType is "not-filtered"', () => { - const notFilteredHeader = { - ...defaultHeaders[0], - columnHeaderType: notFiltered, - }; - - const wrapper = mount(); - - expect(wrapper.find('[data-test-subj="textFilter"]').exists()).toEqual(false); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/timeline/body/column_headers/filter/index.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/filter/index.tsx deleted file mode 100644 index 911a309edfd987..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/body/column_headers/filter/index.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 { noop } from 'lodash/fp'; -import React from 'react'; - -import { OnFilterChange } from '../../../events'; -import { ColumnHeaderOptions } from '../../../../../store/timeline/model'; -import { TextFilter } from '../text_filter'; - -interface Props { - header: ColumnHeaderOptions; - onFilterChange?: OnFilterChange; -} - -/** Renders a header's filter, based on the `columnHeaderType` */ -export const Filter = React.memo(({ header, onFilterChange = noop }) => { - switch (header.columnHeaderType) { - case 'text-filter': - return ( - - ); - case 'not-filtered': // fall through - default: - return null; - } -}); - -Filter.displayName = 'Filter'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/column_headers/header/helpers.ts b/x-pack/plugins/siem/public/components/timeline/body/column_headers/header/helpers.ts deleted file mode 100644 index 47ce21e4c96371..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/body/column_headers/header/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 { Direction } from '../../../../../graphql/types'; -import { assertUnreachable } from '../../../../../lib/helpers'; -import { ColumnHeaderOptions } from '../../../../../store/timeline/model'; -import { Sort, SortDirection } from '../../sort'; - -interface GetNewSortDirectionOnClickParams { - clickedHeader: ColumnHeaderOptions; - currentSort: Sort; -} - -/** Given a `header`, returns the `SortDirection` applicable to it */ -export const getNewSortDirectionOnClick = ({ - clickedHeader, - currentSort, -}: GetNewSortDirectionOnClickParams): Direction => - clickedHeader.id === currentSort.columnId ? getNextSortDirection(currentSort) : Direction.desc; - -/** Given a current sort direction, it returns the next sort direction */ -export const getNextSortDirection = (currentSort: Sort): Direction => { - switch (currentSort.sortDirection) { - case Direction.desc: - return Direction.asc; - case Direction.asc: - return Direction.desc; - case 'none': - return Direction.desc; - default: - return assertUnreachable(currentSort.sortDirection, 'Unhandled sort direction'); - } -}; - -interface GetSortDirectionParams { - header: ColumnHeaderOptions; - sort: Sort; -} - -export const getSortDirection = ({ header, sort }: GetSortDirectionParams): SortDirection => - header.id === sort.columnId ? sort.sortDirection : 'none'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/column_headers/header/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/header/index.test.tsx deleted file mode 100644 index 80ae2aab0a19c2..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/body/column_headers/header/index.test.tsx +++ /dev/null @@ -1,350 +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 { mount, shallow } from 'enzyme'; -import React from 'react'; - -import { Direction } from '../../../../../graphql/types'; -import { TestProviders } from '../../../../../mock'; -import { ColumnHeaderType } from '../../../../../store/timeline/model'; -import { Sort } from '../../sort'; -import { CloseButton } from '../actions'; -import { defaultHeaders } from '../default_headers'; - -import { HeaderComponent } from '.'; -import { getNewSortDirectionOnClick, getNextSortDirection, getSortDirection } from './helpers'; - -const filteredColumnHeader: ColumnHeaderType = 'text-filter'; - -describe('Header', () => { - const columnHeader = defaultHeaders[0]; - const sort: Sort = { - columnId: columnHeader.id, - sortDirection: Direction.desc, - }; - const timelineId = 'fakeId'; - - test('renders correctly against snapshot', () => { - const wrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); - }); - - describe('rendering', () => { - test('it renders the header text', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find(`[data-test-subj="header-text-${columnHeader.id}"]`) - .first() - .text() - ).toEqual(columnHeader.id); - }); - - test('it renders the header text alias when label is provided', () => { - const label = 'Timestamp'; - const headerWithLabel = { ...columnHeader, label }; - const wrapper = mount( - - - - ); - - expect( - wrapper - .find(`[data-test-subj="header-text-${columnHeader.id}"]`) - .first() - .text() - ).toEqual(label); - }); - - test('it renders a sort indicator', () => { - const headerSortable = { ...columnHeader, aggregatable: true }; - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="header-sort-indicator"]') - .first() - .exists() - ).toEqual(true); - }); - - test('it renders a filter', () => { - const columnWithFilter = { - ...columnHeader, - columnHeaderType: filteredColumnHeader, - }; - - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="textFilter"]') - .first() - .props() - ).toHaveProperty('placeholder'); - }); - }); - - describe('onColumnSorted', () => { - test('it invokes the onColumnSorted callback when the header sort button is clicked', () => { - const mockOnColumnSorted = jest.fn(); - const headerSortable = { ...columnHeader, aggregatable: true }; - const wrapper = mount( - - - - ); - - wrapper - .find('[data-test-subj="header-sort-button"]') - .first() - .simulate('click'); - - expect(mockOnColumnSorted).toBeCalledWith({ - columnId: columnHeader.id, - sortDirection: 'asc', // (because the previous state was Direction.desc) - }); - }); - - test('it does NOT render the header sort button when aggregatable is false', () => { - const mockOnColumnSorted = jest.fn(); - const headerSortable = { ...columnHeader, aggregatable: false }; - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="header-sort-button"]').length).toEqual(0); - }); - - test('it does NOT render the header sort button when aggregatable is missing', () => { - const mockOnColumnSorted = jest.fn(); - const headerSortable = { ...columnHeader }; - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="header-sort-button"]').length).toEqual(0); - }); - - test('it does NOT invoke the onColumnSorted callback when the header is clicked and aggregatable is undefined', () => { - const mockOnColumnSorted = jest.fn(); - const headerSortable = { ...columnHeader, aggregatable: undefined }; - const wrapper = mount( - - - - ); - - wrapper - .find('[data-test-subj="header"]') - .first() - .simulate('click'); - - expect(mockOnColumnSorted).not.toHaveBeenCalled(); - }); - }); - - describe('CloseButton', () => { - test('it invokes the onColumnRemoved callback with the column ID when the close button is clicked', () => { - const mockOnColumnRemoved = jest.fn(); - - const wrapper = mount( - - ); - - wrapper - .find('[data-test-subj="remove-column"]') - .first() - .simulate('click'); - - expect(mockOnColumnRemoved).toBeCalledWith(columnHeader.id); - }); - }); - - describe('getSortDirection', () => { - test('it returns the sort direction when the header id matches the sort column id', () => { - expect(getSortDirection({ header: columnHeader, sort })).toEqual(sort.sortDirection); - }); - - test('it returns "none" when sort direction when the header id does NOT match the sort column id', () => { - const nonMatching: Sort = { - columnId: 'differentSocks', - sortDirection: Direction.desc, - }; - - expect(getSortDirection({ header: columnHeader, sort: nonMatching })).toEqual('none'); - }); - }); - - describe('getNextSortDirection', () => { - test('it returns "asc" when the current direction is "desc"', () => { - const sortDescending: Sort = { columnId: columnHeader.id, sortDirection: Direction.desc }; - - expect(getNextSortDirection(sortDescending)).toEqual('asc'); - }); - - test('it returns "desc" when the current direction is "asc"', () => { - const sortAscending: Sort = { - columnId: columnHeader.id, - sortDirection: Direction.asc, - }; - - expect(getNextSortDirection(sortAscending)).toEqual(Direction.desc); - }); - - test('it returns "desc" by default', () => { - const sortNone: Sort = { - columnId: columnHeader.id, - sortDirection: 'none', - }; - - expect(getNextSortDirection(sortNone)).toEqual(Direction.desc); - }); - }); - - describe('getNewSortDirectionOnClick', () => { - test('it returns the expected new sort direction when the header id matches the sort column id', () => { - const sortMatches: Sort = { - columnId: columnHeader.id, - sortDirection: Direction.desc, - }; - - expect( - getNewSortDirectionOnClick({ - clickedHeader: columnHeader, - currentSort: sortMatches, - }) - ).toEqual(Direction.asc); - }); - - test('it returns the expected new sort direction when the header id does NOT match the sort column id', () => { - const sortDoesNotMatch: Sort = { - columnId: 'someOtherColumn', - sortDirection: 'none', - }; - - expect( - getNewSortDirectionOnClick({ - clickedHeader: columnHeader, - currentSort: sortDoesNotMatch, - }) - ).toEqual(Direction.desc); - }); - }); - - describe('text truncation styling', () => { - test('truncates the header text with an ellipsis', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find(`[data-test-subj="header-text-${columnHeader.id}"]`)).toHaveStyleRule( - 'text-overflow', - 'ellipsis' - ); - }); - }); - - describe('header tooltip', () => { - test('it has a tooltip to display the properties of the field', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="header-tooltip"]').exists()).toEqual(true); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/timeline/body/column_headers/header/index.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/header/index.tsx deleted file mode 100644 index 82c5d7eb73f024..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/body/column_headers/header/index.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. - */ - -import { noop } from 'lodash/fp'; -import React, { useCallback } from 'react'; - -import { ColumnHeaderOptions } from '../../../../../store/timeline/model'; -import { OnColumnRemoved, OnColumnSorted, OnFilterChange } from '../../../events'; -import { Sort } from '../../sort'; -import { Actions } from '../actions'; -import { Filter } from '../filter'; -import { getNewSortDirectionOnClick } from './helpers'; -import { HeaderContent } from './header_content'; - -interface Props { - header: ColumnHeaderOptions; - onColumnRemoved: OnColumnRemoved; - onColumnSorted: OnColumnSorted; - onFilterChange?: OnFilterChange; - sort: Sort; - timelineId: string; -} - -export const HeaderComponent: React.FC = ({ - header, - onColumnRemoved, - onColumnSorted, - onFilterChange = noop, - sort, -}) => { - const onClick = useCallback(() => { - onColumnSorted!({ - columnId: header.id, - sortDirection: getNewSortDirectionOnClick({ - clickedHeader: header, - currentSort: sort, - }), - }); - }, [onColumnSorted, header, sort]); - - return ( - <> - - - - - - - ); -}; - -export const Header = React.memo(HeaderComponent); diff --git a/x-pack/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx deleted file mode 100644 index 9afc852373bc6c..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx +++ /dev/null @@ -1,93 +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 { mount, shallow } from 'enzyme'; -import { cloneDeep } from 'lodash/fp'; -import React from 'react'; - -import { ColumnHeaderOptions } from '../../../../../store/timeline/model'; -import { defaultHeaders } from '../../../../../mock'; - -import { HeaderToolTipContent } from '.'; - -describe('HeaderToolTipContent', () => { - let header: ColumnHeaderOptions; - beforeEach(() => { - header = cloneDeep(defaultHeaders[0]); - }); - - test('it renders the category', () => { - const wrapper = mount(); - - expect( - wrapper - .find('[data-test-subj="category-value"]') - .first() - .text() - ).toEqual(header.category); - }); - - test('it renders the name of the field', () => { - const wrapper = mount(); - - expect( - wrapper - .find('[data-test-subj="field-value"]') - .first() - .text() - ).toEqual(header.id); - }); - - test('it renders the expected icon for the header type', () => { - const wrapper = mount(); - - expect( - wrapper - .find('[data-test-subj="type-icon"]') - .first() - .props().type - ).toEqual('clock'); - }); - - test('it renders the type of the field', () => { - const wrapper = mount(); - - expect( - wrapper - .find('[data-test-subj="type-value"]') - .first() - .text() - ).toEqual(header.type); - }); - - test('it renders the description of the field', () => { - const wrapper = mount(); - - expect( - wrapper - .find('[data-test-subj="description-value"]') - .first() - .text() - ).toEqual(header.description); - }); - - test('it does NOT render the description column when the field does NOT contain a description', () => { - const noDescription = { - ...header, - description: '', - }; - - const wrapper = mount(); - - expect(wrapper.find('[data-test-subj="description"]').exists()).toEqual(false); - }); - - test('it renders the expected table content', () => { - const wrapper = shallow(); - - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/index.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/index.tsx deleted file mode 100644 index bef4bcc42b0c78..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/index.tsx +++ /dev/null @@ -1,79 +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 { EuiIcon } from '@elastic/eui'; -import { isEmpty } from 'lodash/fp'; -import React from 'react'; -import styled from 'styled-components'; - -import { ColumnHeaderOptions } from '../../../../../store/timeline/model'; -import { getIconFromType } from '../../../../event_details/helpers'; -import * as i18n from '../translations'; - -const IconType = styled(EuiIcon)` - margin-right: 3px; - position: relative; - top: -2px; -`; -IconType.displayName = 'IconType'; - -const P = styled.p` - margin-bottom: 5px; -`; -P.displayName = 'P'; - -const ToolTipTableMetadata = styled.span` - margin-right: 5px; -`; -ToolTipTableMetadata.displayName = 'ToolTipTableMetadata'; - -const ToolTipTableValue = styled.span` - word-wrap: break-word; -`; -ToolTipTableValue.displayName = 'ToolTipTableValue'; - -export const HeaderToolTipContent = React.memo<{ header: ColumnHeaderOptions }>(({ header }) => ( - <> - {!isEmpty(header.category) && ( -

- - {i18n.CATEGORY} - {':'} - - {header.category} -

- )} -

- - {i18n.FIELD} - {':'} - - {header.id} -

-

- - {i18n.TYPE} - {':'} - - - - {header.type} - -

- {!isEmpty(header.description) && ( -

- - {i18n.DESCRIPTION} - {':'} - - - {header.description} - -

- )} - -)); -HeaderToolTipContent.displayName = 'HeaderToolTipContent'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/column_headers/helpers.ts b/x-pack/plugins/siem/public/components/timeline/body/column_headers/helpers.ts deleted file mode 100644 index 6923831f9ef634..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/body/column_headers/helpers.ts +++ /dev/null @@ -1,48 +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 { get } from 'lodash/fp'; - -import { BrowserFields } from '../../../../containers/source'; -import { ColumnHeaderOptions } from '../../../../store/timeline/model'; -import { - DEFAULT_COLUMN_MIN_WIDTH, - DEFAULT_DATE_COLUMN_MIN_WIDTH, - SHOW_CHECK_BOXES_COLUMN_WIDTH, - EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH, - DEFAULT_ACTIONS_COLUMN_WIDTH, -} from '../constants'; - -/** Enriches the column headers with field details from the specified browserFields */ -export const getColumnHeaders = ( - headers: ColumnHeaderOptions[], - browserFields: BrowserFields -): ColumnHeaderOptions[] => { - return headers.map(header => { - const splitHeader = header.id.split('.'); // source.geo.city_name -> [source, geo, city_name] - - return { - ...header, - ...get( - [splitHeader.length > 1 ? splitHeader[0] : 'base', 'fields', header.id], - browserFields - ), - }; - }); -}; - -export const getColumnWidthFromType = (type: string): number => - type !== 'date' ? DEFAULT_COLUMN_MIN_WIDTH : DEFAULT_DATE_COLUMN_MIN_WIDTH; - -/** Returns the (fixed) width of the Actions column */ -export const getActionsColumnWidth = ( - isEventViewer: boolean, - showCheckboxes = false, - additionalActionWidth = 0 -): number => - (showCheckboxes ? SHOW_CHECK_BOXES_COLUMN_WIDTH : 0) + - (isEventViewer ? EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH : DEFAULT_ACTIONS_COLUMN_WIDTH) + - additionalActionWidth; diff --git a/x-pack/plugins/siem/public/components/timeline/body/column_headers/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/index.test.tsx deleted file mode 100644 index 4fafacfd01633c..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/body/column_headers/index.test.tsx +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { shallow } from 'enzyme'; -import React from 'react'; - -import { DEFAULT_ACTIONS_COLUMN_WIDTH } from '../constants'; -import { defaultHeaders } from './default_headers'; -import { Direction } from '../../../../graphql/types'; -import { mockBrowserFields } from '../../../../../public/containers/source/mock'; -import { Sort } from '../sort'; -import { TestProviders } from '../../../../mock/test_providers'; -import { useMountAppended } from '../../../../utils/use_mount_appended'; - -import { ColumnHeadersComponent } from '.'; - -describe('ColumnHeaders', () => { - const mount = useMountAppended(); - - describe('rendering', () => { - const sort: Sort = { - columnId: 'fooColumn', - sortDirection: Direction.desc, - }; - - test('renders correctly against snapshot', () => { - const wrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); - }); - - test('it renders the field browser', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="field-browser"]') - .first() - .exists() - ).toEqual(true); - }); - - test('it renders every column header', () => { - const wrapper = mount( - - - - ); - - defaultHeaders.forEach(h => { - expect( - wrapper - .find('[data-test-subj="headers-group"]') - .first() - .text() - ).toContain(h.id); - }); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/timeline/body/column_headers/index.tsx b/x-pack/plugins/siem/public/components/timeline/body/column_headers/index.tsx deleted file mode 100644 index 7a072f1dbf578b..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/body/column_headers/index.tsx +++ /dev/null @@ -1,250 +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 { EuiCheckbox } from '@elastic/eui'; -import { noop } from 'lodash/fp'; -import React, { useState, useEffect, useCallback, useMemo } from 'react'; -import { Droppable, DraggableChildrenFn } from 'react-beautiful-dnd'; -import deepEqual from 'fast-deep-equal'; - -import { DragEffects } from '../../../drag_and_drop/draggable_wrapper'; -import { DraggableFieldBadge } from '../../../draggables/field_badge'; -import { BrowserFields } from '../../../../containers/source'; -import { ColumnHeaderOptions } from '../../../../store/timeline/model'; -import { DRAG_TYPE_FIELD, droppableTimelineColumnsPrefix } from '../../../drag_and_drop/helpers'; -import { StatefulFieldsBrowser } from '../../../fields_browser'; -import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from '../../../fields_browser/helpers'; -import { - OnColumnRemoved, - OnColumnResized, - OnColumnSorted, - OnFilterChange, - OnSelectAll, - OnUpdateColumns, -} from '../../events'; -import { - EventsTh, - EventsThContent, - EventsThead, - EventsThGroupActions, - EventsThGroupData, - EventsTrHeader, -} from '../../styles'; -import { Sort } from '../sort'; -import { EventsSelect } from './events_select'; -import { ColumnHeader } from './column_header'; - -interface Props { - actionsColumnWidth: number; - browserFields: BrowserFields; - columnHeaders: ColumnHeaderOptions[]; - isEventViewer?: boolean; - isSelectAllChecked: boolean; - onColumnRemoved: OnColumnRemoved; - onColumnResized: OnColumnResized; - onColumnSorted: OnColumnSorted; - onFilterChange?: OnFilterChange; - onSelectAll: OnSelectAll; - onUpdateColumns: OnUpdateColumns; - showEventsSelect: boolean; - showSelectAllCheckbox: boolean; - sort: Sort; - timelineId: string; - toggleColumn: (column: ColumnHeaderOptions) => void; -} - -interface DraggableContainerProps { - children: React.ReactNode; - onMount: () => void; - onUnmount: () => void; -} - -export const DraggableContainer = React.memo( - ({ children, onMount, onUnmount }) => { - useEffect(() => { - onMount(); - - return () => onUnmount(); - }, [onMount, onUnmount]); - - return <>{children}; - } -); - -DraggableContainer.displayName = 'DraggableContainer'; - -/** Renders the timeline header columns */ -export const ColumnHeadersComponent = ({ - actionsColumnWidth, - browserFields, - columnHeaders, - isEventViewer = false, - isSelectAllChecked, - onColumnRemoved, - onColumnResized, - onColumnSorted, - onSelectAll, - onUpdateColumns, - onFilterChange = noop, - showEventsSelect, - showSelectAllCheckbox, - sort, - timelineId, - toggleColumn, -}: Props) => { - const [draggingIndex, setDraggingIndex] = useState(null); - - const handleSelectAllChange = useCallback( - (event: React.ChangeEvent) => { - onSelectAll({ isSelected: event.currentTarget.checked }); - }, - [onSelectAll] - ); - - const renderClone: DraggableChildrenFn = useCallback( - (dragProvided, dragSnapshot, rubric) => { - // TODO: Remove after github.com/DefinitelyTyped/DefinitelyTyped/pull/43057 is merged - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const index = (rubric as any).source.index; - const header = columnHeaders[index]; - - const onMount = () => setDraggingIndex(index); - const onUnmount = () => setDraggingIndex(null); - - return ( - - - - - - - - ); - }, - [columnHeaders, setDraggingIndex] - ); - - const ColumnHeaderList = useMemo( - () => - columnHeaders.map((header, draggableIndex) => ( - - )), - [ - columnHeaders, - timelineId, - draggingIndex, - onColumnRemoved, - onFilterChange, - onColumnResized, - sort, - ] - ); - - return ( - - - - {showEventsSelect && ( - - - - - - )} - {showSelectAllCheckbox && ( - - - - - - )} - - - - - - - - - {(dropProvided, snapshot) => ( - <> - - {ColumnHeaderList} - - - )} - - - - ); -}; - -export const ColumnHeaders = React.memo( - ColumnHeadersComponent, - (prevProps, nextProps) => - prevProps.actionsColumnWidth === nextProps.actionsColumnWidth && - prevProps.isEventViewer === nextProps.isEventViewer && - prevProps.isSelectAllChecked === nextProps.isSelectAllChecked && - prevProps.onColumnRemoved === nextProps.onColumnRemoved && - prevProps.onColumnResized === nextProps.onColumnResized && - prevProps.onColumnSorted === nextProps.onColumnSorted && - prevProps.onSelectAll === nextProps.onSelectAll && - prevProps.onUpdateColumns === nextProps.onUpdateColumns && - prevProps.onFilterChange === nextProps.onFilterChange && - prevProps.showEventsSelect === nextProps.showEventsSelect && - prevProps.showSelectAllCheckbox === nextProps.showSelectAllCheckbox && - prevProps.sort === nextProps.sort && - prevProps.timelineId === nextProps.timelineId && - prevProps.toggleColumn === nextProps.toggleColumn && - deepEqual(prevProps.columnHeaders, nextProps.columnHeaders) && - deepEqual(prevProps.browserFields, nextProps.browserFields) -); diff --git a/x-pack/plugins/siem/public/components/timeline/body/data_driven_columns/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/data_driven_columns/index.test.tsx deleted file mode 100644 index 098bd3108dba16..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/body/data_driven_columns/index.test.tsx +++ /dev/null @@ -1,34 +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 { shallow } from 'enzyme'; - -import React from 'react'; - -import { mockTimelineData } from '../../../../mock'; -import { defaultHeaders } from '../column_headers/default_headers'; -import { columnRenderers } from '../renderers'; - -import { DataDrivenColumns } from '.'; - -describe('Columns', () => { - const headersSansTimestamp = defaultHeaders.filter(h => h.id !== '@timestamp'); - - test('it renders the expected columns', () => { - const wrapper = shallow( - - ); - - expect(wrapper).toMatchSnapshot(); - }); -}); diff --git a/x-pack/plugins/siem/public/components/timeline/body/data_driven_columns/index.tsx b/x-pack/plugins/siem/public/components/timeline/body/data_driven_columns/index.tsx deleted file mode 100644 index c15c468373c5ac..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/body/data_driven_columns/index.tsx +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { getOr } from 'lodash/fp'; - -import { Ecs, TimelineNonEcsData } from '../../../../graphql/types'; -import { ColumnHeaderOptions } from '../../../../store/timeline/model'; -import { OnColumnResized } from '../../events'; -import { EventsTd, EventsTdContent, EventsTdGroupData } from '../../styles'; -import { ColumnRenderer } from '../renderers/column_renderer'; -import { getColumnRenderer } from '../renderers/get_column_renderer'; - -interface Props { - _id: string; - columnHeaders: ColumnHeaderOptions[]; - columnRenderers: ColumnRenderer[]; - data: TimelineNonEcsData[]; - ecsData: Ecs; - onColumnResized: OnColumnResized; - timelineId: string; -} - -export const DataDrivenColumns = React.memo( - ({ _id, columnHeaders, columnRenderers, data, ecsData, timelineId }) => ( - - {columnHeaders.map(header => ( - - - {getColumnRenderer(header.id, columnRenderers, data).renderColumn({ - columnName: header.id, - eventId: _id, - field: header, - linkValues: getOr([], header.linkField ?? '', ecsData), - timelineId, - truncate: true, - values: getMappedNonEcsValue({ - data, - fieldName: header.id, - }), - })} - - - ))} - - ) -); - -DataDrivenColumns.displayName = 'DataDrivenColumns'; - -const getMappedNonEcsValue = ({ - data, - fieldName, -}: { - data: TimelineNonEcsData[]; - fieldName: string; -}): string[] | undefined => { - const item = data.find(d => d.field === fieldName); - if (item != null && item.value != null) { - return item.value; - } - return undefined; -}; diff --git a/x-pack/plugins/siem/public/components/timeline/body/events/index.tsx b/x-pack/plugins/siem/public/components/timeline/body/events/index.tsx deleted file mode 100644 index 4178bc656f32d3..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/body/events/index.tsx +++ /dev/null @@ -1,112 +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 { BrowserFields } from '../../../../containers/source'; -import { TimelineItem, TimelineNonEcsData } from '../../../../graphql/types'; -import { ColumnHeaderOptions } from '../../../../store/timeline/model'; -import { maxDelay } from '../../../../lib/helpers/scheduler'; -import { Note } from '../../../../lib/note'; -import { AddNoteToEvent, UpdateNote } from '../../../notes/helpers'; -import { - OnColumnResized, - OnPinEvent, - OnRowSelected, - OnUnPinEvent, - OnUpdateColumns, -} from '../../events'; -import { EventsTbody } from '../../styles'; -import { ColumnRenderer } from '../renderers/column_renderer'; -import { RowRenderer } from '../renderers/row_renderer'; -import { StatefulEvent } from './stateful_event'; -import { eventIsPinned } from '../helpers'; - -interface Props { - actionsColumnWidth: number; - addNoteToEvent: AddNoteToEvent; - browserFields: BrowserFields; - columnHeaders: ColumnHeaderOptions[]; - columnRenderers: ColumnRenderer[]; - containerElementRef: HTMLDivElement; - data: TimelineItem[]; - eventIdToNoteIds: Readonly>; - getNotesByIds: (noteIds: string[]) => Note[]; - id: string; - isEventViewer?: boolean; - loadingEventIds: Readonly; - onColumnResized: OnColumnResized; - onPinEvent: OnPinEvent; - onRowSelected: OnRowSelected; - onUpdateColumns: OnUpdateColumns; - onUnPinEvent: OnUnPinEvent; - pinnedEventIds: Readonly>; - rowRenderers: RowRenderer[]; - selectedEventIds: Readonly>; - showCheckboxes: boolean; - toggleColumn: (column: ColumnHeaderOptions) => void; - updateNote: UpdateNote; -} - -const EventsComponent: React.FC = ({ - actionsColumnWidth, - addNoteToEvent, - browserFields, - columnHeaders, - columnRenderers, - containerElementRef, - data, - eventIdToNoteIds, - getNotesByIds, - id, - isEventViewer = false, - loadingEventIds, - onColumnResized, - onPinEvent, - onRowSelected, - onUpdateColumns, - onUnPinEvent, - pinnedEventIds, - rowRenderers, - selectedEventIds, - showCheckboxes, - toggleColumn, - updateNote, -}) => ( - - {data.map((event, i) => ( - - ))} - -); - -export const Events = React.memo(EventsComponent); diff --git a/x-pack/plugins/siem/public/components/timeline/body/helpers.test.ts b/x-pack/plugins/siem/public/components/timeline/body/helpers.test.ts deleted file mode 100644 index f021bf38b56c2d..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/body/helpers.test.ts +++ /dev/null @@ -1,228 +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 { Ecs } from '../../../graphql/types'; - -import { eventHasNotes, eventIsPinned, getPinTooltip, stringifyEvent } from './helpers'; - -describe('helpers', () => { - describe('stringifyEvent', () => { - test('it omits __typename when it appears at arbitrary levels', () => { - const toStringify: Ecs = { - __typename: 'level 0', - _id: '4', - timestamp: '2018-11-08T19:03:25.937Z', - host: { - __typename: 'level 1', - name: ['suricata'], - ip: ['192.168.0.1'], - }, - event: { - id: ['4'], - category: ['Attempted Administrator Privilege Gain'], - type: ['Alert'], - module: ['suricata'], - severity: [1], - }, - source: { - ip: ['192.168.0.3'], - port: [53], - }, - destination: { - ip: ['192.168.0.3'], - port: [6343], - }, - suricata: { - eve: { - flow_id: [4], - proto: [''], - alert: { - signature: ['ET PHONE HOME Stack Overflow (CVE-2019-90210)'], - signature_id: [4], - __typename: 'level 2', - }, - }, - }, - user: { - id: ['4'], - name: ['jack.black'], - }, - geo: { - region_name: ['neither'], - country_iso_code: ['sasquatch'], - }, - } as Ecs; // as cast so that `__typename` can be added for the tests even though it is not part of ECS - const expected: Ecs = { - _id: '4', - timestamp: '2018-11-08T19:03:25.937Z', - host: { - name: ['suricata'], - ip: ['192.168.0.1'], - }, - event: { - id: ['4'], - category: ['Attempted Administrator Privilege Gain'], - type: ['Alert'], - module: ['suricata'], - severity: [1], - }, - source: { - ip: ['192.168.0.3'], - port: [53], - }, - destination: { - ip: ['192.168.0.3'], - port: [6343], - }, - suricata: { - eve: { - flow_id: [4], - proto: [''], - alert: { - signature: ['ET PHONE HOME Stack Overflow (CVE-2019-90210)'], - signature_id: [4], - }, - }, - }, - user: { - id: ['4'], - name: ['jack.black'], - }, - geo: { - region_name: ['neither'], - country_iso_code: ['sasquatch'], - }, - }; - expect(JSON.parse(stringifyEvent(toStringify))).toEqual(expected); - }); - - test('it omits null and undefined values at arbitrary levels, for arbitrary data types', () => { - const expected: Ecs = { - _id: '4', - host: {}, - event: { - id: ['4'], - category: ['theory'], - type: ['Alert'], - module: ['me'], - severity: [1], - }, - source: { - port: [53], - }, - destination: { - ip: ['192.168.0.3'], - port: [6343], - }, - suricata: { - eve: { - flow_id: [4], - proto: [''], - alert: { - signature: ['dance moves'], - }, - }, - }, - user: { - id: ['4'], - name: ['no use for a'], - }, - geo: { - region_name: ['bizzaro'], - country_iso_code: ['world'], - }, - }; - const toStringify: Ecs = { - _id: '4', - timestamp: null, - host: { - name: null, - ip: null, - }, - event: { - id: ['4'], - category: ['theory'], - type: ['Alert'], - module: ['me'], - severity: [1], - }, - source: { - ip: undefined, - port: [53], - }, - destination: { - ip: ['192.168.0.3'], - port: [6343], - }, - suricata: { - eve: { - flow_id: [4], - proto: [''], - alert: { - signature: ['dance moves'], - signature_id: undefined, - }, - }, - }, - user: { - id: ['4'], - name: ['no use for a'], - }, - geo: { - region_name: ['bizzaro'], - country_iso_code: ['world'], - }, - }; - expect(JSON.parse(stringifyEvent(toStringify))).toEqual(expected); - }); - }); - - describe('eventHasNotes', () => { - test('it returns false for when notes is empty', () => { - expect(eventHasNotes([])).toEqual(false); - }); - - test('it returns true when notes is non-empty', () => { - expect(eventHasNotes(['8af859e2-e4f8-4754-b702-4f227f15aae5'])).toEqual(true); - }); - }); - - describe('getPinTooltip', () => { - test('it indicates the event may NOT be unpinned when `isPinned` is `true` and the event has notes', () => { - expect(getPinTooltip({ isPinned: true, eventHasNotes: true })).toEqual( - 'This event cannot be unpinned because it has notes' - ); - }); - - test('it indicates the event is pinned when `isPinned` is `true` and the event does NOT have notes', () => { - expect(getPinTooltip({ isPinned: true, eventHasNotes: false })).toEqual('Pinned event'); - }); - - test('it indicates the event is NOT pinned when `isPinned` is `false` and the event has notes', () => { - expect(getPinTooltip({ isPinned: false, eventHasNotes: true })).toEqual('Unpinned event'); - }); - - test('it indicates the event is NOT pinned when `isPinned` is `false` and the event does NOT have notes', () => { - expect(getPinTooltip({ isPinned: false, eventHasNotes: false })).toEqual('Unpinned event'); - }); - }); - - describe('eventIsPinned', () => { - test('returns true when the specified event id is contained in the pinnedEventIds', () => { - const eventId = 'race-for-the-prize'; - const pinnedEventIds = { [eventId]: true, 'waiting-for-superman': true }; - - expect(eventIsPinned({ eventId, pinnedEventIds })).toEqual(true); - }); - - test('returns false when the specified event id is NOT contained in the pinnedEventIds', () => { - const eventId = 'safety-pin'; - const pinnedEventIds = { 'thumb-tack': true }; - - expect(eventIsPinned({ eventId, pinnedEventIds })).toEqual(false); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/timeline/body/helpers.ts b/x-pack/plugins/siem/public/components/timeline/body/helpers.ts deleted file mode 100644 index 3d1d165ef4fa60..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/body/helpers.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. - */ -import { isEmpty, noop } from 'lodash/fp'; - -import { Ecs, TimelineItem, TimelineNonEcsData } from '../../../graphql/types'; -import { EventType } from '../../../store/timeline/model'; -import { OnPinEvent, OnUnPinEvent } from '../events'; - -import * as i18n from './translations'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const omitTypenameAndEmpty = (k: string, v: any): any | undefined => - k !== '__typename' && v != null ? v : undefined; - -export const stringifyEvent = (ecs: Ecs): string => JSON.stringify(ecs, omitTypenameAndEmpty, 2); - -export const eventHasNotes = (noteIds: string[]): boolean => !isEmpty(noteIds); - -export const getPinTooltip = ({ - isPinned, - // eslint-disable-next-line no-shadow - eventHasNotes, -}: { - isPinned: boolean; - eventHasNotes: boolean; -}) => (isPinned && eventHasNotes ? i18n.PINNED_WITH_NOTES : isPinned ? i18n.PINNED : i18n.UNPINNED); - -export interface IsPinnedParams { - eventId: string; - pinnedEventIds: Readonly>; -} - -export const eventIsPinned = ({ eventId, pinnedEventIds }: IsPinnedParams): boolean => - pinnedEventIds[eventId] === true; - -export interface GetPinOnClickParams { - allowUnpinning: boolean; - eventId: string; - onPinEvent: OnPinEvent; - onUnPinEvent: OnUnPinEvent; - isEventPinned: boolean; -} - -export const getPinOnClick = ({ - allowUnpinning, - eventId, - onPinEvent, - onUnPinEvent, - isEventPinned, -}: GetPinOnClickParams): (() => void) => { - if (!allowUnpinning) { - return noop; - } - return isEventPinned ? () => onUnPinEvent(eventId) : () => onPinEvent(eventId); -}; - -/** - * Creates mapping of eventID -> fieldData for given fieldsToKeep. Used to store additional field - * data necessary for custom timeline actions in conjunction with selection state - * @param timelineData - * @param eventIds - * @param fieldsToKeep - */ -export const getEventIdToDataMapping = ( - timelineData: TimelineItem[], - eventIds: string[], - fieldsToKeep: string[] -): Record => { - return timelineData.reduce((acc, v) => { - const fvm = eventIds.includes(v._id) - ? { [v._id]: v.data.filter(ti => fieldsToKeep.includes(ti.field)) } - : {}; - return { - ...acc, - ...fvm, - }; - }, {}); -}; - -/** Return eventType raw or signal */ -export const getEventType = (event: Ecs): Omit => { - if (!isEmpty(event.signal?.rule?.id)) { - return 'signal'; - } - return 'raw'; -}; diff --git a/x-pack/plugins/siem/public/components/timeline/body/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/index.test.tsx deleted file mode 100644 index cf35c8e565bbc0..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/body/index.test.tsx +++ /dev/null @@ -1,355 +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 { mockBrowserFields } from '../../../containers/source/mock'; -import { Direction } from '../../../graphql/types'; -import { defaultHeaders, mockTimelineData } from '../../../mock'; -import { TestProviders } from '../../../mock/test_providers'; - -import { Body, BodyProps } from '.'; -import { columnRenderers, rowRenderers } from './renderers'; -import { Sort } from './sort'; -import { wait } from '../../../lib/helpers'; -import { useMountAppended } from '../../../utils/use_mount_appended'; - -const testBodyHeight = 700; -const mockGetNotesByIds = (eventId: string[]) => []; -const mockSort: Sort = { - columnId: '@timestamp', - sortDirection: Direction.desc, -}; - -jest.mock( - 'react-visibility-sensor', - () => ({ children }: { children: (args: { isVisible: boolean }) => React.ReactNode }) => - children({ isVisible: true }) -); - -jest.mock('../../../lib/helpers/scheduler', () => ({ - requestIdleCallbackViaScheduler: (callback: () => void, opts?: unknown) => { - callback(); - }, - maxDelay: () => 3000, -})); - -describe('Body', () => { - const mount = useMountAppended(); - - describe('rendering', () => { - test('it renders the column headers', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="column-headers"]') - .first() - .exists() - ).toEqual(true); - }); - - test('it renders the scroll container', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="timeline-body"]') - .first() - .exists() - ).toEqual(true); - }); - - test('it renders events', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="events"]') - .first() - .exists() - ).toEqual(true); - }); - - test('it renders a tooltip for timestamp', async () => { - const headersJustTimestamp = defaultHeaders.filter(h => h.id === '@timestamp'); - - const wrapper = mount( - - - - ); - wrapper.update(); - await wait(); - wrapper.update(); - headersJustTimestamp.forEach(() => { - expect( - wrapper - .find('[data-test-subj="data-driven-columns"]') - .first() - .find('[data-test-subj="localized-date-tool-tip"]') - .exists() - ).toEqual(true); - }); - }); - }); - - describe('action on event', () => { - const dispatchAddNoteToEvent = jest.fn(); - const dispatchOnPinEvent = jest.fn(); - - const addaNoteToEvent = (wrapper: ReturnType, note: string) => { - wrapper - .find('[data-test-subj="add-note"]') - .first() - .find('button') - .simulate('click'); - wrapper.update(); - wrapper - .find('[data-test-subj="new-note-tabs"] textarea') - .simulate('change', { target: { value: note } }); - wrapper.update(); - wrapper - .find('button[data-test-subj="add-note"]') - .first() - .simulate('click'); - wrapper.update(); - }; - - // We are doing that because we need to wrapped this component with redux - // and redux does not like to be updated and since we need to update our - // child component (BODY) and we do not want to scare anyone with this error - // we are hiding it!!! - // eslint-disable-next-line no-console - const originalError = console.error; - beforeAll(() => { - // eslint-disable-next-line no-console - console.error = (...args: string[]) => { - if (/ does not support changing `store` on the fly/.test(args[0])) { - return; - } - originalError.call(console, ...args); - }; - }); - - beforeEach(() => { - dispatchAddNoteToEvent.mockClear(); - dispatchOnPinEvent.mockClear(); - }); - - test('Add a Note to an event', () => { - const wrapper = mount( - - - - ); - addaNoteToEvent(wrapper, 'hello world'); - - expect(dispatchAddNoteToEvent).toHaveBeenCalled(); - expect(dispatchOnPinEvent).toHaveBeenCalled(); - }); - - test('Add two Note to an event', () => { - const Proxy = (props: BodyProps) => ( - - - - ); - - const wrapper = mount( - - ); - addaNoteToEvent(wrapper, 'hello world'); - dispatchAddNoteToEvent.mockClear(); - dispatchOnPinEvent.mockClear(); - wrapper.setProps({ pinnedEventIds: { 1: true } }); - wrapper.update(); - addaNoteToEvent(wrapper, 'new hello world'); - expect(dispatchAddNoteToEvent).toHaveBeenCalled(); - expect(dispatchOnPinEvent).not.toHaveBeenCalled(); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/timeline/body/index.tsx b/x-pack/plugins/siem/public/components/timeline/body/index.tsx deleted file mode 100644 index fac8cc61cddd2e..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/body/index.tsx +++ /dev/null @@ -1,169 +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, { useMemo, useRef } from 'react'; - -import { BrowserFields } from '../../../containers/source'; -import { TimelineItem, TimelineNonEcsData } from '../../../graphql/types'; -import { Note } from '../../../lib/note'; -import { ColumnHeaderOptions } from '../../../store/timeline/model'; -import { AddNoteToEvent, UpdateNote } from '../../notes/helpers'; -import { - OnColumnRemoved, - OnColumnResized, - OnColumnSorted, - OnFilterChange, - OnPinEvent, - OnRowSelected, - OnSelectAll, - OnUnPinEvent, - OnUpdateColumns, -} from '../events'; -import { EventsTable, TimelineBody, TimelineBodyGlobalStyle } from '../styles'; -import { ColumnHeaders } from './column_headers'; -import { getActionsColumnWidth } from './column_headers/helpers'; -import { Events } from './events'; -import { ColumnRenderer } from './renderers/column_renderer'; -import { RowRenderer } from './renderers/row_renderer'; -import { Sort } from './sort'; -import { useTimelineTypeContext } from '../timeline_context'; - -export interface BodyProps { - addNoteToEvent: AddNoteToEvent; - browserFields: BrowserFields; - columnHeaders: ColumnHeaderOptions[]; - columnRenderers: ColumnRenderer[]; - data: TimelineItem[]; - getNotesByIds: (noteIds: string[]) => Note[]; - height?: number; - id: string; - isEventViewer?: boolean; - isSelectAllChecked: boolean; - eventIdToNoteIds: Readonly>; - loadingEventIds: Readonly; - onColumnRemoved: OnColumnRemoved; - onColumnResized: OnColumnResized; - onColumnSorted: OnColumnSorted; - onRowSelected: OnRowSelected; - onSelectAll: OnSelectAll; - onFilterChange: OnFilterChange; - onPinEvent: OnPinEvent; - onUpdateColumns: OnUpdateColumns; - onUnPinEvent: OnUnPinEvent; - pinnedEventIds: Readonly>; - rowRenderers: RowRenderer[]; - selectedEventIds: Readonly>; - showCheckboxes: boolean; - sort: Sort; - toggleColumn: (column: ColumnHeaderOptions) => void; - updateNote: UpdateNote; -} - -/** Renders the timeline body */ -export const Body = React.memo( - ({ - addNoteToEvent, - browserFields, - columnHeaders, - columnRenderers, - data, - eventIdToNoteIds, - getNotesByIds, - height, - id, - isEventViewer = false, - isSelectAllChecked, - loadingEventIds, - onColumnRemoved, - onColumnResized, - onColumnSorted, - onRowSelected, - onSelectAll, - onFilterChange, - onPinEvent, - onUpdateColumns, - onUnPinEvent, - pinnedEventIds, - rowRenderers, - selectedEventIds, - showCheckboxes, - sort, - toggleColumn, - updateNote, - }) => { - const containerElementRef = useRef(null); - const timelineTypeContext = useTimelineTypeContext(); - const additionalActionWidth = useMemo( - () => timelineTypeContext.timelineActions?.reduce((acc, v) => acc + v.width, 0) ?? 0, - [timelineTypeContext.timelineActions] - ); - const actionsColumnWidth = useMemo( - () => getActionsColumnWidth(isEventViewer, showCheckboxes, additionalActionWidth), - [isEventViewer, showCheckboxes, additionalActionWidth] - ); - - const columnWidths = useMemo( - () => - columnHeaders.reduce((totalWidth, header) => totalWidth + header.width, actionsColumnWidth), - [actionsColumnWidth, columnHeaders] - ); - - return ( - <> - - - - - - - - - - ); - } -); -Body.displayName = 'Body'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.test.tsx deleted file mode 100644 index 21cccc88f4fbce..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.test.tsx +++ /dev/null @@ -1,485 +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 { shallow } from 'enzyme'; -import React from 'react'; - -import { BrowserFields } from '../../../../../containers/source'; -import { mockBrowserFields } from '../../../../../containers/source/mock'; -import { mockTimelineData, TestProviders } from '../../../../../mock'; -import { AuditdGenericDetails, AuditdGenericLine } from './generic_details'; -import { useMountAppended } from '../../../../../utils/use_mount_appended'; - -describe('GenericDetails', () => { - const mount = useMountAppended(); - - describe('rendering', () => { - test('it renders the default AuditAcquiredCredsDetails', () => { - // I cannot and do not want to use BrowserFields for the mocks for the snapshot tests as they are too heavy - const browserFields: BrowserFields = {}; - const wrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); - }); - - test('it returns auditd if the data does contain auditd data', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual( - 'Sessionalice@zeek-sanfranin/generic-text-123gpgconf(5402)gpgconf--list-dirsagent-socketgpgconf --list-dirs agent-socket' - ); - }); - - test('it returns null for text if the data contains no auditd data', () => { - const wrapper = shallow( - - ); - expect(wrapper.isEmptyRender()).toBeTruthy(); - }); - }); - - describe('#AuditdConnectedToLine', () => { - test('it returns pretty output if you send in all your happy path data', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual( - 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123process-name-1(123)arg1arg2arg3process-title-1with resultsuccess' - ); - }); - - test('it returns a session with username if username, primary, and secondary all equal each other ', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual( - 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123process-name-1(123)arg1arg2arg3process-title-1with resultsuccess' - ); - }); - - test('it returns a session with username if primary and secondary equal unset', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual( - 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123process-name-1(123)arg1arg2arg3process-title-1with resultsuccess' - ); - }); - - test('it returns a session with username if primary and secondary equal unset with different casing', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual( - 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123process-name-1(123)arg1arg2arg3process-title-1with resultsuccess' - ); - }); - - test('it returns a session with username if primary and secondary are undefined', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual( - 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123process-name-1(123)arg1arg2arg3process-title-1with resultsuccess' - ); - }); - - test('it returns a session with "as" wording if username, primary, and secondary are all different', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual( - 'Sessionsession-1[username-2]as[username-3]@host-1inworking-directory-1generic-text-123process-name-1(123)arg1arg2arg3process-title-1with resultsuccess' - ); - }); - - test('it returns a session with "as" wording if username and primary are the same but secondary is different', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual( - 'Sessionsession-1[username-1]as[username-2]@host-1inworking-directory-1generic-text-123process-name-1(123)arg1arg2arg3process-title-1with resultsuccess' - ); - }); - - test('it returns a session with primary if username and secondary are unset with different casing', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual( - 'Sessionsession-1[username-primary]@host-1inworking-directory-1generic-text-123process-name-1(123)arg1arg2arg3process-title-1with resultsuccess' - ); - }); - - test('it returns a session with primary if username and secondary are undefined', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual( - 'Sessionsession-1[username-primary]@host-1inworking-directory-1generic-text-123process-name-1(123)arg1arg2arg3process-title-1with resultsuccess' - ); - }); - - test('it returns just a session if only given an id', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual('Session'); - }); - - test('it returns only session and hostName if only hostname and an id is given', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual('Session@some-host-name'); - }); - - test('it returns only a session and user name if only a user name and id is given', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual('Sessionsome-user-name'); - }); - - test('it returns only a process name if only given a process name and id', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual('Sessiongeneric-text-123some-process-name'); - }); - - test('it returns session, user name, and process title if process title with id is given', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual('Sessionsome-user-namesome-process-title'); - }); - - test('it returns only a working directory if that is all that is given with a id', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual('Sessioninsome-working-directory'); - }); - - test('it returns only the session and args with id if that is all that is given (very unlikely situation)', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual('Sessionarg1arg2arg 3'); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.tsx deleted file mode 100644 index c25c656b75e41f..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_details.tsx +++ /dev/null @@ -1,159 +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 { EuiFlexGroup, EuiSpacer } from '@elastic/eui'; -import { get } from 'lodash/fp'; -import React from 'react'; - -import { BrowserFields } from '../../../../../containers/source'; -import { Ecs } from '../../../../../graphql/types'; -import { DraggableBadge } from '../../../../draggables'; - -import * as i18n from './translations'; -import { NetflowRenderer } from '../netflow'; -import { TokensFlexItem, Details } from '../helpers'; -import { ProcessDraggable } from '../process_draggable'; -import { Args } from '../args'; -import { SessionUserHostWorkingDir } from './session_user_host_working_dir'; - -interface Props { - id: string; - hostName: string | null | undefined; - result: string | null | undefined; - userName: string | null | undefined; - primary: string | null | undefined; - contextId: string; - text: string; - secondary: string | null | undefined; - processName: string | null | undefined; - processPid: number | null | undefined; - processExecutable: string | null | undefined; - processTitle: string | null | undefined; - workingDirectory: string | null | undefined; - args: string[] | null | undefined; - session: string | null | undefined; -} - -export const AuditdGenericLine = React.memo( - ({ - id, - contextId, - hostName, - userName, - primary, - processName, - processPid, - processExecutable, - processTitle, - secondary, - workingDirectory, - args, - result, - session, - text, - }) => ( - - - {processExecutable != null && ( - - {text} - - )} - - - - - {result != null && ( - - {i18n.WITH_RESULT} - - )} - - - - - ) -); - -AuditdGenericLine.displayName = 'AuditdGenericLine'; - -interface GenericDetailsProps { - browserFields: BrowserFields; - data: Ecs; - contextId: string; - text: string; - timelineId: string; -} - -export const AuditdGenericDetails = React.memo( - ({ data, contextId, text, timelineId }) => { - const id = data._id; - const session: string | null | undefined = get('auditd.session[0]', data); - const hostName: string | null | undefined = get('host.name[0]', data); - const userName: string | null | undefined = get('user.name[0]', data); - const result: string | null | undefined = get('auditd.result[0]', data); - const processPid: number | null | undefined = get('process.pid[0]', data); - const processName: string | null | undefined = get('process.name[0]', data); - const processExecutable: string | null | undefined = get('process.executable[0]', data); - const processTitle: string | null | undefined = get('process.title[0]', data); - const workingDirectory: string | null | undefined = get('process.working_directory[0]', data); - const primary: string | null | undefined = get('auditd.summary.actor.primary[0]', data); - const secondary: string | null | undefined = get('auditd.summary.actor.secondary[0]', data); - const args: string[] | null | undefined = get('process.args', data); - if (data.process != null) { - return ( -
- - - -
- ); - } else { - return null; - } - } -); - -AuditdGenericDetails.displayName = 'AuditdGenericDetails'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.test.tsx deleted file mode 100644 index fce0e1d645e164..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.test.tsx +++ /dev/null @@ -1,520 +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 { shallow } from 'enzyme'; -import React from 'react'; - -import { BrowserFields } from '../../../../../containers/source'; -import { mockBrowserFields } from '../../../../../containers/source/mock'; -import { mockTimelineData, TestProviders } from '../../../../../mock'; -import { AuditdGenericFileDetails, AuditdGenericFileLine } from './generic_file_details'; -import { useMountAppended } from '../../../../../utils/use_mount_appended'; - -describe('GenericFileDetails', () => { - const mount = useMountAppended(); - - describe('rendering', () => { - test('it renders the default GenericFileDetails', () => { - // I cannot and do not want to use BrowserFields for the mocks for the snapshot tests as they are too heavy - const browserFields: BrowserFields = {}; - const wrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); - }); - - test('it returns auditd if the data does contain auditd data', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual( - 'Sessionalice@zeek-sanfranin/generic-text-123usinggpgconf(5402)gpgconf--list-dirsagent-socketgpgconf --list-dirs agent-socket' - ); - }); - - test('it returns null for text if the data contains no auditd data', () => { - const wrapper = shallow( - - ); - expect(wrapper.isEmptyRender()).toBeTruthy(); - }); - }); - - describe('#AuditdGenericFileLine', () => { - test('it returns pretty output if you send in all your happy path data', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual( - 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1(123)arg1arg2arg3process-title-1with resultsuccess' - ); - }); - - test('it returns a session with username if username, primary, and secondary all equal each other ', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual( - 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1(123)arg1arg2arg3process-title-1with resultsuccess' - ); - }); - - test('it returns a session with username if primary and secondary equal unset', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual( - 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1(123)arg1arg2arg3process-title-1with resultsuccess' - ); - }); - - test('it returns a session with username if primary and secondary equal unset with different casing', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual( - 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1(123)arg1arg2arg3process-title-1with resultsuccess' - ); - }); - - test('it returns a session with username if primary and secondary are undefined', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual( - 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1(123)arg1arg2arg3process-title-1with resultsuccess' - ); - }); - - test('it returns a session with "as" wording if username, primary, and secondary are all different', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual( - 'Sessionsession-1[username-2]as[username-3]@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1(123)arg1arg2arg3process-title-1with resultsuccess' - ); - }); - - test('it returns a session with "as" wording if username and primary are the same but secondary is different', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual( - 'Sessionsession-1[username-1]as[username-2]@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1(123)arg1arg2arg3process-title-1with resultsuccess' - ); - }); - - test('it returns a session with primary if username and secondary are unset with different casing', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual( - 'Sessionsession-1[username-primary]@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1(123)arg1arg2arg3process-title-1with resultsuccess' - ); - }); - - test('it returns a session with primary if username and secondary are undefined', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual( - 'Sessionsession-1[username-primary]@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1(123)arg1arg2arg3process-title-1with resultsuccess' - ); - }); - - test('it returns just session if only session id is given', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual('Session'); - }); - - test('it returns only session and hostName if only hostname and an id is given', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual('Session@some-host-name'); - }); - - test('it returns only a session and user name if only a user name and id is given', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual('Sessionsome-user-name'); - }); - - test('it returns only a process name if only given a process name and id', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual('Sessiongeneric-text-123usingsome-process-name'); - }); - - test('it returns session user name and title if process title with id is given', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual('Sessionsome-user-namesome-process-title'); - }); - - test('it returns only a working directory if that is all that is given with a id', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual('Sessioninsome-working-directory'); - }); - - test('it returns only the session and args with id if that is all that is given (very unlikely situation)', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual('Sessionarg1arg2arg 3'); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.tsx deleted file mode 100644 index 797361878e6c53..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_file_details.tsx +++ /dev/null @@ -1,182 +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 { EuiFlexGroup, EuiSpacer, IconType } from '@elastic/eui'; -import { get } from 'lodash/fp'; -import React from 'react'; - -import { BrowserFields } from '../../../../../containers/source'; -import { Ecs } from '../../../../../graphql/types'; -import { DraggableBadge } from '../../../../draggables'; - -import * as i18n from './translations'; -import { NetflowRenderer } from '../netflow'; -import { TokensFlexItem, Details } from '../helpers'; -import { ProcessDraggable } from '../process_draggable'; -import { Args } from '../args'; -import { SessionUserHostWorkingDir } from './session_user_host_working_dir'; - -interface Props { - id: string; - hostName: string | null | undefined; - userName: string | null | undefined; - result: string | null | undefined; - primary: string | null | undefined; - fileIcon: IconType; - contextId: string; - text: string; - secondary: string | null | undefined; - filePath: string | null | undefined; - processName: string | null | undefined; - processPid: number | null | undefined; - processExecutable: string | null | undefined; - processTitle: string | null | undefined; - workingDirectory: string | null | undefined; - args: string[] | null | undefined; - session: string | null | undefined; -} - -export const AuditdGenericFileLine = React.memo( - ({ - id, - contextId, - hostName, - userName, - result, - primary, - secondary, - filePath, - processName, - processPid, - processExecutable, - processTitle, - workingDirectory, - args, - session, - text, - fileIcon, - }) => ( - - - {(filePath != null || processExecutable != null) && ( - - {text} - - )} - - - - {processExecutable != null && ( - - {i18n.USING} - - )} - - - - - {result != null && ( - - {i18n.WITH_RESULT} - - )} - - - - - ) -); - -AuditdGenericFileLine.displayName = 'AuditdGenericFileLine'; - -interface GenericDetailsProps { - browserFields: BrowserFields; - data: Ecs; - contextId: string; - text: string; - fileIcon: IconType; - timelineId: string; -} - -export const AuditdGenericFileDetails = React.memo( - ({ data, contextId, text, fileIcon = 'document', timelineId }) => { - const id = data._id; - const session: string | null | undefined = get('auditd.session[0]', data); - const hostName: string | null | undefined = get('host.name[0]', data); - const userName: string | null | undefined = get('user.name[0]', data); - const result: string | null | undefined = get('auditd.result[0]', data); - const processPid: number | null | undefined = get('process.pid[0]', data); - const processName: string | null | undefined = get('process.name[0]', data); - const processExecutable: string | null | undefined = get('process.executable[0]', data); - const processTitle: string | null | undefined = get('process.title[0]', data); - const workingDirectory: string | null | undefined = get('process.working_directory[0]', data); - const filePath: string | null | undefined = get('file.path[0]', data); - const primary: string | null | undefined = get('auditd.summary.actor.primary[0]', data); - const secondary: string | null | undefined = get('auditd.summary.actor.secondary[0]', data); - const args: string[] | null | undefined = get('process.args', data); - - if (data.process != null) { - return ( -
- - - -
- ); - } else { - return null; - } - } -); - -AuditdGenericFileDetails.displayName = 'AuditdGenericFileDetails'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx deleted file mode 100644 index 417a078a081509..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx +++ /dev/null @@ -1,148 +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 { shallow } from 'enzyme'; -import { cloneDeep } from 'lodash/fp'; -import React from 'react'; - -import { BrowserFields } from '../../../../../containers/source'; -import { mockBrowserFields } from '../../../../../containers/source/mock'; -import { Ecs } from '../../../../../graphql/types'; -import { mockTimelineData, TestProviders } from '../../../../../mock'; -import { useMountAppended } from '../../../../../utils/use_mount_appended'; -import { RowRenderer } from '../row_renderer'; -import { - createGenericAuditRowRenderer, - createGenericFileRowRenderer, -} from './generic_row_renderer'; - -jest.mock('../../../../../pages/overview/events_by_dataset'); - -describe('GenericRowRenderer', () => { - const mount = useMountAppended(); - - describe('#createGenericAuditRowRenderer', () => { - let nonAuditd: Ecs; - let auditd: Ecs; - let connectedToRenderer: RowRenderer; - beforeEach(() => { - nonAuditd = cloneDeep(mockTimelineData[0].ecs); - auditd = cloneDeep(mockTimelineData[26].ecs); - connectedToRenderer = createGenericAuditRowRenderer({ - actionName: 'connected-to', - text: 'some text', - }); - }); - test('renders correctly against snapshot', () => { - // I cannot and do not want to use BrowserFields mocks for the snapshot tests as they are too heavy - const browserFields: BrowserFields = {}; - const children = connectedToRenderer.renderRow({ - browserFields, - data: auditd, - timelineId: 'test', - }); - - const wrapper = shallow({children}); - expect(wrapper).toMatchSnapshot(); - }); - - test('should return false if not a auditd datum', () => { - expect(connectedToRenderer.isInstance(nonAuditd)).toBe(false); - }); - - test('should return true if it is a auditd datum', () => { - expect(connectedToRenderer.isInstance(auditd)).toBe(true); - }); - - test('should return false when action is set to some other value', () => { - if (auditd.event != null && auditd.event.action != null) { - auditd.event.action[0] = 'some other value'; - expect(connectedToRenderer.isInstance(auditd)).toBe(false); - } else { - // will fail and give you an error if either is not defined as a mock - expect(auditd.event).toBeDefined(); - } - }); - - test('should render a auditd row', () => { - const children = connectedToRenderer.renderRow({ - browserFields: mockBrowserFields, - data: auditd, - timelineId: 'test', - }); - const wrapper = mount( - - {children} - - ); - expect(wrapper.text()).toContain( - 'Session246alice@zeek-londonsome textwget(1490)wget www.example.comwith resultsuccessDestination93.184.216.34:80' - ); - }); - }); - - describe('#createGenericFileRowRenderer', () => { - let nonAuditd: Ecs; - let auditdFile: Ecs; - let fileToRenderer: RowRenderer; - - beforeEach(() => { - nonAuditd = cloneDeep(mockTimelineData[0].ecs); - auditdFile = cloneDeep(mockTimelineData[27].ecs); - fileToRenderer = createGenericFileRowRenderer({ - actionName: 'opened-file', - text: 'some text', - }); - }); - - test('renders correctly against snapshot', () => { - // I cannot and do not want to use BrowserFields mocks for the snapshot tests as they are too heavy - const browserFields: BrowserFields = {}; - const children = fileToRenderer.renderRow({ - browserFields, - data: auditdFile, - timelineId: 'test', - }); - - const wrapper = shallow({children}); - expect(wrapper).toMatchSnapshot(); - }); - - test('should return false if not a auditd datum', () => { - expect(fileToRenderer.isInstance(nonAuditd)).toBe(false); - }); - - test('should return true if it is a auditd datum', () => { - expect(fileToRenderer.isInstance(auditdFile)).toBe(true); - }); - - test('should return false when action is set to some other value', () => { - if (auditdFile.event != null && auditdFile.event.action != null) { - auditdFile.event.action[0] = 'some other value'; - expect(fileToRenderer.isInstance(auditdFile)).toBe(false); - } else { - // will fail and give you an error if either is not defined as a mock - expect(auditdFile.event).toBeDefined(); - } - }); - - test('should render a auditd row', () => { - const children = fileToRenderer.renderRow({ - browserFields: mockBrowserFields, - data: auditdFile, - timelineId: 'test', - }); - const wrapper = mount( - - {children} - - ); - expect(wrapper.text()).toContain( - 'Sessionunsetroot@zeek-londonin/some text/proc/15990/attr/currentusingsystemd-journal(27244)/lib/systemd/systemd-journaldwith resultsuccess' - ); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/helpers.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/helpers.test.tsx deleted file mode 100644 index 98a99cb6e40897..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/helpers.test.tsx +++ /dev/null @@ -1,284 +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 { cloneDeep } from 'lodash/fp'; - -import { TimelineNonEcsData } from '../../../../graphql/types'; -import { mockTimelineData } from '../../../../mock'; -import { - deleteItemIdx, - findItem, - getValues, - isFileEvent, - isNillEmptyOrNotFinite, - isProcessStoppedOrTerminationEvent, - showVia, -} from './helpers'; - -describe('helpers', () => { - describe('#deleteItemIdx', () => { - let mockDatum: TimelineNonEcsData[]; - beforeEach(() => { - mockDatum = cloneDeep(mockTimelineData[0].data); - }); - - test('should delete part of a value value', () => { - const deleted = deleteItemIdx(mockDatum, 1); - const expected: TimelineNonEcsData[] = [ - { field: '@timestamp', value: ['2018-11-05T19:03:25.937Z'] }, - // { field: 'event.category', value: ['Access'] <-- deleted entry - { field: 'event.category', value: ['Access'] }, - { field: 'event.action', value: ['Action'] }, - { field: 'host.name', value: ['apache'] }, - { field: 'source.ip', value: ['192.168.0.1'] }, - { field: 'destination.ip', value: ['192.168.0.3'] }, - { field: 'destination.bytes', value: ['123456'] }, - { field: 'user.name', value: ['john.dee'] }, - ]; - expect(deleted).toEqual(expected); - }); - - test('should not delete any part of the value, when the value when out of bounds', () => { - const deleted = deleteItemIdx(mockDatum, 1000); - const expected: TimelineNonEcsData[] = [ - { field: '@timestamp', value: ['2018-11-05T19:03:25.937Z'] }, - { field: 'event.severity', value: ['3'] }, - { field: 'event.category', value: ['Access'] }, - { field: 'event.action', value: ['Action'] }, - { field: 'host.name', value: ['apache'] }, - { field: 'source.ip', value: ['192.168.0.1'] }, - { field: 'destination.ip', value: ['192.168.0.3'] }, - { field: 'destination.bytes', value: ['123456'] }, - { field: 'user.name', value: ['john.dee'] }, - ]; - expect(deleted).toEqual(expected); - }); - }); - - describe('#findItem', () => { - let mockDatum: TimelineNonEcsData[]; - beforeEach(() => { - mockDatum = cloneDeep(mockTimelineData[0].data); - }); - test('should find an index with non-zero', () => { - expect(findItem(mockDatum, 'event.severity')).toEqual(1); - }); - - test('should return -1 with a field not found', () => { - expect(findItem(mockDatum, 'event.made-up')).toEqual(-1); - }); - }); - - describe('#getValues', () => { - let mockDatum: TimelineNonEcsData[]; - beforeEach(() => { - mockDatum = cloneDeep(mockTimelineData[0].data); - }); - - test('should return a valid value', () => { - expect(getValues('event.severity', mockDatum)).toEqual(['3']); - }); - - test('should return undefined when the value is not found', () => { - expect(getValues('event.made-up-value', mockDatum)).toBeUndefined(); - }); - - test('should return an undefined when the value found is null', () => { - const nullValue: TimelineNonEcsData[] = [ - { field: '@timestamp', value: ['2018-11-05T19:03:25.937Z'] }, - { field: 'event.severity', value: ['3'] }, - { field: 'event.category', value: ['Access'] }, - { field: 'event.action', value: ['Action'] }, - { field: 'host.name', value: ['apache'] }, - { field: 'source.ip', value: ['192.168.0.1'] }, - { field: 'destination.ip', value: ['192.168.0.3'] }, - { field: 'destination.bytes', value: ['123456'] }, - { field: 'user.name', value: null }, - ]; - expect(getValues('user.name', nullValue)).toBeUndefined(); - }); - - test('should return an undefined when the value found is undefined', () => { - const nullValue: TimelineNonEcsData[] = [ - { field: '@timestamp', value: ['2018-11-05T19:03:25.937Z'] }, - { field: 'event.severity', value: ['3'] }, - { field: 'event.category', value: ['Access'] }, - { field: 'event.action', value: ['Action'] }, - { field: 'host.name', value: ['apache'] }, - { field: 'source.ip', value: ['192.168.0.1'] }, - { field: 'destination.ip', value: ['192.168.0.3'] }, - { field: 'destination.bytes', value: ['123456'] }, - { field: 'user.name', value: undefined }, - ]; - expect(getValues('user.name', nullValue)).toBeUndefined(); - }); - - test('should return an undefined when the value is not present', () => { - const nullValue: TimelineNonEcsData[] = [ - { field: '@timestamp', value: ['2018-11-05T19:03:25.937Z'] }, - { field: 'event.severity', value: ['3'] }, - { field: 'event.category', value: ['Access'] }, - { field: 'event.action', value: ['Action'] }, - { field: 'host.name', value: ['apache'] }, - { field: 'source.ip', value: ['192.168.0.1'] }, - { field: 'destination.ip', value: ['192.168.0.3'] }, - { field: 'destination.bytes', value: ['123456'] }, - { field: 'user.name' }, - ]; - expect(getValues('user.name', nullValue)).toBeUndefined(); - }); - }); - - describe('#isNillEmptyOrNotFinite', () => { - test('undefined returns true', () => { - expect(isNillEmptyOrNotFinite(undefined)).toBe(true); - }); - - test('null returns true', () => { - expect(isNillEmptyOrNotFinite(null)).toBe(true); - }); - - test('empty string returns true', () => { - expect(isNillEmptyOrNotFinite('')).toBe(true); - }); - - test('empty array returns true', () => { - expect(isNillEmptyOrNotFinite([])).toBe(true); - }); - - test('NaN returns true', () => { - expect(isNillEmptyOrNotFinite(NaN)).toBe(true); - }); - - test('Infinity returns true', () => { - expect(isNillEmptyOrNotFinite(Infinity)).toBe(true); - }); - - test('a single space string returns false', () => { - expect(isNillEmptyOrNotFinite(' ')).toBe(false); - }); - - test('a simple string returns false', () => { - expect(isNillEmptyOrNotFinite('a simple string')).toBe(false); - }); - - test('the number 0 returns false', () => { - expect(isNillEmptyOrNotFinite(0)).toBe(false); - }); - - test('a non-empty array return false', () => { - expect(isNillEmptyOrNotFinite(['non empty array'])).toBe(false); - }); - }); - - describe('#showVia', () => { - test('undefined returns false', () => { - expect(showVia(undefined)).toBe(false); - }); - - test('null returns false', () => { - expect(showVia(null)).toBe(false); - }); - - test('empty string returns false', () => { - expect(showVia('')).toBe(false); - }); - - test('a random string returns false', () => { - expect(showVia('a random string')).toBe(false); - }); - - describe('valid values', () => { - const validValues = ['file_create_event', 'created', 'file_delete_event', 'deleted']; - - validValues.forEach(eventAction => { - test(`${eventAction} returns true`, () => { - expect(showVia(eventAction)).toBe(true); - }); - }); - - validValues.forEach(value => { - const upperCaseValue = value.toUpperCase(); - - test(`${upperCaseValue} (upper case) returns true`, () => { - expect(showVia(upperCaseValue)).toBe(true); - }); - }); - }); - }); - - describe('#isFileEvent', () => { - test('returns true when both eventCategory and eventDataset are file', () => { - expect(isFileEvent({ eventCategory: 'file', eventDataset: 'file' })).toBe(true); - }); - - test('returns false when eventCategory and eventDataset are undefined', () => { - expect(isFileEvent({ eventCategory: undefined, eventDataset: undefined })).toBe(false); - }); - - test('returns false when eventCategory and eventDataset are null', () => { - expect(isFileEvent({ eventCategory: null, eventDataset: null })).toBe(false); - }); - - test('returns false when eventCategory and eventDataset are random values', () => { - expect( - isFileEvent({ eventCategory: 'random category', eventDataset: 'random dataset' }) - ).toBe(false); - }); - - test('returns true when just eventCategory is file', () => { - expect(isFileEvent({ eventCategory: 'file', eventDataset: undefined })).toBe(true); - }); - - test('returns true when just eventDataset is file', () => { - expect(isFileEvent({ eventCategory: null, eventDataset: 'file' })).toBe(true); - }); - - test('returns true when just eventCategory is File with a capitol F', () => { - expect(isFileEvent({ eventCategory: 'File', eventDataset: '' })).toBe(true); - }); - - test('returns true when just eventDataset is File with a capitol F', () => { - expect(isFileEvent({ eventCategory: 'random', eventDataset: 'File' })).toBe(true); - }); - }); - - describe('#isProcessStoppedOrTerminationEvent', () => { - test('returns false when eventAction is undefined', () => { - expect(isProcessStoppedOrTerminationEvent(undefined)).toBe(false); - }); - - test('returns false when eventAction is null', () => { - expect(isProcessStoppedOrTerminationEvent(null)).toBe(false); - }); - - test('returns false when eventAction is an empty string', () => { - expect(isProcessStoppedOrTerminationEvent('')).toBe(false); - }); - - test('returns false when eventAction is a random value', () => { - expect(isProcessStoppedOrTerminationEvent('a random value')).toBe(false); - }); - - describe('valid values', () => { - const validValues = ['process_stopped', 'termination_event']; - - validValues.forEach(value => { - test(`returns true when eventAction is ${value}`, () => { - expect(isProcessStoppedOrTerminationEvent(value)).toBe(true); - }); - }); - - validValues.forEach(value => { - const upperCaseValue = value.toUpperCase(); - - test(`returns true when eventAction is (upper case) ${upperCaseValue}`, () => { - expect(isProcessStoppedOrTerminationEvent(upperCaseValue)).toBe(true); - }); - }); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/helpers.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/helpers.tsx deleted file mode 100644 index 26aa5cea51ce73..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/helpers.tsx +++ /dev/null @@ -1,63 +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 { EuiFlexItem } from '@elastic/eui'; -import { isNumber, isEmpty } from 'lodash/fp'; -import styled from 'styled-components'; - -import { TimelineNonEcsData } from '../../../../graphql/types'; - -export const deleteItemIdx = (data: TimelineNonEcsData[], idx: number) => [ - ...data.slice(0, idx), - ...data.slice(idx + 1), -]; - -export const findItem = (data: TimelineNonEcsData[], field: string): number => - data.findIndex(d => d.field === field); - -export const getValues = (field: string, data: TimelineNonEcsData[]): string[] | undefined => { - const obj = data.find(d => d.field === field); - if (obj != null && obj.value != null) { - return obj.value; - } - return undefined; -}; - -export const Details = styled.div` - margin: 5px 0 5px 10px; - & .euiBadge { - margin: 2px 0 2px 0; - } -`; -Details.displayName = 'Details'; - -export const TokensFlexItem = styled(EuiFlexItem)` - margin-left: 3px; -`; -TokensFlexItem.displayName = 'TokensFlexItem'; - -export function isNillEmptyOrNotFinite(value: string | number | T[] | null | undefined) { - return isNumber(value) ? !isFinite(value) : isEmpty(value); -} - -export const isFileEvent = ({ - eventCategory, - eventDataset, -}: { - eventCategory: string | null | undefined; - eventDataset: string | null | undefined; -}) => - (eventCategory != null && eventCategory.toLowerCase() === 'file') || - (eventDataset != null && eventDataset.toLowerCase() === 'file'); - -export const isProcessStoppedOrTerminationEvent = ( - eventAction: string | null | undefined -): boolean => ['process_stopped', 'termination_event'].includes(`${eventAction}`.toLowerCase()); - -export const showVia = (eventAction: string | null | undefined): boolean => - ['file_create_event', 'created', 'file_delete_event', 'deleted'].includes( - `${eventAction}`.toLowerCase() - ); diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/system/generic_details.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/system/generic_details.test.tsx deleted file mode 100644 index 19113d93f7cb03..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/system/generic_details.test.tsx +++ /dev/null @@ -1,542 +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 { shallow } from 'enzyme'; -import React from 'react'; - -import { BrowserFields } from '../../../../../containers/source'; -import { mockBrowserFields } from '../../../../../containers/source/mock'; -import { mockTimelineData, TestProviders } from '../../../../../mock'; -import { SystemGenericDetails, SystemGenericLine } from './generic_details'; -import { useMountAppended } from '../../../../../utils/use_mount_appended'; - -describe('SystemGenericDetails', () => { - const mount = useMountAppended(); - - describe('rendering', () => { - test('it renders the default SystemGenericDetails', () => { - // I cannot and do not want to use BrowserFields for the mocks for the snapshot tests as they are too heavy - const browserFields: BrowserFields = {}; - const wrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); - }); - - test('it returns system rendering if the data does contain system data', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual( - 'Braden@zeek-london[generic-text-123](6278)with resultfailureSource128.199.212.120' - ); - }); - }); - - describe('#SystemGenericLine', () => { - test('it returns pretty output if you send in all your happy path data', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual( - '[username-123]\\[userDomain-123]@[hostname-123]in[working-directory-123][generic-text-123][processName-123](123)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' - ); - }); - - test('it returns nothing if data is all null', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual(''); - }); - - test('it can return only the host name', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual('[hostname-123]'); - }); - - test('it can return the host, message', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual('[hostname-123][message-123]'); - }); - - test('it can return the host, message, outcome', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual('[hostname-123]with result[outcome-123][message-123]'); - }); - - test('it can return the host, message, outcome, packageName', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual( - '[hostname-123]with result[outcome-123][packageName-123][message-123]' - ); - }); - - test('it can return the host, message, outcome, packageName, pacakgeSummary', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual( - '[hostname-123]with result[outcome-123][packageName-123][packageSummary-123][message-123]' - ); - }); - - test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual( - '[hostname-123]with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' - ); - }); - - test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual( - '[hostname-123][processExecutable-123]with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' - ); - }); - - test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual( - '[hostname-123][processExecutable-123](123)with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' - ); - }); - - test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual( - '[hostname-123][processName-123](123)with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' - ); - }); - - test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName, sshMethod', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual( - '[hostname-123][processName-123](123)with result[outcome-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' - ); - }); - - test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName, sshMethod, sshSignature', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual( - '[hostname-123][processName-123](123)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' - ); - }); - - test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName, sshMethod, sshSignature, text', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual( - '[hostname-123][text-123][processName-123](123)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' - ); - }); - - test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName, sshMethod, sshSignature, text, userDomain, username', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual( - '[username-123]\\[userDomain-123]@[hostname-123][text-123][processName-123](123)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' - ); - }); - - test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName, sshMethod, sshSignature, text, userDomain, username, working-directory', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual( - '[username-123]\\[userDomain-123]@[hostname-123]in[working-directory-123][text-123][processName-123](123)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' - ); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/system/generic_details.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/system/generic_details.tsx deleted file mode 100644 index 2ad3eb46814542..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/system/generic_details.tsx +++ /dev/null @@ -1,189 +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 { EuiFlexGroup, EuiSpacer } from '@elastic/eui'; -import { get } from 'lodash/fp'; -import React from 'react'; - -import { BrowserFields } from '../../../../../containers/source'; -import { Ecs } from '../../../../../graphql/types'; -import { DraggableBadge } from '../../../../draggables'; -import { OverflowField } from '../../../../tables/helpers'; - -import * as i18n from './translations'; -import { NetflowRenderer } from '../netflow'; -import { UserHostWorkingDir } from '../user_host_working_dir'; -import { Details, TokensFlexItem } from '../helpers'; -import { ProcessDraggable } from '../process_draggable'; -import { Package } from './package'; -import { AuthSsh } from './auth_ssh'; -import { Badge } from '../../../../page'; - -interface Props { - contextId: string; - hostName: string | null | undefined; - id: string; - message: string | null | undefined; - outcome: string | null | undefined; - packageName: string | null | undefined; - packageSummary: string | null | undefined; - packageVersion: string | null | undefined; - processExecutable: string | null | undefined; - processPid: number | null | undefined; - processName: string | null | undefined; - sshMethod: string | null | undefined; - sshSignature: string | null | undefined; - text: string | null | undefined; - userDomain: string | null | undefined; - userName: string | null | undefined; - workingDirectory: string | null | undefined; -} - -export const SystemGenericLine = React.memo( - ({ - contextId, - hostName, - id, - message, - outcome, - packageName, - packageSummary, - packageVersion, - processPid, - processName, - processExecutable, - sshSignature, - sshMethod, - text, - userDomain, - userName, - workingDirectory, - }) => ( - <> - - - - {text} - - - - - {outcome != null && ( - - {i18n.WITH_RESULT} - - )} - - - - - - - {message != null && ( - <> - - - - - - - - - - )} - - ) -); - -SystemGenericLine.displayName = 'SystemGenericLine'; - -interface GenericDetailsProps { - browserFields: BrowserFields; - data: Ecs; - contextId: string; - text: string; - timelineId: string; -} - -export const SystemGenericDetails = React.memo( - ({ data, contextId, text, timelineId }) => { - const id = data._id; - const message: string | null = data.message != null ? data.message[0] : null; - const hostName: string | null | undefined = get('host.name[0]', data); - const userDomain: string | null | undefined = get('user.domain[0]', data); - const userName: string | null | undefined = get('user.name[0]', data); - const outcome: string | null | undefined = get('event.outcome[0]', data); - const packageName: string | null | undefined = get('system.audit.package.name[0]', data); - const packageSummary: string | null | undefined = get('system.audit.package.summary[0]', data); - const packageVersion: string | null | undefined = get('system.audit.package.version[0]', data); - const processPid: number | null | undefined = get('process.pid[0]', data); - const processName: string | null | undefined = get('process.name[0]', data); - const processExecutable: string | null | undefined = get('process.executable[0]', data); - const sshSignature: string | null | undefined = get('system.auth.ssh.signature[0]', data); - const sshMethod: string | null | undefined = get('system.auth.ssh.method[0]', data); - const workingDirectory: string | null | undefined = get('process.working_directory[0]', data); - - return ( -
- - - -
- ); - } -); - -SystemGenericDetails.displayName = 'SystemGenericDetails'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.test.tsx deleted file mode 100644 index cab7191c13aef5..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.test.tsx +++ /dev/null @@ -1,1599 +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 { shallow } from 'enzyme'; -import React from 'react'; - -import { BrowserFields } from '../../../../../containers/source'; -import { mockBrowserFields } from '../../../../../containers/source/mock'; -import { mockTimelineData, TestProviders } from '../../../../../mock'; -import { SystemGenericFileDetails, SystemGenericFileLine } from './generic_file_details'; -import { useMountAppended } from '../../../../../utils/use_mount_appended'; - -describe('SystemGenericFileDetails', () => { - const mount = useMountAppended(); - - describe('rendering', () => { - test('it renders the default SystemGenericDetails', () => { - // I cannot and do not want to use BrowserFields for the mocks for the snapshot tests as they are too heavy - const browserFields: BrowserFields = {}; - const wrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); - }); - - test('it returns system rendering if the data does contain system data', () => { - const wrapper = mount( - - - - ); - expect(wrapper.text()).toEqual( - 'Evan@zeek-london[generic-text-123](6278)with resultfailureSource128.199.212.120' - ); - }); - }); - - describe('#SystemGenericFileLine', () => { - test('it returns pretty output if you send in all your happy path data', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual( - '[username-123]\\[userDomain-123]@[hostname-123]in[working-directory-123][generic-text-123][fileName-123]in[filePath-123][processName-123](123)[arg-1][arg-2][arg-3][some-title-123]with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' - ); - }); - - test('it returns nothing if data is all null', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual('an unknown process'); - }); - - test('it can return only the host name', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual('[hostname-123]an unknown process'); - }); - - test('it can return the host, message', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual('[hostname-123]an unknown process[message-123]'); - }); - - test('it can return the host, message, outcome', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual( - '[hostname-123]an unknown processwith result[outcome-123][message-123]' - ); - }); - - test('it can return the host, message, outcome, packageName', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual( - '[hostname-123]an unknown processwith result[outcome-123][packageName-123][message-123]' - ); - }); - - test('it can return the host, message, outcome, packageName, pacakgeSummary', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual( - '[hostname-123]an unknown processwith result[outcome-123][packageName-123][packageSummary-123][message-123]' - ); - }); - - test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual( - '[hostname-123]an unknown processwith result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' - ); - }); - - test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual( - '[hostname-123][packageVersion-123]with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' - ); - }); - - test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual( - '[hostname-123][packageVersion-123]with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][processHashMd5-123][message-123]' - ); - }); - - test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual( - '[hostname-123][packageVersion-123]with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha1-123][processHashMd5-123][message-123]' - ); - }); - - test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual( - '[hostname-123][packageVersion-123]with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' - ); - }); - - test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual( - '[hostname-123][processExecutable-123](123)with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' - ); - }); - - test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual( - '[hostname-123][processName-123](123)via parent process(456)with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' - ); - }); - - test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual( - '[hostname-123][processName-123](123)with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' - ); - }); - - test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual( - '[hostname-123][processName-123](123)with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' - ); - }); - - test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual( - '[hostname-123][text-123][processName-123](123)with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' - ); - }); - - test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text, userDomain', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual( - '\\[userDomain-123][hostname-123][text-123][processName-123](123)with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' - ); - }); - - test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text, userDomain, username', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual( - '[username-123]\\[userDomain-123]@[hostname-123][text-123][processName-123](123)with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' - ); - }); - - test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text, userDomain, username, working-directory', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual( - '[username-123]\\[userDomain-123]@[hostname-123]in[working-directory-123][text-123][processName-123](123)with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' - ); - }); - - test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text, userDomain, username, working-directory, process-title', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual( - '[username-123]\\[userDomain-123]@[hostname-123]in[working-directory-123][text-123][processName-123](123)[process-title-123]with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' - ); - }); - - test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text, userDomain, username, working-directory, process-title, args', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual( - '[username-123]\\[userDomain-123]@[hostname-123]in[working-directory-123][text-123][processName-123](123)[arg-1][arg-2][arg-3][process-title-123]with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' - ); - }); - - test('it renders a FileDraggable when endgameFileName and endgameFilePath are provided, but fileName and filePath are NOT provided', () => { - const wrapper = mount( - -
- -
-
- ); - expect(wrapper.text()).toEqual('[endgameFileName]in[endgameFilePath]an unknown process'); - }); - - test('it prefers to render fileName and filePath over endgameFileName and endgameFilePath respectfully when all of those fields are provided', () => { - const wrapper = mount( - -
- -
-
- ); - - expect(wrapper.text()).toEqual('[fileName]in[filePath]an unknown process'); - }); - - ['file_create_event', 'created', 'file_delete_event', 'deleted'].forEach(eventAction => { - test(`it renders the text "via" when eventAction is ${eventAction}`, () => { - const wrapper = mount( - -
- -
-
- ); - - expect(wrapper.text().includes('via')).toBe(true); - }); - }); - - test('it does NOT render the text "via" when eventAction is not a whitelisted action', () => { - const eventAction = 'a_non_whitelisted_event_action'; - - const wrapper = mount( - -
- -
-
- ); - - expect(wrapper.text().includes('via')).toBe(false); - }); - - test('it renders a ParentProcessDraggable when eventAction is NOT "process_stopped" and NOT "termination_event"', () => { - const eventAction = 'something_else'; - - const wrapper = mount( - -
- -
-
- ); - - expect(wrapper.text()).toEqual( - 'an unknown processvia parent process[endgameParentProcessName](456)' - ); - }); - - test('it does NOT render a ParentProcessDraggable when eventAction is "process_stopped"', () => { - const eventAction = 'process_stopped'; - - const wrapper = mount( - -
- -
-
- ); - - expect(wrapper.text()).toEqual('an unknown process'); - }); - - test('it does NOT render a ParentProcessDraggable when eventAction is "termination_event"', () => { - const eventAction = 'termination_event'; - - const wrapper = mount( - -
- -
-
- ); - - expect(wrapper.text()).toEqual('an unknown process'); - }); - - test('it returns renders the message when showMessage is true', () => { - const wrapper = mount( - -
- -
-
- ); - - expect(wrapper.text()).toEqual('an unknown process[message]'); - }); - - test('it does NOT render the message when showMessage is false', () => { - const wrapper = mount( - -
- -
-
- ); - - expect(wrapper.text()).toEqual('an unknown process'); - }); - - test('it renders a ProcessDraggableWithNonExistentProcess when endgamePid and endgameProcessName are provided, but processPid and processName are NOT provided', () => { - const wrapper = mount( - -
- -
-
- ); - - expect(wrapper.text()).toEqual('[endgameProcessName](789)'); - }); - - test('it prefers to render processName and processPid over endgameProcessName and endgamePid respectfully when all of those fields are provided', () => { - const wrapper = mount( - -
- -
-
- ); - - expect(wrapper.text()).toEqual('[processName](123)'); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.tsx deleted file mode 100644 index ef7c3b3ccf8591..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/system/generic_file_details.tsx +++ /dev/null @@ -1,298 +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 { EuiFlexGroup, EuiSpacer } from '@elastic/eui'; -import { get } from 'lodash/fp'; -import React from 'react'; - -import { BrowserFields } from '../../../../../containers/source'; -import { Ecs } from '../../../../../graphql/types'; -import { DraggableBadge } from '../../../../draggables'; -import { OverflowField } from '../../../../tables/helpers'; - -import * as i18n from './translations'; -import { NetflowRenderer } from '../netflow'; -import { UserHostWorkingDir } from '../user_host_working_dir'; -import { Details, isProcessStoppedOrTerminationEvent, showVia, TokensFlexItem } from '../helpers'; -import { ProcessDraggableWithNonExistentProcess } from '../process_draggable'; -import { Args } from '../args'; -import { AuthSsh } from './auth_ssh'; -import { ExitCodeDraggable } from '../exit_code_draggable'; -import { FileDraggable } from '../file_draggable'; -import { Package } from './package'; -import { Badge } from '../../../../page'; -import { ParentProcessDraggable } from '../parent_process_draggable'; -import { ProcessHash } from '../process_hash'; - -interface Props { - args: string[] | null | undefined; - contextId: string; - endgameExitCode: string | null | undefined; - endgameFileName: string | null | undefined; - endgameFilePath: string | null | undefined; - endgameParentProcessName: string | null | undefined; - endgamePid: number | null | undefined; - endgameProcessName: string | null | undefined; - eventAction: string | null | undefined; - fileName: string | null | undefined; - filePath: string | null | undefined; - hostName: string | null | undefined; - id: string; - message: string | null | undefined; - outcome: string | null | undefined; - packageName: string | null | undefined; - packageSummary: string | null | undefined; - packageVersion: string | null | undefined; - processName: string | null | undefined; - processPid: number | null | undefined; - processPpid: number | null | undefined; - processExecutable: string | null | undefined; - processHashMd5: string | null | undefined; - processHashSha1: string | null | undefined; - processHashSha256: string | null | undefined; - processTitle: string | null | undefined; - showMessage: boolean; - sshSignature: string | null | undefined; - sshMethod: string | null | undefined; - text: string | null | undefined; - userDomain: string | null | undefined; - userName: string | null | undefined; - workingDirectory: string | null | undefined; -} - -export const SystemGenericFileLine = React.memo( - ({ - args, - contextId, - endgameExitCode, - endgameFileName, - endgameFilePath, - endgameParentProcessName, - endgamePid, - endgameProcessName, - eventAction, - fileName, - filePath, - hostName, - id, - message, - outcome, - packageName, - packageSummary, - packageVersion, - processExecutable, - processHashMd5, - processHashSha1, - processHashSha256, - processName, - processPid, - processPpid, - processTitle, - showMessage, - sshSignature, - sshMethod, - text, - userDomain, - userName, - workingDirectory, - }) => ( - <> - - - - {text} - - - {showVia(eventAction) && ( - - {i18n.VIA} - - )} - - - - - - {!isProcessStoppedOrTerminationEvent(eventAction) && ( - - )} - {outcome != null && ( - - {i18n.WITH_RESULT} - - )} - - - - - - - - - {message != null && showMessage && ( - <> - - - - - - - - - - )} - - ) -); - -SystemGenericFileLine.displayName = 'SystemGenericFileLine'; - -interface GenericDetailsProps { - browserFields: BrowserFields; - data: Ecs; - contextId: string; - showMessage?: boolean; - text: string; - timelineId: string; -} - -export const SystemGenericFileDetails = React.memo( - ({ data, contextId, showMessage = true, text, timelineId }) => { - const id = data._id; - const message: string | null = data.message != null ? data.message[0] : null; - const hostName: string | null | undefined = get('host.name[0]', data); - const endgameExitCode: string | null | undefined = get('endgame.exit_code[0]', data); - const endgameFileName: string | null | undefined = get('endgame.file_name[0]', data); - const endgameFilePath: string | null | undefined = get('endgame.file_path[0]', data); - const endgameParentProcessName: string | null | undefined = get( - 'endgame.parent_process_name[0]', - data - ); - const endgamePid: number | null | undefined = get('endgame.pid[0]', data); - const endgameProcessName: string | null | undefined = get('endgame.process_name[0]', data); - const eventAction: string | null | undefined = get('event.action[0]', data); - const fileName: string | null | undefined = get('file.name[0]', data); - const filePath: string | null | undefined = get('file.path[0]', data); - const userDomain: string | null | undefined = get('user.domain[0]', data); - const userName: string | null | undefined = get('user.name[0]', data); - const outcome: string | null | undefined = get('event.outcome[0]', data); - const packageName: string | null | undefined = get('system.audit.package.name[0]', data); - const packageSummary: string | null | undefined = get('system.audit.package.summary[0]', data); - const packageVersion: string | null | undefined = get('system.audit.package.version[0]', data); - const processHashMd5: string | null | undefined = get('process.hash.md5[0]', data); - const processHashSha1: string | null | undefined = get('process.hash.sha1[0]', data); - const processHashSha256: string | null | undefined = get('process.hash.sha256', data); - const processPid: number | null | undefined = get('process.pid[0]', data); - const processPpid: number | null | undefined = get('process.ppid[0]', data); - const processName: string | null | undefined = get('process.name[0]', data); - const sshSignature: string | null | undefined = get('system.auth.ssh.signature[0]', data); - const sshMethod: string | null | undefined = get('system.auth.ssh.method[0]', data); - const processExecutable: string | null | undefined = get('process.executable[0]', data); - const processTitle: string | null | undefined = get('process.title[0]', data); - const workingDirectory: string | null | undefined = get('process.working_directory[0]', data); - const args: string[] | null | undefined = get('process.args', data); - - return ( -
- - - -
- ); - } -); - -SystemGenericFileDetails.displayName = 'SystemGenericFileDetails'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.test.tsx b/x-pack/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.test.tsx deleted file mode 100644 index 2f5fa76855f2be..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.test.tsx +++ /dev/null @@ -1,936 +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 { shallow } from 'enzyme'; -import { cloneDeep } from 'lodash/fp'; -import React from 'react'; - -import { BrowserFields } from '../../../../../containers/source'; -import { mockBrowserFields } from '../../../../../containers/source/mock'; -import { Ecs } from '../../../../../graphql/types'; -import { - mockDnsEvent, - mockFimFileCreatedEvent, - mockFimFileDeletedEvent, - mockSocketClosedEvent, - mockSocketOpenedEvent, - mockTimelineData, - TestProviders, -} from '../../../../../mock'; -import { - mockEndgameAdminLogon, - mockEndgameCreationEvent, - mockEndgameDnsRequest, - mockEndgameExplicitUserLogon, - mockEndgameFileCreateEvent, - mockEndgameFileDeleteEvent, - mockEndgameIpv4ConnectionAcceptEvent, - mockEndgameIpv6ConnectionAcceptEvent, - mockEndgameIpv4DisconnectReceivedEvent, - mockEndgameIpv6DisconnectReceivedEvent, - mockEndgameTerminationEvent, - mockEndgameUserLogoff, - mockEndgameUserLogon, -} from '../../../../../mock/mock_endgame_ecs_data'; -import { useMountAppended } from '../../../../../utils/use_mount_appended'; -import { RowRenderer } from '../row_renderer'; -import { - createDnsRowRenderer, - createEndgameProcessRowRenderer, - createFimRowRenderer, - createGenericSystemRowRenderer, - createGenericFileRowRenderer, - createSecurityEventRowRenderer, - createSocketRowRenderer, -} from './generic_row_renderer'; -import * as i18n from './translations'; - -jest.mock('../../../../../pages/overview/events_by_dataset'); - -describe('GenericRowRenderer', () => { - const mount = useMountAppended(); - - describe('#createGenericSystemRowRenderer', () => { - let nonSystem: Ecs; - let system: Ecs; - let connectedToRenderer: RowRenderer; - beforeEach(() => { - nonSystem = cloneDeep(mockTimelineData[0].ecs); - system = cloneDeep(mockTimelineData[29].ecs); - connectedToRenderer = createGenericSystemRowRenderer({ - actionName: 'process_started', - text: 'some text', - }); - }); - test('renders correctly against snapshot', () => { - // I cannot and do not want to use BrowserFields mocks for the snapshot tests as they are too heavy - const browserFields: BrowserFields = {}; - const children = connectedToRenderer.renderRow({ - browserFields, - data: system, - timelineId: 'test', - }); - - const wrapper = shallow({children}); - expect(wrapper).toMatchSnapshot(); - }); - - test('should return false if not a system datum', () => { - expect(connectedToRenderer.isInstance(nonSystem)).toBe(false); - }); - - test('should return true if it is a system datum', () => { - expect(connectedToRenderer.isInstance(system)).toBe(true); - }); - - test('should return false when action is set to some other value', () => { - if (system.event != null && system.event.action != null) { - system.event.action[0] = 'some other value'; - expect(connectedToRenderer.isInstance(system)).toBe(false); - } else { - // if system.event or system.event.action is not defined in the mock - // then we will get an error here - expect(system.event).toBeDefined(); - } - }); - test('should render a system row', () => { - const children = connectedToRenderer.renderRow({ - browserFields: mockBrowserFields, - data: system, - timelineId: 'test', - }); - const wrapper = mount( - - {children} - - ); - expect(wrapper.text()).toContain( - 'Evan@zeek-londonsome text(6278)with resultfailureSource128.199.212.120' - ); - }); - }); - - describe('#createGenericFileRowRenderer', () => { - let nonSystem: Ecs; - let systemFile: Ecs; - let fileToRenderer: RowRenderer; - - beforeEach(() => { - nonSystem = cloneDeep(mockTimelineData[0].ecs); - systemFile = cloneDeep(mockTimelineData[28].ecs); - fileToRenderer = createGenericFileRowRenderer({ - actionName: 'user_login', - text: 'some text', - }); - }); - - test('renders correctly against snapshot', () => { - // I cannot and do not want to use BrowserFields mocks for the snapshot tests as they are too heavy - const browserFields: BrowserFields = {}; - const children = fileToRenderer.renderRow({ - browserFields, - data: systemFile, - timelineId: 'test', - }); - - const wrapper = shallow({children}); - expect(wrapper).toMatchSnapshot(); - }); - - test('should return false if not a auditd datum', () => { - expect(fileToRenderer.isInstance(nonSystem)).toBe(false); - }); - - test('should return true if it is a auditd datum', () => { - expect(fileToRenderer.isInstance(systemFile)).toBe(true); - }); - - test('should return false when action is set to some other value', () => { - if (systemFile.event != null && systemFile.event.action != null) { - systemFile.event.action[0] = 'some other value'; - expect(fileToRenderer.isInstance(systemFile)).toBe(false); - } else { - expect(systemFile.event).toBeDefined(); - } - }); - - test('should render a system row', () => { - const children = fileToRenderer.renderRow({ - browserFields: mockBrowserFields, - data: systemFile, - timelineId: 'test', - }); - const wrapper = mount( - - {children} - - ); - expect(wrapper.text()).toContain( - 'Braden@zeek-londonsome text(6278)with resultfailureSource128.199.212.120' - ); - }); - }); - - describe('#createEndgameProcessRowRenderer', () => { - test('it renders an endgame process creation_event', () => { - const actionName = 'creation_event'; - const text = i18n.PROCESS_STARTED; - const endgameCreationEvent = { - ...mockEndgameCreationEvent, - }; - - const endgameProcessCreationEventRowRenderer = createEndgameProcessRowRenderer({ - actionName, - text, - }); - - const wrapper = mount( - - {endgameProcessCreationEventRowRenderer.isInstance(endgameCreationEvent) && - endgameProcessCreationEventRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: endgameCreationEvent, - timelineId: 'test', - })} - - ); - - expect(wrapper.text()).toEqual( - 'Arun\\Anvi-Acer@HD-obe-8bf77f54started processMicrosoft.Photos.exe(441684)C:\\Program Files\\WindowsApps\\Microsoft.Windows.Photos_2018.18091.17210.0_x64__8wekyb3d8bbwe\\Microsoft.Photos.exe-ServerName:App.AppXzst44mncqdg84v7sv6p7yznqwssy6f7f.mcavia parent processsvchost.exe(8)d4c97ed46046893141652e2ec0056a698f6445109949d7fcabbce331146889ee12563599116157778a22600d2a163d8112aed84562d06d7235b37895b68de56687895743' - ); - }); - - test('it renders an endgame process termination_event', () => { - const actionName = 'termination_event'; - const text = i18n.TERMINATED_PROCESS; - const endgameTerminationEvent = { - ...mockEndgameTerminationEvent, - }; - - const endgameProcessTerminationEventRowRenderer = createEndgameProcessRowRenderer({ - actionName, - text, - }); - - const wrapper = mount( - - {endgameProcessTerminationEventRowRenderer.isInstance(endgameTerminationEvent) && - endgameProcessTerminationEventRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: endgameTerminationEvent, - timelineId: 'test', - })} - - ); - - expect(wrapper.text()).toEqual( - 'Arun\\Anvi-Acer@HD-obe-8bf77f54terminated processRuntimeBroker.exe(442384)with exit code087976f3430cc99bc939e0694247c0759961a49832b87218f4313d6fc0bc3a776797255e72d5ed5c058d4785950eba7abaa057653bd4401441a21bf1abce6404f4231db4d' - ); - }); - - test('it does NOT render the event if the action name does not match', () => { - const actionName = 'does_not_match'; - const text = i18n.PROCESS_STARTED; - const endgameCreationEvent = { - ...mockEndgameCreationEvent, - }; - - const endgameProcessCreationEventRowRenderer = createEndgameProcessRowRenderer({ - actionName, - text, - }); - - const wrapper = mount( - - {endgameProcessCreationEventRowRenderer.isInstance(endgameCreationEvent) && - endgameProcessCreationEventRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: endgameCreationEvent, - timelineId: 'test', - })} - - ); - - expect(wrapper.text()).toEqual(''); - }); - - test('it does NOT render the event when the event category is NOT process', () => { - const actionName = 'creation_event'; - const text = i18n.PROCESS_STARTED; - const endgameCreationEvent = { - ...mockEndgameCreationEvent, - event: { - ...mockEndgameCreationEvent.event, - category: ['something_else'], - }, - }; - - const endgameProcessCreationEventRowRenderer = createEndgameProcessRowRenderer({ - actionName, - text, - }); - - const wrapper = mount( - - {endgameProcessCreationEventRowRenderer.isInstance(endgameCreationEvent) && - endgameProcessCreationEventRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: endgameCreationEvent, - timelineId: 'test', - })} - - ); - - expect(wrapper.text()).toEqual(''); - }); - - test('it does NOT render the event when both the action name and event category do NOT match', () => { - const actionName = 'does_not_match'; - const text = i18n.PROCESS_STARTED; - const endgameCreationEvent = { - ...mockEndgameCreationEvent, - event: { - ...mockEndgameCreationEvent.event, - category: ['something_else'], - }, - }; - - const endgameProcessCreationEventRowRenderer = createEndgameProcessRowRenderer({ - actionName, - text, - }); - - const wrapper = mount( - - {endgameProcessCreationEventRowRenderer.isInstance(endgameCreationEvent) && - endgameProcessCreationEventRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: endgameCreationEvent, - timelineId: 'test', - })} - - ); - - expect(wrapper.text()).toEqual(''); - }); - }); - - describe('#createFimRowRenderer', () => { - test('it renders an endgame file_create_event', () => { - const actionName = 'file_create_event'; - const text = i18n.CREATED_FILE; - const endgameFileCreateEvent = { - ...mockEndgameFileCreateEvent, - }; - - const endgameFileCreateEventRowRenderer = createFimRowRenderer({ - actionName, - text, - }); - - const wrapper = mount( - - {endgameFileCreateEventRowRenderer.isInstance(endgameFileCreateEvent) && - endgameFileCreateEventRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: endgameFileCreateEvent, - timelineId: 'test', - })} - - ); - - expect(wrapper.text()).toEqual( - 'Arun\\Anvi-Acer@HD-obe-8bf77f54created a fileinC:\\Users\\Arun\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\63d78c21-e593-4484-b7a9-db33cd522ddc.tmpviachrome.exe(11620)' - ); - }); - - test('it renders an endgame file_delete_event', () => { - const actionName = 'file_delete_event'; - const text = i18n.DELETED_FILE; - const endgameFileDeleteEvent = { - ...mockEndgameFileDeleteEvent, - }; - - const endgameFileDeleteEventRowRenderer = createFimRowRenderer({ - actionName, - text, - }); - - const wrapper = mount( - - {endgameFileDeleteEventRowRenderer.isInstance(endgameFileDeleteEvent) && - endgameFileDeleteEventRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: endgameFileDeleteEvent, - timelineId: 'test', - })} - - ); - - expect(wrapper.text()).toEqual( - 'SYSTEM\\NT AUTHORITY@HD-v1s-d2118419deleted a filetmp000002f6inC:\\Windows\\TEMP\\tmp00000404\\tmp000002f6viaAmSvc.exe(1084)' - ); - }); - - test('it renders a FIM (non-endgame) file created event', () => { - const actionName = 'created'; - const text = i18n.CREATED_FILE; - const fimFileCreatedEvent = { - ...mockFimFileCreatedEvent, - }; - - const fileCreatedEventRowRenderer = createFimRowRenderer({ - actionName, - text, - }); - - const wrapper = mount( - - {fileCreatedEventRowRenderer.isInstance(fimFileCreatedEvent) && - fileCreatedEventRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: fimFileCreatedEvent, - timelineId: 'test', - })} - - ); - - expect(wrapper.text()).toEqual('foohostcreated a filein/etc/subgidviaan unknown process'); - }); - - test('it renders a FIM (non-endgame) file deleted event', () => { - const actionName = 'deleted'; - const text = i18n.DELETED_FILE; - const fimFileDeletedEvent = { - ...mockFimFileDeletedEvent, - }; - - const fileDeletedEventRowRenderer = createFimRowRenderer({ - actionName, - text, - }); - - const wrapper = mount( - - {fileDeletedEventRowRenderer.isInstance(fimFileDeletedEvent) && - fileDeletedEventRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: fimFileDeletedEvent, - timelineId: 'test', - })} - - ); - - expect(wrapper.text()).toEqual( - 'foohostdeleted a filein/etc/gshadow.lockviaan unknown process' - ); - }); - - test('it does NOT render an event if the action name does not match', () => { - const actionName = 'does_not_match'; - const text = i18n.CREATED_FILE; - const endgameFileCreateEvent = { - ...mockEndgameFileCreateEvent, - }; - - const endgameFileCreateEventRowRenderer = createFimRowRenderer({ - actionName, - text, - }); - - const wrapper = mount( - - {endgameFileCreateEventRowRenderer.isInstance(endgameFileCreateEvent) && - endgameFileCreateEventRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: endgameFileCreateEvent, - timelineId: 'test', - })} - - ); - - expect(wrapper.text()).toEqual(''); - }); - - test('it does NOT render an Endgame file_create_event when category is NOT file', () => { - const actionName = 'file_create_event'; - const text = i18n.CREATED_FILE; - const endgameFileCreateEvent = { - ...mockEndgameFileCreateEvent, - event: { - ...mockEndgameFileCreateEvent.event, - category: ['something_else'], - }, - }; - - const endgameFileCreateEventRowRenderer = createFimRowRenderer({ - actionName, - text, - }); - - const wrapper = mount( - - {endgameFileCreateEventRowRenderer.isInstance(endgameFileCreateEvent) && - endgameFileCreateEventRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: endgameFileCreateEvent, - timelineId: 'test', - })} - - ); - - expect(wrapper.text()).toEqual(''); - }); - - test('it does NOT render a FIM (non-Endgame) file created event when the event dataset is NOT file', () => { - const actionName = 'created'; - const text = i18n.CREATED_FILE; - const fimFileCreatedEvent = { - ...mockFimFileCreatedEvent, - event: { - ...mockEndgameFileCreateEvent.event, - dataset: ['something_else'], - }, - }; - - const fileCreatedEventRowRenderer = createFimRowRenderer({ - actionName, - text, - }); - - const wrapper = mount( - - {fileCreatedEventRowRenderer.isInstance(fimFileCreatedEvent) && - fileCreatedEventRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: fimFileCreatedEvent, - timelineId: 'test', - })} - - ); - - expect(wrapper.text()).toEqual(''); - }); - }); - - describe('#createSocketRowRenderer', () => { - test('it renders an Endgame ipv4_connection_accept_event', () => { - const actionName = 'ipv4_connection_accept_event'; - const text = i18n.ACCEPTED_A_CONNECTION_VIA; - const ipv4ConnectionAcceptEvent = { - ...mockEndgameIpv4ConnectionAcceptEvent, - }; - - const endgameIpv4ConnectionAcceptEventRowRenderer = createSocketRowRenderer({ - actionName, - text, - }); - - const wrapper = mount( - - {endgameIpv4ConnectionAcceptEventRowRenderer.isInstance(ipv4ConnectionAcceptEvent) && - endgameIpv4ConnectionAcceptEventRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: ipv4ConnectionAcceptEvent, - timelineId: 'test', - })} - - ); - - expect(wrapper.text()).toEqual( - 'SYSTEM\\NT AUTHORITY@HD-gqf-0af7b4feaccepted a connection viaAmSvc.exe(1084)tcp1:network-community_idSource127.0.0.1:49306Destination127.0.0.1:49305' - ); - }); - - test('it renders an Endgame ipv6_connection_accept_event', () => { - const actionName = 'ipv6_connection_accept_event'; - const text = i18n.ACCEPTED_A_CONNECTION_VIA; - const ipv6ConnectionAcceptEvent = { - ...mockEndgameIpv6ConnectionAcceptEvent, - }; - - const endgameIpv6ConnectionAcceptEventRowRenderer = createSocketRowRenderer({ - actionName, - text, - }); - - const wrapper = mount( - - {endgameIpv6ConnectionAcceptEventRowRenderer.isInstance(ipv6ConnectionAcceptEvent) && - endgameIpv6ConnectionAcceptEventRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: ipv6ConnectionAcceptEvent, - timelineId: 'test', - })} - - ); - - expect(wrapper.text()).toEqual( - 'SYSTEM\\NT AUTHORITY@HD-55b-3ec87f66accepted a connection via(4)tcp1:network-community_idSource::1:51324Destination::1:5357' - ); - }); - - test('it renders an Endgame ipv4_disconnect_received_event', () => { - const actionName = 'ipv4_disconnect_received_event'; - const text = i18n.DISCONNECTED_VIA; - const ipv4DisconnectReceivedEvent = { - ...mockEndgameIpv4DisconnectReceivedEvent, - }; - - const endgameIpv4DisconnectReceivedEventRowRenderer = createSocketRowRenderer({ - actionName, - text, - }); - - const wrapper = mount( - - {endgameIpv4DisconnectReceivedEventRowRenderer.isInstance(ipv4DisconnectReceivedEvent) && - endgameIpv4DisconnectReceivedEventRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: ipv4DisconnectReceivedEvent, - timelineId: 'test', - })} - - ); - - expect(wrapper.text()).toEqual( - 'Arun\\Anvi-Acer@HD-obe-8bf77f54disconnected viachrome.exe(11620)8.1KBtcp1:LxYHJJv98b2O0fNccXu6HheXmwk=Source192.168.0.6:59356(25.78%)2.1KB(74.22%)6KBDestination10.156.162.53:443' - ); - }); - - test('it renders an Endgame ipv6_disconnect_received_event', () => { - const actionName = 'ipv6_disconnect_received_event'; - const text = i18n.DISCONNECTED_VIA; - const ipv6DisconnectReceivedEvent = { - ...mockEndgameIpv6DisconnectReceivedEvent, - }; - - const endgameIpv6DisconnectReceivedEventRowRenderer = createSocketRowRenderer({ - actionName, - text, - }); - - const wrapper = mount( - - {endgameIpv6DisconnectReceivedEventRowRenderer.isInstance(ipv6DisconnectReceivedEvent) && - endgameIpv6DisconnectReceivedEventRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: ipv6DisconnectReceivedEvent, - timelineId: 'test', - })} - - ); - - expect(wrapper.text()).toEqual( - 'SYSTEM\\NT AUTHORITY@HD-55b-3ec87f66disconnected via(4)7.9KBtcp1:ZylzQhsB1dcptA2t4DY8S6l9o8E=Source::1:51338(96.92%)7.7KB(3.08%)249BDestination::1:2869' - ); - }); - - test('it renders a (non-Endgame) socket_opened event', () => { - const actionName = 'socket_opened'; - const text = i18n.SOCKET_OPENED; - const socketOpenedEvent = { - ...mockSocketOpenedEvent, - }; - - const socketOpenedEventRowRenderer = createSocketRowRenderer({ - actionName, - text, - }); - - const wrapper = mount( - - {socketOpenedEventRowRenderer.isInstance(socketOpenedEvent) && - socketOpenedEventRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: socketOpenedEvent, - timelineId: 'test', - })} - - ); - - expect(wrapper.text()).toEqual( - 'root@foohostopened a socket withgoogle_accounts(2166)Outbound socket (10.4.20.1:59554 -> 10.1.2.3:80) Ooutboundtcp1:network-community_idSource10.4.20.1:59554Destination10.1.2.3:80' - ); - }); - - test('it renders a (non-Endgame) socket_closed event', () => { - const actionName = 'socket_closed'; - const text = i18n.SOCKET_CLOSED; - const socketClosedEvent = { - ...mockSocketClosedEvent, - }; - - const socketClosedEventRowRenderer = createSocketRowRenderer({ - actionName, - text, - }); - - const wrapper = mount( - - {socketClosedEventRowRenderer.isInstance(socketClosedEvent) && - socketClosedEventRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: socketClosedEvent, - timelineId: 'test', - })} - - ); - - expect(wrapper.text()).toEqual( - 'root@foohostclosed a socket withgoogle_accounts(2166)Outbound socket (10.4.20.1:59508 -> 10.1.2.3:80) Coutboundtcp1:network-community_idSource10.4.20.1:59508Destination10.1.2.3:80' - ); - }); - - test('it does NOT render an event if the action name does not match', () => { - const actionName = 'does_not_match'; - const text = i18n.ACCEPTED_A_CONNECTION_VIA; - const ipv4ConnectionAcceptEvent = { - ...mockEndgameIpv4ConnectionAcceptEvent, - }; - - const endgameIpv4ConnectionAcceptEventRowRenderer = createSocketRowRenderer({ - actionName, - text, - }); - - const wrapper = mount( - - {endgameIpv4ConnectionAcceptEventRowRenderer.isInstance(ipv4ConnectionAcceptEvent) && - endgameIpv4ConnectionAcceptEventRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: ipv4ConnectionAcceptEvent, - timelineId: 'test', - })} - - ); - - expect(wrapper.text()).toEqual(''); - }); - }); - - describe('#createSecurityEventRowRenderer', () => { - test('it renders an Endgame user_logon event', () => { - const actionName = 'user_logon'; - const userLogonEvent = { - ...mockEndgameUserLogon, - }; - - const userLogonEventRowRenderer = createSecurityEventRowRenderer({ actionName }); - - const wrapper = mount( - - {userLogonEventRowRenderer.isInstance(userLogonEvent) && - userLogonEventRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: userLogonEvent, - timelineId: 'test', - })} - - ); - - expect(wrapper.text()).toEqual( - 'SYSTEM\\NT AUTHORITY@HD-v1s-d2118419successfully logged inusing logon type5 - Service(target logon ID0x3e7)viaC:\\Windows\\System32\\services.exe(432)as requested by subjectWIN-Q3DOP1UKA81$(subject logon ID0x3e7)4624' - ); - }); - - test('it renders an Endgame admin_logon event', () => { - const actionName = 'admin_logon'; - const adminLogonEvent = { - ...mockEndgameAdminLogon, - }; - - const adminLogonEventRowRenderer = createSecurityEventRowRenderer({ actionName }); - - const wrapper = mount( - - {adminLogonEventRowRenderer.isInstance(adminLogonEvent) && - adminLogonEventRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: adminLogonEvent, - timelineId: 'test', - })} - - ); - - expect(wrapper.text()).toEqual( - 'With special privileges,SYSTEM\\NT AUTHORITY@HD-obe-8bf77f54successfully logged inviaC:\\Windows\\System32\\lsass.exe(964)as requested by subjectSYSTEM\\NT AUTHORITY4672' - ); - }); - - test('it renders an Endgame explicit_user_logon event', () => { - const actionName = 'explicit_user_logon'; - const explicitUserLogonEvent = { - ...mockEndgameExplicitUserLogon, - }; - - const explicitUserLogonEventRowRenderer = createSecurityEventRowRenderer({ actionName }); - - const wrapper = mount( - - {explicitUserLogonEventRowRenderer.isInstance(explicitUserLogonEvent) && - explicitUserLogonEventRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: explicitUserLogonEvent, - timelineId: 'test', - })} - - ); - - expect(wrapper.text()).toEqual( - 'A login was attempted using explicit credentialsArun\\Anvi-AcertoHD-55b-3ec87f66viaC:\\Windows\\System32\\svchost.exe(1736)as requested by subjectANVI-ACER$\\WORKGROUP(subject logon ID0x3e7)4648' - ); - }); - - test('it renders an Endgame user_logoff event', () => { - const actionName = 'user_logoff'; - const userLogoffEvent = { - ...mockEndgameUserLogoff, - }; - - const userLogoffEventRowRenderer = createSecurityEventRowRenderer({ actionName }); - - const wrapper = mount( - - {userLogoffEventRowRenderer.isInstance(userLogoffEvent) && - userLogoffEventRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: userLogoffEvent, - timelineId: 'test', - })} - - ); - - expect(wrapper.text()).toEqual( - 'Arun\\Anvi-Acer@HD-55b-3ec87f66logged offusing logon type2 - Interactive(target logon ID0x16db41e)viaC:\\Windows\\System32\\lsass.exe(964)4634' - ); - }); - - test('it does NOT render an event if the action name does not match', () => { - const actionName = 'does_not_match'; - const userLogonEvent = { - ...mockEndgameUserLogon, - }; - - const userLogonEventRowRenderer = createSecurityEventRowRenderer({ actionName }); - - const wrapper = mount( - - {userLogonEventRowRenderer.isInstance(userLogonEvent) && - userLogonEventRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: userLogonEvent, - timelineId: 'test', - })} - - ); - - expect(wrapper.text()).toEqual(''); - }); - }); - - describe('#createDnsRowRenderer', () => { - test('it renders an Endgame DNS request_event', () => { - const requestEvent = { - ...mockEndgameDnsRequest, - }; - - const dnsRowRenderer = createDnsRowRenderer(); - - const wrapper = mount( - - {dnsRowRenderer.isInstance(requestEvent) && - dnsRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: requestEvent, - timelineId: 'test', - })} - - ); - - expect(wrapper.text()).toEqual( - 'SYSTEM\\NT AUTHORITY@HD-obe-8bf77f54asked forupdate.googleapis.comwith question typeA, which resolved to10.100.197.67viaGoogleUpdate.exe(443192)3008dns' - ); - }); - - test('it renders a non-Endgame DNS event', () => { - const dnsEvent = { - ...mockDnsEvent, - }; - - const dnsRowRenderer = createDnsRowRenderer(); - - const wrapper = mount( - - {dnsRowRenderer.isInstance(dnsEvent) && - dnsRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: dnsEvent, - timelineId: 'test', - })} - - ); - - expect(wrapper.text()).toEqual( - 'iot.example.comasked forlookup.example.comwith question typeA, which resolved to10.1.2.3(response code:NOERROR)viaan unknown process6.937500msOct 8, 2019 @ 10:05:23.241Oct 8, 2019 @ 10:05:23.248outbounddns177Budp1:network-community_idSource10.9.9.9:58732(22.60%)40B(77.40%)137BDestination10.1.1.1:53OceaniaAustralia🇦🇺AU' - ); - }); - - test('it does NOT render an event if dns.question.type is not provided', () => { - const requestEvent = { - ...mockEndgameDnsRequest, - dns: { - ...mockDnsEvent.dns, - question: { - name: ['lookup.example.com'], - }, - }, - }; - - const dnsRowRenderer = createDnsRowRenderer(); - - const wrapper = mount( - - {dnsRowRenderer.isInstance(requestEvent) && - dnsRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: requestEvent, - timelineId: 'test', - })} - - ); - - expect(wrapper.text()).toEqual(''); - }); - - test('it does NOT render an event if dns.question.name is not provided', () => { - const requestEvent = { - ...mockEndgameDnsRequest, - dns: { - ...mockDnsEvent.dns, - question: { - type: ['A'], - }, - }, - }; - - const dnsRowRenderer = createDnsRowRenderer(); - - const wrapper = mount( - - {dnsRowRenderer.isInstance(requestEvent) && - dnsRowRenderer.renderRow({ - browserFields: mockBrowserFields, - data: requestEvent, - timelineId: 'test', - })} - - ); - - expect(wrapper.text()).toEqual(''); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/timeline/body/sort/index.ts b/x-pack/plugins/siem/public/components/timeline/body/sort/index.ts deleted file mode 100644 index 4a55ba8e1e8eea..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/body/sort/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Direction } from '../../../../graphql/types'; -import { ColumnId } from '../column_id'; - -/** Specifies a column's sort direction */ -export type SortDirection = 'none' | Direction; - -/** Specifies which column the timeline is sorted on */ -export interface Sort { - columnId: ColumnId; - sortDirection: SortDirection; -} diff --git a/x-pack/plugins/siem/public/components/timeline/data_providers/index.tsx b/x-pack/plugins/siem/public/components/timeline/data_providers/index.tsx deleted file mode 100644 index caead394db051b..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/data_providers/index.tsx +++ /dev/null @@ -1,135 +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 { rgba } from 'polished'; -import React from 'react'; -import styled from 'styled-components'; - -import { BrowserFields } from '../../../containers/source'; -import { DroppableWrapper } from '../../drag_and_drop/droppable_wrapper'; -import { - droppableTimelineProvidersPrefix, - IS_DRAGGING_CLASS_NAME, -} from '../../drag_and_drop/helpers'; -import { - OnDataProviderEdited, - OnDataProviderRemoved, - OnToggleDataProviderEnabled, - OnToggleDataProviderExcluded, -} from '../events'; -import { TimelineContext } from '../timeline_context'; - -import { DataProvider } from './data_provider'; -import { Empty } from './empty'; -import { Providers } from './providers'; - -interface Props { - browserFields: BrowserFields; - id: string; - dataProviders: DataProvider[]; - onDataProviderEdited: OnDataProviderEdited; - onDataProviderRemoved: OnDataProviderRemoved; - onToggleDataProviderEnabled: OnToggleDataProviderEnabled; - onToggleDataProviderExcluded: OnToggleDataProviderExcluded; - show: boolean; -} - -const DropTargetDataProvidersContainer = styled.div` - padding: 2px 0 4px 0; - - .${IS_DRAGGING_CLASS_NAME} & .drop-target-data-providers { - background: ${({ theme }) => rgba(theme.eui.euiColorSuccess, 0.1)}; - border: 0.2rem dashed ${({ theme }) => theme.eui.euiColorSuccess}; - - & .euiTextColor--subdued { - color: ${({ theme }) => theme.eui.euiColorSuccess}; - } - - & .euiFormHelpText { - color: ${({ theme }) => theme.eui.euiColorSuccess}; - } - } -`; - -const DropTargetDataProviders = styled.div` - position: relative; - border: 0.2rem dashed ${props => props.theme.eui.euiColorMediumShade}; - border-radius: 5px; - margin: 5px 0 5px 0; - min-height: 100px; - overflow-y: auto; - background-color: ${props => props.theme.eui.euiFormBackgroundColor}; -`; - -DropTargetDataProviders.displayName = 'DropTargetDataProviders'; - -const getDroppableId = (id: string): string => `${droppableTimelineProvidersPrefix}${id}`; - -/** - * Renders the data providers section of the timeline. - * - * The data providers section is a drop target where users - * can drag-and drop new data providers into the timeline. - * - * It renders an interactive card representation of the - * data providers. It also provides uniform - * UI controls for the following actions: - * 1) removing a data provider - * 2) temporarily disabling a data provider - * 3) applying boolean negation to the data provider - * - * Given an empty collection of DataProvider[], it prompts - * the user to drop anything with a facet count into - * the data pro section. - */ -export const DataProviders = React.memo( - ({ - browserFields, - id, - dataProviders, - onDataProviderEdited, - onDataProviderRemoved, - onToggleDataProviderEnabled, - onToggleDataProviderExcluded, - show, - }) => { - return ( - - - - {({ isLoading }) => ( - <> - {dataProviders != null && dataProviders.length ? ( - - ) : ( - - - - )} - - )} - - - - ); - } -); - -DataProviders.displayName = 'DataProviders'; diff --git a/x-pack/plugins/siem/public/components/timeline/expandable_event/index.tsx b/x-pack/plugins/siem/public/components/timeline/expandable_event/index.tsx deleted file mode 100644 index 218d4db9901cbf..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/expandable_event/index.tsx +++ /dev/null @@ -1,74 +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 styled from 'styled-components'; - -import { BrowserFields } from '../../../containers/source'; -import { ColumnHeaderOptions } from '../../../store/timeline/model'; -import { DetailItem } from '../../../graphql/types'; -import { StatefulEventDetails } from '../../event_details/stateful_event_details'; -import { LazyAccordion } from '../../lazy_accordion'; -import { OnUpdateColumns } from '../events'; - -const ExpandableDetails = styled.div<{ hideExpandButton: boolean }>` - ${({ hideExpandButton }) => - hideExpandButton - ? ` - .euiAccordion__button { - display: none; - } - ` - : ''}; -`; - -ExpandableDetails.displayName = 'ExpandableDetails'; - -interface Props { - browserFields: BrowserFields; - columnHeaders: ColumnHeaderOptions[]; - id: string; - event: DetailItem[]; - forceExpand?: boolean; - hideExpandButton?: boolean; - onUpdateColumns: OnUpdateColumns; - timelineId: string; - toggleColumn: (column: ColumnHeaderOptions) => void; -} - -export const ExpandableEvent = React.memo( - ({ - browserFields, - columnHeaders, - event, - forceExpand = false, - id, - timelineId, - toggleColumn, - onUpdateColumns, - }) => ( - - ( - - )} - forceExpand={forceExpand} - paddingSize="none" - /> - - ) -); - -ExpandableEvent.displayName = 'ExpandableEvent'; diff --git a/x-pack/plugins/siem/public/components/timeline/footer/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/footer/index.test.tsx deleted file mode 100644 index d54a4cee83e527..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/footer/index.test.tsx +++ /dev/null @@ -1,287 +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 { mount, shallow } from 'enzyme'; -import { getOr } from 'lodash/fp'; -import React from 'react'; - -import { TestProviders } from '../../../mock/test_providers'; - -import { FooterComponent, PagingControlComponent } from './index'; -import { mockData } from './mock'; - -describe('Footer Timeline Component', () => { - const loadMore = jest.fn(); - const onChangeItemsPerPage = jest.fn(); - const getUpdatedAt = () => 1546878704036; - - describe('rendering', () => { - test('it renders the default timeline footer', () => { - const wrapper = shallow( - - ); - - expect(wrapper).toMatchSnapshot(); - }); - - test('it renders the loading panel at the beginning ', () => { - const wrapper = mount( - - ); - - expect(wrapper.find('[data-test-subj="LoadingPanelTimeline"]').exists()).toBeTruthy(); - }); - - test('it renders the loadMore button if need to fetch more', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="TimelineMoreButton"]').exists()).toBeTruthy(); - }); - - test('it renders the Loading... in the more load button when fetching new data', () => { - const wrapper = shallow( - - ); - - const loadButton = wrapper - .find('[data-test-subj="TimelineMoreButton"]') - .dive() - .text(); - expect(wrapper.find('[data-test-subj="LoadingPanelTimeline"]').exists()).toBeFalsy(); - expect(loadButton).toContain('Loading...'); - }); - - test('it renders the Load More in the more load button when fetching new data', () => { - const wrapper = shallow( - - ); - - const loadButton = wrapper - .find('[data-test-subj="TimelineMoreButton"]') - .dive() - .text(); - expect(loadButton).toContain('Load more'); - }); - - test('it does NOT render the loadMore button because there is nothing else to fetch', () => { - const wrapper = mount( - - ); - - expect(wrapper.find('[data-test-subj="TimelineMoreButton"]').exists()).toBeFalsy(); - }); - - test('it render popover to select new itemsPerPage in timeline', () => { - const wrapper = mount( - - - - ); - - wrapper - .find('[data-test-subj="timelineSizeRowPopover"] button') - .first() - .simulate('click'); - expect(wrapper.find('[data-test-subj="timelinePickSizeRow"]').exists()).toBeTruthy(); - }); - }); - - describe('Events', () => { - test('should call loadmore when clicking on the button load more', () => { - const wrapper = mount( - - - - ); - - wrapper - .find('[data-test-subj="TimelineMoreButton"]') - .first() - .simulate('click'); - - expect(loadMore).toBeCalled(); - }); - - test('Should call onChangeItemsPerPage when you pick a new limit', () => { - const wrapper = mount( - - - - ); - - wrapper - .find('[data-test-subj="timelineSizeRowPopover"] button') - .first() - .simulate('click'); - wrapper.update(); - wrapper - .find('[data-test-subj="timelinePickSizeRow"] button') - .first() - .simulate('click'); - expect(onChangeItemsPerPage).toBeCalled(); - }); - - test('it does render the auto-refresh message instead of load more button when stream live is on', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="paging-control"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="is-live-on-message"]').exists()).toBeTruthy(); - }); - - test('it does render the load more button when stream live is off', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="paging-control"]').exists()).toBeTruthy(); - expect(wrapper.find('[data-test-subj="is-live-on-message"]').exists()).toBeFalsy(); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/timeline/footer/index.tsx b/x-pack/plugins/siem/public/components/timeline/footer/index.tsx deleted file mode 100644 index 7a025e96e57f29..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/footer/index.tsx +++ /dev/null @@ -1,368 +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 { - EuiBadge, - EuiButton, - EuiButtonEmpty, - EuiContextMenuItem, - EuiContextMenuPanel, - EuiFlexGroup, - EuiFlexItem, - EuiIconTip, - EuiPopover, - EuiText, - EuiToolTip, - EuiPopoverProps, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { FC, useCallback, useEffect, useState, useMemo } from 'react'; -import styled from 'styled-components'; - -import { LoadingPanel } from '../../loading'; -import { OnChangeItemsPerPage, OnLoadMore } from '../events'; - -import { LastUpdatedAt } from './last_updated'; -import * as i18n from './translations'; -import { useTimelineTypeContext } from '../timeline_context'; -import { useEventDetailsWidthContext } from '../../events_viewer/event_details_width_context'; - -export const isCompactFooter = (width: number): boolean => width < 600; - -interface FixedWidthLastUpdatedContainerProps { - updatedAt: number; -} - -const FixedWidthLastUpdatedContainer = React.memo( - ({ updatedAt }) => { - const width = useEventDetailsWidthContext(); - const compact = useMemo(() => isCompactFooter(width), [width]); - - return ( - - - - ); - } -); - -FixedWidthLastUpdatedContainer.displayName = 'FixedWidthLastUpdatedContainer'; - -const FixedWidthLastUpdated = styled.div<{ compact?: boolean }>` - width: ${({ compact }) => (!compact ? 200 : 25)}px; - overflow: hidden; - text-align: end; -`; - -FixedWidthLastUpdated.displayName = 'FixedWidthLastUpdated'; - -interface HeightProp { - height: number; -} - -const FooterContainer = styled(EuiFlexGroup).attrs(({ height }) => ({ - style: { - height: `${height}px`, - }, -}))` - flex: 0; -`; - -FooterContainer.displayName = 'FooterContainer'; - -const FooterFlexGroup = styled(EuiFlexGroup)` - height: 35px; - width: 100%; -`; - -FooterFlexGroup.displayName = 'FooterFlexGroup'; - -const LoadingPanelContainer = styled.div` - padding-top: 3px; -`; - -LoadingPanelContainer.displayName = 'LoadingPanelContainer'; - -const PopoverRowItems = styled((EuiPopover as unknown) as FC)< - EuiPopoverProps & { - className?: string; - id?: string; - } ->` - .euiButtonEmpty__content { - padding: 0px 0px; - } -`; - -PopoverRowItems.displayName = 'PopoverRowItems'; - -export const ServerSideEventCount = styled.div` - margin: 0 5px 0 5px; -`; - -ServerSideEventCount.displayName = 'ServerSideEventCount'; - -/** The height of the footer, exported for use in height calculations */ -export const footerHeight = 40; // px - -/** Displays the server-side count of events */ -export const EventsCountComponent = ({ - closePopover, - isOpen, - items, - itemsCount, - onClick, - serverSideEventCount, -}: { - closePopover: () => void; - isOpen: boolean; - items: React.ReactElement[]; - itemsCount: number; - onClick: () => void; - serverSideEventCount: number; -}) => { - const timelineTypeContext = useTimelineTypeContext(); - return ( -
- - - {itemsCount} - - - {` ${i18n.OF} `} - - } - isOpen={isOpen} - closePopover={closePopover} - panelPaddingSize="none" - > - - - - - - {serverSideEventCount} - {' '} - {timelineTypeContext.documentType ?? i18n.EVENTS} - - -
- ); -}; - -EventsCountComponent.displayName = 'EventsCountComponent'; - -export const EventsCount = React.memo(EventsCountComponent); - -EventsCount.displayName = 'EventsCount'; - -export const PagingControlComponent = ({ - hasNextPage, - isLoading, - loadMore, -}: { - hasNextPage: boolean; - isLoading: boolean; - loadMore: () => void; -}) => ( - <> - {hasNextPage && ( - - {isLoading ? `${i18n.LOADING}...` : i18n.LOAD_MORE} - - )} - -); - -PagingControlComponent.displayName = 'PagingControlComponent'; - -export const PagingControl = React.memo(PagingControlComponent); - -PagingControl.displayName = 'PagingControl'; - -interface FooterProps { - getUpdatedAt: () => number; - hasNextPage: boolean; - height: number; - isLive: boolean; - isLoading: boolean; - itemsCount: number; - itemsPerPage: number; - itemsPerPageOptions: number[]; - nextCursor: string; - onChangeItemsPerPage: OnChangeItemsPerPage; - onLoadMore: OnLoadMore; - serverSideEventCount: number; - tieBreaker: string; -} - -/** Renders a loading indicator and paging controls */ -export const FooterComponent = ({ - getUpdatedAt, - hasNextPage, - height, - isLive, - isLoading, - itemsCount, - itemsPerPage, - itemsPerPageOptions, - nextCursor, - onChangeItemsPerPage, - onLoadMore, - serverSideEventCount, - tieBreaker, -}: FooterProps) => { - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const [paginationLoading, setPaginationLoading] = useState(false); - const [updatedAt, setUpdatedAt] = useState(null); - const timelineTypeContext = useTimelineTypeContext(); - - const loadMore = useCallback(() => { - setPaginationLoading(true); - onLoadMore(nextCursor, tieBreaker); - }, [nextCursor, tieBreaker, onLoadMore, setPaginationLoading]); - - const onButtonClick = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [ - isPopoverOpen, - setIsPopoverOpen, - ]); - const closePopover = useCallback(() => setIsPopoverOpen(false), [setIsPopoverOpen]); - - useEffect(() => { - if (paginationLoading && !isLoading) { - setPaginationLoading(false); - setUpdatedAt(getUpdatedAt()); - } - - if (updatedAt === null || !isLoading) { - setUpdatedAt(getUpdatedAt()); - } - }, [isLoading]); - - if (isLoading && !paginationLoading) { - return ( - - - - ); - } - - const rowItems = - itemsPerPageOptions && - itemsPerPageOptions.map(item => ( - { - closePopover(); - onChangeItemsPerPage(item); - }} - > - {`${item} ${i18n.ROWS}`} - - )); - - return ( - - - - - - - - - - {isLive ? ( - - - {i18n.AUTO_REFRESH_ACTIVE}{' '} - - } - type="iInCircle" - /> - - - ) : ( - - )} - - - - - - - - ); -}; - -FooterComponent.displayName = 'FooterComponent'; - -export const Footer = React.memo(FooterComponent); - -Footer.displayName = 'Footer'; diff --git a/x-pack/plugins/siem/public/components/timeline/footer/mock.ts b/x-pack/plugins/siem/public/components/timeline/footer/mock.ts deleted file mode 100644 index f6aaf9475f2c4e..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/footer/mock.ts +++ /dev/null @@ -1,86 +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 { EventsTimelineData } from '../../../graphql/types'; - -export const mockData: { Events: EventsTimelineData } = { - Events: { - totalCount: 15546, - pageInfo: { - hasNextPage: true, - endCursor: { - value: '1546878704036', - tiebreaker: '10624', - }, - }, - edges: [ - { - cursor: { - value: '1546878704036', - tiebreaker: '10656', - }, - node: { - _id: 'Fo8nKWgBiyhPd5Zo3cib', - timestamp: '2019-01-07T16:31:44.036Z', - _index: 'auditbeat-7.0.0-2019.01.07', - destination: { - ip: ['24.168.54.169'], - port: [62123], - }, - event: { - category: null, - id: null, - module: ['system'], - severity: null, - type: null, - }, - geo: null, - host: { - name: ['siem-general'], - ip: null, - }, - source: { - ip: ['10.142.0.6'], - port: [9200], - }, - suricata: null, - }, - }, - { - cursor: { - value: '1546878704036', - tiebreaker: '10624', - }, - node: { - _id: 'F48nKWgBiyhPd5Zo3cib', - timestamp: '2019-01-07T16:31:44.036Z', - _index: 'auditbeat-7.0.0-2019.01.07', - destination: { - ip: ['24.168.54.169'], - port: [62145], - }, - event: { - category: null, - id: null, - module: ['system'], - severity: null, - type: null, - }, - geo: null, - host: { - name: ['siem-general'], - ip: null, - }, - source: { - ip: ['10.142.0.6'], - port: [9200], - }, - suricata: null, - }, - }, - ], - }, -}; diff --git a/x-pack/plugins/siem/public/components/timeline/header/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/header/index.test.tsx deleted file mode 100644 index 7da76df4977684..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/header/index.test.tsx +++ /dev/null @@ -1,91 +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 { shallow } from 'enzyme'; -import React from 'react'; - -import { mockIndexPattern } from '../../../mock'; -import { createKibanaCoreStartMock } from '../../../mock/kibana_core'; -import { TestProviders } from '../../../mock/test_providers'; -import { FilterManager } from '../../../../../../../src/plugins/data/public'; -import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; -import { useMountAppended } from '../../../utils/use_mount_appended'; - -import { TimelineHeader } from '.'; - -const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; - -jest.mock('../../../lib/kibana'); - -describe('Header', () => { - const indexPattern = mockIndexPattern; - const mount = useMountAppended(); - - describe('rendering', () => { - test('renders correctly against snapshot', () => { - const wrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); - }); - - test('it renders the data providers', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="dataProviders"]').exists()).toEqual(true); - }); - - test('it renders the unauthorized call out providers', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="timelineCallOutUnauthorized"]').exists()).toEqual(true); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/timeline/header/index.tsx b/x-pack/plugins/siem/public/components/timeline/header/index.tsx deleted file mode 100644 index 58e6b6e8372495..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/header/index.tsx +++ /dev/null @@ -1,98 +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 { EuiCallOut } from '@elastic/eui'; -import React from 'react'; -import { FilterManager, IIndexPattern } from 'src/plugins/data/public'; -import deepEqual from 'fast-deep-equal'; - -import { DataProviders } from '../data_providers'; -import { DataProvider } from '../data_providers/data_provider'; -import { - OnDataProviderEdited, - OnDataProviderRemoved, - OnToggleDataProviderEnabled, - OnToggleDataProviderExcluded, -} from '../events'; -import { StatefulSearchOrFilter } from '../search_or_filter'; -import { BrowserFields } from '../../../containers/source'; - -import * as i18n from './translations'; - -interface Props { - browserFields: BrowserFields; - dataProviders: DataProvider[]; - filterManager: FilterManager; - id: string; - indexPattern: IIndexPattern; - onDataProviderEdited: OnDataProviderEdited; - onDataProviderRemoved: OnDataProviderRemoved; - onToggleDataProviderEnabled: OnToggleDataProviderEnabled; - onToggleDataProviderExcluded: OnToggleDataProviderExcluded; - show: boolean; - showCallOutUnauthorizedMsg: boolean; -} - -const TimelineHeaderComponent: React.FC = ({ - browserFields, - id, - indexPattern, - dataProviders, - filterManager, - onDataProviderEdited, - onDataProviderRemoved, - onToggleDataProviderEnabled, - onToggleDataProviderExcluded, - show, - showCallOutUnauthorizedMsg, -}) => ( - <> - {showCallOutUnauthorizedMsg && ( - - )} - {show && ( - - )} - - - -); - -export const TimelineHeader = React.memo( - TimelineHeaderComponent, - (prevProps, nextProps) => - deepEqual(prevProps.browserFields, nextProps.browserFields) && - prevProps.id === nextProps.id && - deepEqual(prevProps.indexPattern, nextProps.indexPattern) && - deepEqual(prevProps.dataProviders, nextProps.dataProviders) && - prevProps.filterManager === nextProps.filterManager && - prevProps.onDataProviderEdited === nextProps.onDataProviderEdited && - prevProps.onDataProviderRemoved === nextProps.onDataProviderRemoved && - prevProps.onToggleDataProviderEnabled === nextProps.onToggleDataProviderEnabled && - prevProps.onToggleDataProviderExcluded === nextProps.onToggleDataProviderExcluded && - prevProps.show === nextProps.show && - prevProps.showCallOutUnauthorizedMsg === nextProps.showCallOutUnauthorizedMsg -); diff --git a/x-pack/plugins/siem/public/components/timeline/helpers.test.tsx b/x-pack/plugins/siem/public/components/timeline/helpers.test.tsx deleted file mode 100644 index fc5a8ae924f828..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/helpers.test.tsx +++ /dev/null @@ -1,398 +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 { cloneDeep } from 'lodash/fp'; -import { mockIndexPattern } from '../../mock'; - -import { mockDataProviders } from './data_providers/mock/mock_data_providers'; -import { buildGlobalQuery, combineQueries } from './helpers'; -import { mockBrowserFields } from '../../containers/source/mock'; -import { EsQueryConfig, Filter, esFilters } from '../../../../../../src/plugins/data/public'; - -const cleanUpKqlQuery = (str: string) => str.replace(/\n/g, '').replace(/\s\s+/g, ' '); -const startDate = new Date('2018-03-23T18:49:23.132Z').valueOf(); -const endDate = new Date('2018-03-24T03:33:52.253Z').valueOf(); - -describe('Build KQL Query', () => { - test('Build KQL query with one data provider', () => { - const dataProviders = mockDataProviders.slice(0, 1); - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1"'); - }); - - test('Build KQL query with one data provider as timestamp (string input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.field = '@timestamp'; - dataProviders[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('@timestamp: 1521848183232'); - }); - - test('Buld KQL query with one data provider as timestamp (numeric input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.field = '@timestamp'; - dataProviders[0].queryMatch.value = 1521848183232; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('@timestamp: 1521848183232'); - }); - - test('Build KQL query with one data provider as date type (string input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.field = 'event.end'; - dataProviders[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('event.end: 1521848183232'); - }); - - test('Buld KQL query with one data provider as date type (numeric input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.field = 'event.end'; - dataProviders[0].queryMatch.value = 1521848183232; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('event.end: 1521848183232'); - }); - - test('Build KQL query with two data provider', () => { - const dataProviders = mockDataProviders.slice(0, 2); - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('(name : "Provider 1") or (name : "Provider 2" )'); - }); - - test('Build KQL query with one data provider and one and', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].and = mockDataProviders.slice(1, 2); - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and name : "Provider 2"'); - }); - - test('Build KQL query with one data provider and one and as timestamp (string input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); - dataProviders[0].and[0].queryMatch.field = '@timestamp'; - dataProviders[0].and[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and @timestamp: 1521848183232'); - }); - - test('Build KQL query with one data provider and one and as timestamp (numeric input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); - dataProviders[0].and[0].queryMatch.field = '@timestamp'; - dataProviders[0].and[0].queryMatch.value = 1521848183232; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and @timestamp: 1521848183232'); - }); - - test('Build KQL query with one data provider and one and as date type (string input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); - dataProviders[0].and[0].queryMatch.field = 'event.end'; - dataProviders[0].and[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and event.end: 1521848183232'); - }); - - test('Build KQL query with one data provider and one and as date type (numeric input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); - dataProviders[0].and[0].queryMatch.field = 'event.end'; - dataProviders[0].and[0].queryMatch.value = 1521848183232; - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and event.end: 1521848183232'); - }); - - test('Build KQL query with two data provider and multiple and', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); - dataProviders[0].and = mockDataProviders.slice(2, 4); - dataProviders[1].and = mockDataProviders.slice(4, 5); - const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); - expect(cleanUpKqlQuery(kqlQuery)).toEqual( - '(name : "Provider 1" and name : "Provider 3" and name : "Provider 4") or (name : "Provider 2" and name : "Provider 5")' - ); - }); -}); - -describe('Combined Queries', () => { - const config: EsQueryConfig = { - allowLeadingWildcards: true, - queryStringOptions: {}, - ignoreFilterIfFieldNotInIndex: true, - dateFormatTZ: 'America/New_York', - }; - test('No Data Provider & No kqlQuery & and isEventViewer is false', () => { - expect( - combineQueries({ - config, - dataProviders: [], - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - start: startDate, - end: endDate, - }) - ).toBeNull(); - }); - - test('No Data Provider & No kqlQuery & isEventViewer is true', () => { - const isEventViewer = true; - expect( - combineQueries({ - config, - dataProviders: [], - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - start: startDate, - end: endDate, - isEventViewer, - }) - ).toEqual({ - filterQuery: - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}', - }); - }); - - test('No Data Provider & No kqlQuery & with Filters', () => { - const isEventViewer = true; - expect( - combineQueries({ - config, - dataProviders: [], - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [ - { - $state: { store: esFilters.FilterStateStore.APP_STATE }, - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { query: 'file' }, - type: 'phrase', - }, - query: { match_phrase: { 'event.category': 'file' } }, - }, - { - $state: { store: esFilters.FilterStateStore.APP_STATE }, - meta: { - alias: null, - disabled: false, - key: 'host.name', - negate: false, - type: 'exists', - value: 'exists', - }, - exists: { field: 'host.name' }, - } as Filter, - ], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - start: startDate, - end: endDate, - isEventViewer, - }) - ).toEqual({ - filterQuery: - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}},{"exists":{"field":"host.name"}}],"should":[],"must_not":[]}}', - }); - }); - - test('Only Data Provider', () => { - const dataProviders = mockDataProviders.slice(0, 1); - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - start: startDate, - end: endDate, - })!; - expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' - ); - }); - - test('Only Data Provider with timestamp (string input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.field = '@timestamp'; - dataProviders[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - start: startDate, - end: endDate, - })!; - expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521848183232,"lte":1521848183232}}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' - ); - }); - - test('Only Data Provider with timestamp (numeric input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.field = '@timestamp'; - dataProviders[0].queryMatch.value = 1521848183232; - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - start: startDate, - end: endDate, - })!; - expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521848183232,"lte":1521848183232}}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' - ); - }); - - test('Only Data Provider with a date type (string input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.field = 'event.end'; - dataProviders[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - start: startDate, - end: endDate, - })!; - expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match":{"event.end":1521848183232}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' - ); - }); - - test('Only Data Provider with date type (numeric input)', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); - dataProviders[0].queryMatch.field = 'event.end'; - dataProviders[0].queryMatch.value = 1521848183232; - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: '', language: 'kuery' }, - kqlMode: 'search', - start: startDate, - end: endDate, - })!; - expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match":{"event.end":1521848183232}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' - ); - }); - - test('Only KQL search/filter query', () => { - const { filterQuery } = combineQueries({ - config, - dataProviders: [], - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, - kqlMode: 'search', - start: startDate, - end: endDate, - })!; - expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' - ); - }); - - test('Data Provider & KQL search query', () => { - const dataProviders = mockDataProviders.slice(0, 1); - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, - kqlMode: 'search', - start: startDate, - end: endDate, - })!; - expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' - ); - }); - - test('Data Provider & KQL filter query', () => { - const dataProviders = mockDataProviders.slice(0, 1); - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, - kqlMode: 'filter', - start: startDate, - end: endDate, - })!; - expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}]}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' - ); - }); - - test('Data Provider & KQL search query multiple', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); - dataProviders[0].and = mockDataProviders.slice(2, 4); - dataProviders[1].and = mockDataProviders.slice(4, 5); - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, - kqlMode: 'search', - start: startDate, - end: endDate, - })!; - expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"bool":{"should":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 3"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 4"}}],"minimum_should_match":1}}]}}]}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 2"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 5"}}],"minimum_should_match":1}}]}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' - ); - }); - - test('Data Provider & KQL filter query multiple', () => { - const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); - dataProviders[0].and = mockDataProviders.slice(2, 4); - dataProviders[1].and = mockDataProviders.slice(4, 5); - const { filterQuery } = combineQueries({ - config, - dataProviders, - indexPattern: mockIndexPattern, - browserFields: mockBrowserFields, - filters: [], - kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, - kqlMode: 'filter', - start: startDate, - end: endDate, - })!; - expect(filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"filter":[{"bool":{"should":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 3"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 4"}}],"minimum_should_match":1}}]}}]}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 2"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 5"}}],"minimum_should_match":1}}]}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}]}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' - ); - }); -}); diff --git a/x-pack/plugins/siem/public/components/timeline/helpers.tsx b/x-pack/plugins/siem/public/components/timeline/helpers.tsx deleted file mode 100644 index 53ab7d81cadc26..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/helpers.tsx +++ /dev/null @@ -1,160 +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 { isEmpty, isNumber, get } from 'lodash/fp'; -import memoizeOne from 'memoize-one'; - -import { escapeQueryValue, convertToBuildEsQuery } from '../../lib/keury'; - -import { DataProvider, DataProvidersAnd, EXISTS_OPERATOR } from './data_providers/data_provider'; -import { BrowserFields } from '../../containers/source'; -import { - IIndexPattern, - Query, - EsQueryConfig, - Filter, -} from '../../../../../../src/plugins/data/public'; - -const convertDateFieldToQuery = (field: string, value: string | number) => - `${field}: ${isNumber(value) ? value : new Date(value).valueOf()}`; - -const getBaseFields = memoizeOne((browserFields: BrowserFields): string[] => { - const baseFields = get('base', browserFields); - if (baseFields != null && baseFields.fields != null) { - return Object.keys(baseFields.fields); - } - return []; -}); - -const getBrowserFieldPath = (field: string, browserFields: BrowserFields) => { - const splitFields = field.split('.'); - const baseFields = getBaseFields(browserFields); - if (baseFields.includes(field)) { - return ['base', 'fields', field]; - } - return [splitFields[0], 'fields', field]; -}; - -const checkIfFieldTypeIsDate = (field: string, browserFields: BrowserFields) => { - const pathBrowserField = getBrowserFieldPath(field, browserFields); - const browserField = get(pathBrowserField, browserFields); - if (browserField != null && browserField.type === 'date') { - return true; - } - return false; -}; - -const buildQueryMatch = ( - dataProvider: DataProvider | DataProvidersAnd, - browserFields: BrowserFields -) => - `${dataProvider.excluded ? 'NOT ' : ''}${ - dataProvider.queryMatch.operator !== EXISTS_OPERATOR - ? checkIfFieldTypeIsDate(dataProvider.queryMatch.field, browserFields) - ? convertDateFieldToQuery(dataProvider.queryMatch.field, dataProvider.queryMatch.value) - : `${dataProvider.queryMatch.field} : ${ - isNumber(dataProvider.queryMatch.value) - ? dataProvider.queryMatch.value - : escapeQueryValue(dataProvider.queryMatch.value) - }` - : `${dataProvider.queryMatch.field} ${EXISTS_OPERATOR}` - }`.trim(); - -const buildQueryForAndProvider = ( - dataAndProviders: DataProvidersAnd[], - browserFields: BrowserFields -) => - dataAndProviders - .reduce((andQuery, andDataProvider) => { - const prepend = (q: string) => `${q !== '' ? `${q} and ` : ''}`; - return andDataProvider.enabled - ? `${prepend(andQuery)} ${buildQueryMatch(andDataProvider, browserFields)}` - : andQuery; - }, '') - .trim(); - -export const buildGlobalQuery = (dataProviders: DataProvider[], browserFields: BrowserFields) => - dataProviders - .reduce((query, dataProvider: DataProvider, i) => { - const prepend = (q: string) => `${q !== '' ? `(${q}) or ` : ''}`; - const openParen = i > 0 ? '(' : ''; - const closeParen = i > 0 ? ')' : ''; - return dataProvider.enabled - ? `${prepend(query)}${openParen}${buildQueryMatch(dataProvider, browserFields)} - ${ - dataProvider.and.length > 0 - ? ` and ${buildQueryForAndProvider(dataProvider.and, browserFields)}` - : '' - }${closeParen}`.trim() - : query; - }, '') - .trim(); - -export const combineQueries = ({ - config, - dataProviders, - indexPattern, - browserFields, - filters = [], - kqlQuery, - kqlMode, - start, - end, - isEventViewer, -}: { - config: EsQueryConfig; - dataProviders: DataProvider[]; - indexPattern: IIndexPattern; - browserFields: BrowserFields; - filters: Filter[]; - kqlQuery: Query; - kqlMode: string; - start: number; - end: number; - isEventViewer?: boolean; -}): { filterQuery: string } | null => { - const kuery: Query = { query: '', language: kqlQuery.language }; - if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && isEmpty(filters) && !isEventViewer) { - return null; - } else if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && isEventViewer) { - kuery.query = `@timestamp >= ${start} and @timestamp <= ${end}`; - return { - filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), - }; - } else if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && !isEmpty(filters)) { - kuery.query = `@timestamp >= ${start} and @timestamp <= ${end}`; - return { - filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), - }; - } else if (isEmpty(dataProviders) && !isEmpty(kqlQuery.query)) { - kuery.query = `(${kqlQuery.query}) and @timestamp >= ${start} and @timestamp <= ${end}`; - return { - filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), - }; - } else if (!isEmpty(dataProviders) && isEmpty(kqlQuery)) { - kuery.query = `(${buildGlobalQuery( - dataProviders, - browserFields - )}) and @timestamp >= ${start} and @timestamp <= ${end}`; - return { - filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), - }; - } - const operatorKqlQuery = kqlMode === 'filter' ? 'and' : 'or'; - const postpend = (q: string) => `${!isEmpty(q) ? ` ${operatorKqlQuery} (${q})` : ''}`; - kuery.query = `((${buildGlobalQuery(dataProviders, browserFields)})${postpend( - kqlQuery.query as string - )}) and @timestamp >= ${start} and @timestamp <= ${end}`; - return { - filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), - }; -}; - -/** - * The CSS class name of a "stateful event", which appears in both - * the `Timeline` and the `Events Viewer` widget - */ -export const STATEFUL_EVENT_CSS_CLASS_NAME = 'event-column-view'; diff --git a/x-pack/plugins/siem/public/components/timeline/index.tsx b/x-pack/plugins/siem/public/components/timeline/index.tsx deleted file mode 100644 index bebc6f9b654c58..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/index.tsx +++ /dev/null @@ -1,277 +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, { useEffect, useCallback, useMemo } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import deepEqual from 'fast-deep-equal'; - -import { WithSource } from '../../containers/source'; -import { useSignalIndex } from '../../containers/detection_engine/signals/use_signal_index'; -import { inputsModel, inputsSelectors, State, timelineSelectors } from '../../store'; -import { timelineActions } from '../../store/actions'; -import { ColumnHeaderOptions, TimelineModel } from '../../store/timeline/model'; -import { timelineDefaults } from '../../store/timeline/defaults'; -import { defaultHeaders } from './body/column_headers/default_headers'; -import { - OnChangeItemsPerPage, - OnDataProviderRemoved, - OnDataProviderEdited, - OnToggleDataProviderEnabled, - OnToggleDataProviderExcluded, -} from './events'; -import { Timeline } from './timeline'; - -export interface OwnProps { - id: string; - onClose: () => void; - usersViewing: string[]; -} - -type Props = OwnProps & PropsFromRedux; - -const StatefulTimelineComponent = React.memo( - ({ - columns, - createTimeline, - dataProviders, - eventType, - end, - filters, - id, - isLive, - itemsPerPage, - itemsPerPageOptions, - kqlMode, - kqlQueryExpression, - onClose, - onDataProviderEdited, - removeColumn, - removeProvider, - show, - showCallOutUnauthorizedMsg, - sort, - start, - updateDataProviderEnabled, - updateDataProviderExcluded, - updateItemsPerPage, - upsertColumn, - usersViewing, - }) => { - const { loading, signalIndexExists, signalIndexName } = useSignalIndex(); - - const indexToAdd = useMemo(() => { - if ( - eventType && - signalIndexExists && - signalIndexName != null && - ['signal', 'all'].includes(eventType) - ) { - return [signalIndexName]; - } - return []; - }, [eventType, signalIndexExists, signalIndexName]); - - const onDataProviderRemoved: OnDataProviderRemoved = useCallback( - (providerId: string, andProviderId?: string) => - removeProvider!({ id, providerId, andProviderId }), - [id] - ); - - const onToggleDataProviderEnabled: OnToggleDataProviderEnabled = useCallback( - ({ providerId, enabled, andProviderId }) => - updateDataProviderEnabled!({ - id, - enabled, - providerId, - andProviderId, - }), - [id] - ); - - const onToggleDataProviderExcluded: OnToggleDataProviderExcluded = useCallback( - ({ providerId, excluded, andProviderId }) => - updateDataProviderExcluded!({ - id, - excluded, - providerId, - andProviderId, - }), - [id] - ); - - const onDataProviderEditedLocal: OnDataProviderEdited = useCallback( - ({ andProviderId, excluded, field, operator, providerId, value }) => - onDataProviderEdited!({ - andProviderId, - excluded, - field, - id, - operator, - providerId, - value, - }), - [id] - ); - - const onChangeItemsPerPage: OnChangeItemsPerPage = useCallback( - itemsChangedPerPage => updateItemsPerPage!({ id, itemsPerPage: itemsChangedPerPage }), - [id] - ); - - const toggleColumn = useCallback( - (column: ColumnHeaderOptions) => { - const exists = columns.findIndex(c => c.id === column.id) !== -1; - - if (!exists && upsertColumn != null) { - upsertColumn({ - column, - id, - index: 1, - }); - } - - if (exists && removeColumn != null) { - removeColumn({ - columnId: column.id, - id, - }); - } - }, - [columns, id] - ); - - useEffect(() => { - if (createTimeline != null) { - createTimeline({ id, columns: defaultHeaders, show: false }); - } - }, []); - - return ( - - {({ indexPattern, browserFields }) => ( - - )} - - ); - }, - (prevProps, nextProps) => { - return ( - prevProps.eventType === nextProps.eventType && - prevProps.end === nextProps.end && - prevProps.id === nextProps.id && - prevProps.isLive === nextProps.isLive && - prevProps.itemsPerPage === nextProps.itemsPerPage && - prevProps.kqlMode === nextProps.kqlMode && - prevProps.kqlQueryExpression === nextProps.kqlQueryExpression && - prevProps.show === nextProps.show && - prevProps.showCallOutUnauthorizedMsg === nextProps.showCallOutUnauthorizedMsg && - prevProps.start === nextProps.start && - deepEqual(prevProps.columns, nextProps.columns) && - deepEqual(prevProps.dataProviders, nextProps.dataProviders) && - deepEqual(prevProps.filters, nextProps.filters) && - deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) && - deepEqual(prevProps.sort, nextProps.sort) && - deepEqual(prevProps.usersViewing, nextProps.usersViewing) - ); - } -); - -StatefulTimelineComponent.displayName = 'StatefulTimelineComponent'; - -const makeMapStateToProps = () => { - const getShowCallOutUnauthorizedMsg = timelineSelectors.getShowCallOutUnauthorizedMsg(); - const getTimeline = timelineSelectors.getTimelineByIdSelector(); - const getKqlQueryTimeline = timelineSelectors.getKqlFilterQuerySelector(); - const getInputsTimeline = inputsSelectors.getTimelineSelector(); - const mapStateToProps = (state: State, { id }: OwnProps) => { - const timeline: TimelineModel = getTimeline(state, id) ?? timelineDefaults; - const input: inputsModel.InputsRange = getInputsTimeline(state); - const { - columns, - dataProviders, - eventType, - filters, - itemsPerPage, - itemsPerPageOptions, - kqlMode, - show, - sort, - } = timeline; - const kqlQueryExpression = getKqlQueryTimeline(state, id)!; - - const timelineFilter = kqlMode === 'filter' ? filters || [] : []; - - return { - columns, - dataProviders, - eventType, - end: input.timerange.to, - filters: timelineFilter, - id, - isLive: input.policy.kind === 'interval', - itemsPerPage, - itemsPerPageOptions, - kqlMode, - kqlQueryExpression, - show, - showCallOutUnauthorizedMsg: getShowCallOutUnauthorizedMsg(state), - sort, - start: input.timerange.from, - }; - }; - return mapStateToProps; -}; - -const mapDispatchToProps = { - addProvider: timelineActions.addProvider, - createTimeline: timelineActions.createTimeline, - onDataProviderEdited: timelineActions.dataProviderEdited, - removeColumn: timelineActions.removeColumn, - removeProvider: timelineActions.removeProvider, - updateColumns: timelineActions.updateColumns, - updateDataProviderEnabled: timelineActions.updateDataProviderEnabled, - updateDataProviderExcluded: timelineActions.updateDataProviderExcluded, - updateDataProviderKqlQuery: timelineActions.updateDataProviderKqlQuery, - updateHighlightedDropAndProviderId: timelineActions.updateHighlightedDropAndProviderId, - updateItemsPerPage: timelineActions.updateItemsPerPage, - updateItemsPerPageOptions: timelineActions.updateItemsPerPageOptions, - updateSort: timelineActions.updateSort, - upsertColumn: timelineActions.upsertColumn, -}; - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const StatefulTimeline = connector(StatefulTimelineComponent); diff --git a/x-pack/plugins/siem/public/components/timeline/insert_timeline_popover/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/insert_timeline_popover/index.test.tsx deleted file mode 100644 index c5aea833a4b2f9..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/insert_timeline_popover/index.test.tsx +++ /dev/null @@ -1,65 +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 { mount } from 'enzyme'; -/* eslint-disable @kbn/eslint/module_migration */ -import routeData from 'react-router'; -/* eslint-enable @kbn/eslint/module_migration */ -import { InsertTimelinePopoverComponent } from './'; - -const mockDispatch = jest.fn(); -jest.mock('react-redux', () => { - const reactRedux = jest.requireActual('react-redux'); - return { - ...reactRedux, - useDispatch: () => mockDispatch, - }; -}); -const mockLocation = { - pathname: '/apath', - hash: '', - search: '', - state: '', -}; -const mockLocationWithState = { - ...mockLocation, - state: { - insertTimeline: { - timelineId: 'timeline-id', - timelineSavedObjectId: '34578-3497-5893-47589-34759', - timelineTitle: 'Timeline title', - }, - }, -}; - -const onTimelineChange = jest.fn(); -const defaultProps = { - isDisabled: false, - onTimelineChange, -}; - -describe('Insert timeline popover ', () => { - beforeEach(() => { - jest.resetAllMocks(); - }); - - it('should insert a timeline when passed in the router state', () => { - jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocationWithState); - mount(); - expect(mockDispatch).toBeCalledWith({ - payload: { id: 'timeline-id', show: false }, - type: 'x-pack/siem/local/timeline/SHOW_TIMELINE', - }); - expect(onTimelineChange).toBeCalledWith('Timeline title', '34578-3497-5893-47589-34759'); - }); - it('should do nothing when router state', () => { - jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); - mount(); - expect(mockDispatch).toHaveBeenCalledTimes(0); - expect(onTimelineChange).toHaveBeenCalledTimes(0); - }); -}); diff --git a/x-pack/plugins/siem/public/components/timeline/insert_timeline_popover/index.tsx b/x-pack/plugins/siem/public/components/timeline/insert_timeline_popover/index.tsx deleted file mode 100644 index 573e010868babd..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/insert_timeline_popover/index.tsx +++ /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 { EuiButtonIcon, EuiPopover, EuiSelectableOption, EuiToolTip } from '@elastic/eui'; -import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; -import { useLocation } from 'react-router-dom'; -import { useDispatch } from 'react-redux'; - -import { OpenTimelineResult } from '../../open_timeline/types'; -import { SelectableTimeline } from '../selectable_timeline'; -import * as i18n from '../translations'; -import { timelineActions } from '../../../store/timeline'; - -interface InsertTimelinePopoverProps { - isDisabled: boolean; - hideUntitled?: boolean; - onTimelineChange: (timelineTitle: string, timelineId: string | null) => void; -} - -interface RouterState { - insertTimeline: { - timelineId: string; - timelineSavedObjectId: string; - timelineTitle: string; - }; -} - -type Props = InsertTimelinePopoverProps; - -export const InsertTimelinePopoverComponent: React.FC = ({ - isDisabled, - hideUntitled = false, - onTimelineChange, -}) => { - const dispatch = useDispatch(); - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const { state } = useLocation(); - const [routerState, setRouterState] = useState(state ?? null); - - useEffect(() => { - if (routerState && routerState.insertTimeline) { - dispatch( - timelineActions.showTimeline({ id: routerState.insertTimeline.timelineId, show: false }) - ); - onTimelineChange( - routerState.insertTimeline.timelineTitle, - routerState.insertTimeline.timelineSavedObjectId - ); - setRouterState(null); - } - }, [routerState]); - - const handleClosePopover = useCallback(() => { - setIsPopoverOpen(false); - }, []); - - const handleOpenPopover = useCallback(() => { - setIsPopoverOpen(true); - }, []); - - const insertTimelineButton = useMemo( - () => ( - {i18n.INSERT_TIMELINE}

}> - -
- ), - [handleOpenPopover, isDisabled] - ); - - const handleGetSelectableOptions = useCallback( - ({ timelines }) => [ - ...timelines.map( - (t: OpenTimelineResult, index: number) => - ({ - description: t.description, - favorite: t.favorite, - label: t.title, - id: t.savedObjectId, - key: `${t.title}-${index}`, - title: t.title, - checked: undefined, - } as EuiSelectableOption) - ), - ], - [] - ); - - return ( - - - - ); -}; - -export const InsertTimelinePopover = memo(InsertTimelinePopoverComponent); diff --git a/x-pack/plugins/siem/public/components/timeline/properties/helpers.tsx b/x-pack/plugins/siem/public/components/timeline/properties/helpers.tsx deleted file mode 100644 index 4c64c8a100b41a..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/properties/helpers.tsx +++ /dev/null @@ -1,327 +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 { - EuiBadge, - EuiButton, - EuiButtonEmpty, - EuiButtonIcon, - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiModal, - EuiOverlayMask, - EuiToolTip, -} from '@elastic/eui'; -import React, { useCallback } from 'react'; -import uuid from 'uuid'; -import styled from 'styled-components'; -import { useHistory } from 'react-router-dom'; -import { useSelector } from 'react-redux'; - -import { Note } from '../../../lib/note'; -import { Notes } from '../../notes'; -import { AssociateNote, UpdateNote } from '../../notes/helpers'; -import { NOTES_PANEL_WIDTH } from './notes_size'; -import { ButtonContainer, DescriptionContainer, LabelText, NameField, StyledStar } from './styles'; -import * as i18n from './translations'; -import { SiemPageName } from '../../../pages/home/types'; -import { timelineSelectors } from '../../../store/timeline'; -import { State } from '../../../store'; - -export const historyToolTip = 'The chronological history of actions related to this timeline'; -export const streamLiveToolTip = 'Update the Timeline as new data arrives'; -export const newTimelineToolTip = 'Create a new timeline'; - -const NotesCountBadge = (styled(EuiBadge)` - margin-left: 5px; -` as unknown) as typeof EuiBadge; - -NotesCountBadge.displayName = 'NotesCountBadge'; - -type CreateTimeline = ({ id, show }: { id: string; show?: boolean }) => void; -type UpdateIsFavorite = ({ id, isFavorite }: { id: string; isFavorite: boolean }) => void; -type UpdateTitle = ({ id, title }: { id: string; title: string }) => void; -type UpdateDescription = ({ id, description }: { id: string; description: string }) => void; - -export const StarIcon = React.memo<{ - isFavorite: boolean; - timelineId: string; - updateIsFavorite: UpdateIsFavorite; -}>(({ isFavorite, timelineId: id, updateIsFavorite }) => ( - // TODO: 1 error is: Visible, non-interactive elements with click handlers must have at least one keyboard listener - // TODO: 2 error is: Elements with the 'button' interactive role must be focusable - // TODO: Investigate this error - // eslint-disable-next-line -
updateIsFavorite({ id, isFavorite: !isFavorite })}> - {isFavorite ? ( - - - - ) : ( - - - - )} -
-)); -StarIcon.displayName = 'StarIcon'; - -interface DescriptionProps { - description: string; - timelineId: string; - updateDescription: UpdateDescription; -} - -export const Description = React.memo( - ({ description, timelineId, updateDescription }) => ( - - - updateDescription({ id: timelineId, description: e.target.value })} - placeholder={i18n.DESCRIPTION} - spellCheck={true} - value={description} - /> - - - ) -); -Description.displayName = 'Description'; - -interface NameProps { - timelineId: string; - title: string; - updateTitle: UpdateTitle; -} - -export const Name = React.memo(({ timelineId, title, updateTitle }) => ( - - updateTitle({ id: timelineId, title: e.target.value })} - placeholder={i18n.UNTITLED_TIMELINE} - spellCheck={true} - value={title} - /> - -)); -Name.displayName = 'Name'; - -interface NewCaseProps { - onClosePopover: () => void; - timelineId: string; - timelineTitle: string; -} - -export const NewCase = React.memo(({ onClosePopover, timelineId, timelineTitle }) => { - const history = useHistory(); - const { savedObjectId } = useSelector((state: State) => - timelineSelectors.selectTimeline(state, timelineId) - ); - const handleClick = useCallback(() => { - onClosePopover(); - history.push({ - pathname: `/${SiemPageName.case}/create`, - state: { - insertTimeline: { - timelineId, - timelineSavedObjectId: savedObjectId, - timelineTitle: timelineTitle.length > 0 ? timelineTitle : i18n.UNTITLED_TIMELINE, - }, - }, - }); - }, [onClosePopover, history, timelineId, timelineTitle]); - - return ( - - {i18n.ATTACH_TIMELINE_TO_NEW_CASE} - - ); -}); -NewCase.displayName = 'NewCase'; - -interface NewTimelineProps { - createTimeline: CreateTimeline; - onClosePopover: () => void; - timelineId: string; -} - -export const NewTimeline = React.memo( - ({ createTimeline, onClosePopover, timelineId }) => { - const handleClick = useCallback(() => { - createTimeline({ id: timelineId, show: true }); - onClosePopover(); - }, [createTimeline, timelineId, onClosePopover]); - - return ( - - {i18n.NEW_TIMELINE} - - ); - } -); -NewTimeline.displayName = 'NewTimeline'; - -interface NotesButtonProps { - animate?: boolean; - associateNote: AssociateNote; - getNotesByIds: (noteIds: string[]) => Note[]; - noteIds: string[]; - size: 's' | 'l'; - showNotes: boolean; - toggleShowNotes: () => void; - text?: string; - toolTip?: string; - updateNote: UpdateNote; -} - -const getNewNoteId = (): string => uuid.v4(); - -interface LargeNotesButtonProps { - noteIds: string[]; - text?: string; - toggleShowNotes: () => void; -} - -const LargeNotesButton = React.memo(({ noteIds, text, toggleShowNotes }) => ( - toggleShowNotes()} - size="m" - > - - - - - - {text && text.length ? {text} : null} - - - - {noteIds.length} - - - - -)); -LargeNotesButton.displayName = 'LargeNotesButton'; - -interface SmallNotesButtonProps { - noteIds: string[]; - toggleShowNotes: () => void; -} - -const SmallNotesButton = React.memo(({ noteIds, toggleShowNotes }) => ( - toggleShowNotes()} - /> -)); -SmallNotesButton.displayName = 'SmallNotesButton'; - -/** - * The internal implementation of the `NotesButton` - */ -const NotesButtonComponent = React.memo( - ({ - animate = true, - associateNote, - getNotesByIds, - noteIds, - showNotes, - size, - toggleShowNotes, - text, - updateNote, - }) => ( - - <> - {size === 'l' ? ( - - ) : ( - - )} - {size === 'l' && showNotes ? ( - - - - - - ) : null} - - - ) -); -NotesButtonComponent.displayName = 'NotesButtonComponent'; - -export const NotesButton = React.memo( - ({ - animate = true, - associateNote, - getNotesByIds, - noteIds, - showNotes, - size, - toggleShowNotes, - toolTip, - text, - updateNote, - }) => - showNotes ? ( - - ) : ( - - - - ) -); -NotesButton.displayName = 'NotesButton'; diff --git a/x-pack/plugins/siem/public/components/timeline/properties/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/properties/index.test.tsx deleted file mode 100644 index e942c8f36dc837..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/properties/index.test.tsx +++ /dev/null @@ -1,466 +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 { mount } from 'enzyme'; -import React from 'react'; -import { Provider as ReduxStoreProvider } from 'react-redux'; - -import { mockGlobalState, apolloClientObservable } from '../../../mock'; -import { createStore, State } from '../../../store'; -import { useThrottledResizeObserver } from '../../utils'; - -import { Properties, showDescriptionThreshold, showNotesThreshold } from '.'; - -jest.mock('../../../lib/kibana'); - -let mockedWidth = 1000; -jest.mock('../../utils'); -(useThrottledResizeObserver as jest.Mock).mockImplementation(() => ({ - width: mockedWidth, -})); - -describe('Properties', () => { - const usersViewing = ['elastic']; - - const state: State = mockGlobalState; - let store = createStore(state, apolloClientObservable); - - beforeEach(() => { - jest.clearAllMocks(); - store = createStore(state, apolloClientObservable); - mockedWidth = 1000; - }); - - test('renders correctly', () => { - const wrapper = mount( - - - - ); - expect(wrapper.find('[data-test-subj="timeline-properties"]').exists()).toEqual(true); - }); - - test('it renders an empty star icon when it is NOT a favorite', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="timeline-favorite-empty-star"]').exists()).toEqual(true); - }); - - test('it renders a filled star icon when it is a favorite', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="timeline-favorite-filled-star"]').exists()).toEqual(true); - }); - - test('it renders the title of the timeline', () => { - const title = 'foozle'; - - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="timeline-title"]') - .first() - .props().value - ).toEqual(title); - }); - - test('it renders the date picker with the lock icon', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="properties-left"]') - .find('[data-test-subj="timeline-date-picker-container"]') - .exists() - ).toEqual(true); - }); - - test('it renders the lock icon when isDatepickerLocked is true', () => { - const wrapper = mount( - - - - ); - expect( - wrapper - .find('[data-test-subj="properties-left"]') - .find('[data-test-subj="timeline-date-picker-lock-button"]') - .exists() - ).toEqual(true); - }); - - test('it renders the unlock icon when isDatepickerLocked is false', () => { - const wrapper = mount( - - - - ); - expect( - wrapper - .find('[data-test-subj="properties-left"]') - .find('[data-test-subj="timeline-date-picker-unlock-button"]') - .exists() - ).toEqual(true); - }); - - test('it renders a description on the left when the width is at least as wide as the threshold', () => { - const description = 'strange'; - mockedWidth = showDescriptionThreshold; - - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="properties-left"]') - .find('[data-test-subj="timeline-description"]') - .first() - .props().value - ).toEqual(description); - }); - - test('it does NOT render a description on the left when the width is less than the threshold', () => { - const description = 'strange'; - mockedWidth = showDescriptionThreshold - 1; - - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="properties-left"]') - .find('[data-test-subj="timeline-description"]') - .exists() - ).toEqual(false); - }); - - test('it renders a notes button on the left when the width is at least as wide as the threshold', () => { - mockedWidth = showNotesThreshold; - - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="properties-left"]') - .find('[data-test-subj="timeline-notes-button-large"]') - .exists() - ).toEqual(true); - }); - - test('it does NOT render a a notes button on the left when the width is less than the threshold', () => { - mockedWidth = showNotesThreshold - 1; - - const wrapper = mount( - - - - ); - - expect( - wrapper - .find('[data-test-subj="properties-left"]') - .find('[data-test-subj="timeline-notes-button-large"]') - .exists() - ).toEqual(false); - }); - - test('it renders a settings icon', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="settings-gear"]').exists()).toEqual(true); - }); - - test('it renders an avatar for the current user viewing the timeline when it has a title', () => { - const title = 'port scan'; - - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="avatar"]').exists()).toEqual(true); - }); - - test('it does NOT render an avatar for the current user viewing the timeline when it does NOT have a title', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="avatar"]').exists()).toEqual(false); - }); -}); diff --git a/x-pack/plugins/siem/public/components/timeline/properties/index.tsx b/x-pack/plugins/siem/public/components/timeline/properties/index.tsx deleted file mode 100644 index 0080fcb1e69242..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/properties/index.tsx +++ /dev/null @@ -1,154 +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, { useState, useCallback, useMemo } from 'react'; - -import { useThrottledResizeObserver } from '../../utils'; -import { Note } from '../../../lib/note'; -import { InputsModelId } from '../../../store/inputs/constants'; -import { AssociateNote, UpdateNote } from '../../notes/helpers'; - -import { TimelineProperties } from './styles'; -import { PropertiesRight } from './properties_right'; -import { PropertiesLeft } from './properties_left'; - -type CreateTimeline = ({ id, show }: { id: string; show?: boolean }) => void; -type UpdateIsFavorite = ({ id, isFavorite }: { id: string; isFavorite: boolean }) => void; -type UpdateTitle = ({ id, title }: { id: string; title: string }) => void; -type UpdateDescription = ({ id, description }: { id: string; description: string }) => void; -type ToggleLock = ({ linkToId }: { linkToId: InputsModelId }) => void; - -interface Props { - associateNote: AssociateNote; - createTimeline: CreateTimeline; - description: string; - getNotesByIds: (noteIds: string[]) => Note[]; - isDataInTimeline: boolean; - isDatepickerLocked: boolean; - isFavorite: boolean; - noteIds: string[]; - timelineId: string; - title: string; - toggleLock: ToggleLock; - updateDescription: UpdateDescription; - updateIsFavorite: UpdateIsFavorite; - updateNote: UpdateNote; - updateTitle: UpdateTitle; - usersViewing: string[]; -} - -const rightGutter = 60; // px -export const datePickerThreshold = 600; -export const showNotesThreshold = 810; -export const showDescriptionThreshold = 970; - -const starIconWidth = 30; -const nameWidth = 155; -const descriptionWidth = 165; -const noteWidth = 130; -const settingsWidth = 55; - -/** Displays the properties of a timeline, i.e. name, description, notes, etc */ -export const Properties = React.memo( - ({ - associateNote, - createTimeline, - description, - getNotesByIds, - isDataInTimeline, - isDatepickerLocked, - isFavorite, - noteIds, - timelineId, - title, - toggleLock, - updateDescription, - updateIsFavorite, - updateNote, - updateTitle, - usersViewing, - }) => { - const { ref, width = 0 } = useThrottledResizeObserver(300); - const [showActions, setShowActions] = useState(false); - const [showNotes, setShowNotes] = useState(false); - const [showTimelineModal, setShowTimelineModal] = useState(false); - - const onButtonClick = useCallback(() => setShowActions(!showActions), [showActions]); - const onToggleShowNotes = useCallback(() => setShowNotes(!showNotes), [showNotes]); - const onClosePopover = useCallback(() => setShowActions(false), []); - const onCloseTimelineModal = useCallback(() => setShowTimelineModal(false), []); - const onToggleLock = useCallback(() => toggleLock({ linkToId: 'timeline' }), [toggleLock]); - const onOpenTimelineModal = useCallback(() => { - onClosePopover(); - setShowTimelineModal(true); - }, []); - - const datePickerWidth = useMemo( - () => - width - - rightGutter - - starIconWidth - - nameWidth - - (width >= showDescriptionThreshold ? descriptionWidth : 0) - - noteWidth - - settingsWidth, - [width] - ); - - return ( - - datePickerThreshold ? datePickerThreshold : datePickerWidth - } - description={description} - getNotesByIds={getNotesByIds} - isDatepickerLocked={isDatepickerLocked} - isFavorite={isFavorite} - noteIds={noteIds} - onToggleShowNotes={onToggleShowNotes} - showDescription={width >= showDescriptionThreshold} - showNotes={showNotes} - showNotesFromWidth={width >= showNotesThreshold} - timelineId={timelineId} - title={title} - toggleLock={onToggleLock} - updateDescription={updateDescription} - updateIsFavorite={updateIsFavorite} - updateNote={updateNote} - updateTitle={updateTitle} - /> - 0} - timelineId={timelineId} - title={title} - updateDescription={updateDescription} - updateNote={updateNote} - usersViewing={usersViewing} - /> - - ); - } -); - -Properties.displayName = 'Properties'; diff --git a/x-pack/plugins/siem/public/components/timeline/query_bar/index.test.tsx b/x-pack/plugins/siem/public/components/timeline/query_bar/index.test.tsx deleted file mode 100644 index a78e5b8e1d2264..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/query_bar/index.test.tsx +++ /dev/null @@ -1,409 +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 { mount } from 'enzyme'; -import React from 'react'; - -import { DEFAULT_FROM, DEFAULT_TO } from '../../../../common/constants'; -import { mockBrowserFields } from '../../../containers/source/mock'; -import { convertKueryToElasticSearchQuery } from '../../../lib/keury'; -import { mockIndexPattern, TestProviders } from '../../../mock'; -import { createKibanaCoreStartMock } from '../../../mock/kibana_core'; -import { QueryBar } from '../../query_bar'; -import { FilterManager } from '../../../../../../../src/plugins/data/public'; -import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; -import { buildGlobalQuery } from '../helpers'; - -import { QueryBarTimeline, QueryBarTimelineComponentProps, getDataProviderFilter } from './index'; - -const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; - -jest.mock('../../../lib/kibana'); - -describe('Timeline QueryBar ', () => { - // We are doing that because we need to wrapped this component with redux - // and redux does not like to be updated and since we need to update our - // child component (BODY) and we do not want to scare anyone with this error - // we are hiding it!!! - // eslint-disable-next-line no-console - const originalError = console.error; - beforeAll(() => { - // eslint-disable-next-line no-console - console.error = (...args: string[]) => { - if (/ does not support changing `store` on the fly/.test(args[0])) { - return; - } - originalError.call(console, ...args); - }; - }); - - const mockApplyKqlFilterQuery = jest.fn(); - const mockSetFilters = jest.fn(); - const mockSetKqlFilterQueryDraft = jest.fn(); - const mockSetSavedQueryId = jest.fn(); - const mockUpdateReduxTime = jest.fn(); - - beforeEach(() => { - mockApplyKqlFilterQuery.mockClear(); - mockSetFilters.mockClear(); - mockSetKqlFilterQueryDraft.mockClear(); - mockSetSavedQueryId.mockClear(); - mockUpdateReduxTime.mockClear(); - }); - - test('check if we format the appropriate props to QueryBar', () => { - const wrapper = mount( - - - - ); - const queryBarProps = wrapper.find(QueryBar).props(); - - expect(queryBarProps.dateRangeFrom).toEqual('now-24h'); - expect(queryBarProps.dateRangeTo).toEqual('now'); - expect(queryBarProps.filterQuery).toEqual({ query: 'here: query', language: 'kuery' }); - expect(queryBarProps.savedQuery).toEqual(null); - }); - - describe('#onChangeQuery', () => { - test(' is the only reference that changed when filterQueryDraft props get updated', () => { - const Proxy = (props: QueryBarTimelineComponentProps) => ( - - - - ); - - const wrapper = mount( - - ); - const queryBarProps = wrapper.find(QueryBar).props(); - const onChangedQueryRef = queryBarProps.onChangedQuery; - const onSubmitQueryRef = queryBarProps.onSubmitQuery; - const onSavedQueryRef = queryBarProps.onSavedQuery; - - wrapper.setProps({ filterQueryDraft: { expression: 'new: one', kind: 'kuery' } }); - wrapper.update(); - - expect(onChangedQueryRef).not.toEqual(wrapper.find(QueryBar).props().onChangedQuery); - expect(onSubmitQueryRef).toEqual(wrapper.find(QueryBar).props().onSubmitQuery); - expect(onSavedQueryRef).toEqual(wrapper.find(QueryBar).props().onSavedQuery); - }); - }); - - describe('#onSubmitQuery', () => { - test(' is the only reference that changed when filterQuery props get updated', () => { - const Proxy = (props: QueryBarTimelineComponentProps) => ( - - - - ); - - const wrapper = mount( - - ); - const queryBarProps = wrapper.find(QueryBar).props(); - const onChangedQueryRef = queryBarProps.onChangedQuery; - const onSubmitQueryRef = queryBarProps.onSubmitQuery; - const onSavedQueryRef = queryBarProps.onSavedQuery; - - wrapper.setProps({ filterQuery: { expression: 'new: one', kind: 'kuery' } }); - wrapper.update(); - - expect(onSubmitQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSubmitQuery); - expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); - expect(onSavedQueryRef).toEqual(wrapper.find(QueryBar).props().onSavedQuery); - }); - - test(' is only reference that changed when timelineId props get updated', () => { - const Proxy = (props: QueryBarTimelineComponentProps) => ( - - - - ); - - const wrapper = mount( - - ); - const queryBarProps = wrapper.find(QueryBar).props(); - const onChangedQueryRef = queryBarProps.onChangedQuery; - const onSubmitQueryRef = queryBarProps.onSubmitQuery; - const onSavedQueryRef = queryBarProps.onSavedQuery; - - wrapper.setProps({ timelineId: 'new-timeline' }); - wrapper.update(); - - expect(onSubmitQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSubmitQuery); - expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); - expect(onSavedQueryRef).toEqual(wrapper.find(QueryBar).props().onSavedQuery); - }); - }); - - describe('#onSavedQuery', () => { - test('is only reference that changed when dataProviders props get updated', () => { - const Proxy = (props: QueryBarTimelineComponentProps) => ( - - - - ); - - const wrapper = mount( - - ); - const queryBarProps = wrapper.find(QueryBar).props(); - const onChangedQueryRef = queryBarProps.onChangedQuery; - const onSubmitQueryRef = queryBarProps.onSubmitQuery; - const onSavedQueryRef = queryBarProps.onSavedQuery; - - wrapper.setProps({ dataProviders: mockDataProviders.slice(1, 0) }); - wrapper.update(); - - expect(onSavedQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSavedQuery); - expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); - expect(onSubmitQueryRef).toEqual(wrapper.find(QueryBar).props().onSubmitQuery); - }); - - test('is only reference that changed when savedQueryId props get updated', () => { - const Proxy = (props: QueryBarTimelineComponentProps) => ( - - - - ); - - const wrapper = mount( - - ); - const queryBarProps = wrapper.find(QueryBar).props(); - const onChangedQueryRef = queryBarProps.onChangedQuery; - const onSubmitQueryRef = queryBarProps.onSubmitQuery; - const onSavedQueryRef = queryBarProps.onSavedQuery; - - wrapper.setProps({ - savedQueryId: 'new', - }); - wrapper.update(); - - expect(onSavedQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSavedQuery); - expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); - expect(onSubmitQueryRef).toEqual(wrapper.find(QueryBar).props().onSubmitQuery); - }); - }); - - describe('#getDataProviderFilter', () => { - test('returns valid data provider filter with a simple bool data provider', () => { - const dataProvidersDsl = convertKueryToElasticSearchQuery( - buildGlobalQuery(mockDataProviders.slice(0, 1), mockBrowserFields), - mockIndexPattern - ); - const filter = getDataProviderFilter(dataProvidersDsl); - expect(filter).toEqual({ - $state: { - store: 'appState', - }, - bool: { - minimum_should_match: 1, - should: [ - { - match_phrase: { - name: 'Provider 1', - }, - }, - ], - }, - meta: { - alias: 'timeline-filter-drop-area', - controlledBy: 'timeline-filter-drop-area', - disabled: false, - key: 'bool', - negate: false, - type: 'custom', - value: - '{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}}', - }, - }); - }); - - test('returns valid data provider filter with an exists operator', () => { - const dataProvidersDsl = convertKueryToElasticSearchQuery( - buildGlobalQuery( - [ - { - id: `id-exists`, - name, - enabled: true, - excluded: false, - kqlQuery: '', - queryMatch: { - field: 'host.name', - value: '', - operator: ':*', - }, - and: [], - }, - ], - mockBrowserFields - ), - mockIndexPattern - ); - const filter = getDataProviderFilter(dataProvidersDsl); - expect(filter).toEqual({ - $state: { - store: 'appState', - }, - bool: { - minimum_should_match: 1, - should: [ - { - exists: { - field: 'host.name', - }, - }, - ], - }, - meta: { - alias: 'timeline-filter-drop-area', - controlledBy: 'timeline-filter-drop-area', - disabled: false, - key: 'bool', - negate: false, - type: 'custom', - value: '{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}}', - }, - }); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/timeline/query_bar/index.tsx b/x-pack/plugins/siem/public/components/timeline/query_bar/index.tsx deleted file mode 100644 index 7d2b4f71183dde..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/query_bar/index.tsx +++ /dev/null @@ -1,319 +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 { isEmpty } from 'lodash/fp'; -import React, { memo, useCallback, useState, useEffect } from 'react'; -import { Subscription } from 'rxjs'; -import deepEqual from 'fast-deep-equal'; - -import { - IIndexPattern, - Query, - Filter, - esFilters, - FilterManager, - SavedQuery, - SavedQueryTimeFilter, -} from '../../../../../../../src/plugins/data/public'; - -import { BrowserFields } from '../../../containers/source'; -import { convertKueryToElasticSearchQuery } from '../../../lib/keury'; -import { KueryFilterQuery, KueryFilterQueryKind } from '../../../store'; -import { KqlMode } from '../../../store/timeline/model'; -import { useSavedQueryServices } from '../../../utils/saved_query_services'; -import { DispatchUpdateReduxTime } from '../../super_date_picker'; -import { QueryBar } from '../../query_bar'; -import { DataProvider } from '../data_providers/data_provider'; -import { buildGlobalQuery } from '../helpers'; - -export interface QueryBarTimelineComponentProps { - applyKqlFilterQuery: (expression: string, kind: KueryFilterQueryKind) => void; - browserFields: BrowserFields; - dataProviders: DataProvider[]; - filters: Filter[]; - filterManager: FilterManager; - filterQuery: KueryFilterQuery; - filterQueryDraft: KueryFilterQuery; - from: number; - fromStr: string; - kqlMode: KqlMode; - indexPattern: IIndexPattern; - isRefreshPaused: boolean; - refreshInterval: number; - savedQueryId: string | null; - setFilters: (filters: Filter[]) => void; - setKqlFilterQueryDraft: (expression: string, kind: KueryFilterQueryKind) => void; - setSavedQueryId: (savedQueryId: string | null) => void; - timelineId: string; - to: number; - toStr: string; - updateReduxTime: DispatchUpdateReduxTime; -} - -const timelineFilterDropArea = 'timeline-filter-drop-area'; - -export const QueryBarTimeline = memo( - ({ - applyKqlFilterQuery, - browserFields, - dataProviders, - filters, - filterManager, - filterQuery, - filterQueryDraft, - from, - fromStr, - kqlMode, - indexPattern, - isRefreshPaused, - savedQueryId, - setFilters, - setKqlFilterQueryDraft, - setSavedQueryId, - refreshInterval, - timelineId, - to, - toStr, - updateReduxTime, - }) => { - const [dateRangeFrom, setDateRangeFrom] = useState( - fromStr != null ? fromStr : new Date(from).toISOString() - ); - const [dateRangeTo, setDateRangTo] = useState( - toStr != null ? toStr : new Date(to).toISOString() - ); - - const [savedQuery, setSavedQuery] = useState(null); - const [filterQueryConverted, setFilterQueryConverted] = useState({ - query: filterQuery != null ? filterQuery.expression : '', - language: filterQuery != null ? filterQuery.kind : 'kuery', - }); - const [queryBarFilters, setQueryBarFilters] = useState([]); - const [dataProvidersDsl, setDataProvidersDsl] = useState( - convertKueryToElasticSearchQuery(buildGlobalQuery(dataProviders, browserFields), indexPattern) - ); - const savedQueryServices = useSavedQueryServices(); - - useEffect(() => { - let isSubscribed = true; - const subscriptions = new Subscription(); - filterManager.setFilters(filters); - - subscriptions.add( - filterManager.getUpdates$().subscribe({ - next: () => { - if (isSubscribed) { - const filterWithoutDropArea = filterManager - .getFilters() - .filter((f: Filter) => f.meta.controlledBy !== timelineFilterDropArea); - setFilters(filterWithoutDropArea); - setQueryBarFilters(filterWithoutDropArea); - } - }, - }) - ); - - return () => { - isSubscribed = false; - subscriptions.unsubscribe(); - }; - }, []); - - useEffect(() => { - const filterWithoutDropArea = filterManager - .getFilters() - .filter((f: Filter) => f.meta.controlledBy !== timelineFilterDropArea); - if (!deepEqual(filters, filterWithoutDropArea)) { - filterManager.setFilters(filters); - } - }, [filters]); - - useEffect(() => { - setFilterQueryConverted({ - query: filterQuery != null ? filterQuery.expression : '', - language: filterQuery != null ? filterQuery.kind : 'kuery', - }); - }, [filterQuery]); - - useEffect(() => { - setDataProvidersDsl( - convertKueryToElasticSearchQuery( - buildGlobalQuery(dataProviders, browserFields), - indexPattern - ) - ); - }, [dataProviders, browserFields, indexPattern]); - - useEffect(() => { - if (fromStr != null && toStr != null) { - setDateRangeFrom(fromStr); - setDateRangTo(toStr); - } else if (from != null && to != null) { - setDateRangeFrom(new Date(from).toISOString()); - setDateRangTo(new Date(to).toISOString()); - } - }, [from, fromStr, to, toStr]); - - useEffect(() => { - let isSubscribed = true; - async function setSavedQueryByServices() { - if (savedQueryId != null && savedQueryServices != null) { - try { - // The getSavedQuery function will throw a promise rejection in - // src/legacy/core_plugins/data/public/search/search_bar/lib/saved_query_service.ts - // if the savedObjectsClient is undefined. This is happening in a test - // so I wrapped this in a try catch to keep the unhandled promise rejection - // warning from appearing in tests. - const mySavedQuery = await savedQueryServices.getSavedQuery(savedQueryId); - if (isSubscribed && mySavedQuery != null) { - setSavedQuery({ - ...mySavedQuery, - attributes: { - ...mySavedQuery.attributes, - filters: filters.filter(f => f.meta.controlledBy !== timelineFilterDropArea), - }, - }); - } - } catch (exc) { - setSavedQuery(null); - } - } else if (isSubscribed) { - setSavedQuery(null); - } - } - setSavedQueryByServices(); - return () => { - isSubscribed = false; - }; - }, [savedQueryId]); - - const onChangedQuery = useCallback( - (newQuery: Query) => { - if ( - filterQueryDraft == null || - (filterQueryDraft != null && filterQueryDraft.expression !== newQuery.query) || - filterQueryDraft.kind !== newQuery.language - ) { - setKqlFilterQueryDraft( - newQuery.query as string, - newQuery.language as KueryFilterQueryKind - ); - } - }, - [filterQueryDraft] - ); - - const onSubmitQuery = useCallback( - (newQuery: Query, timefilter?: SavedQueryTimeFilter) => { - if ( - filterQuery == null || - (filterQuery != null && filterQuery.expression !== newQuery.query) || - filterQuery.kind !== newQuery.language - ) { - setKqlFilterQueryDraft( - newQuery.query as string, - newQuery.language as KueryFilterQueryKind - ); - applyKqlFilterQuery(newQuery.query as string, newQuery.language as KueryFilterQueryKind); - } - if (timefilter != null) { - const isQuickSelection = timefilter.from.includes('now') || timefilter.to.includes('now'); - - updateReduxTime({ - id: 'timeline', - end: timefilter.to, - start: timefilter.from, - isInvalid: false, - isQuickSelection, - timelineId, - }); - } - }, - [filterQuery, timelineId] - ); - - const onSavedQuery = useCallback( - (newSavedQuery: SavedQuery | null) => { - if (newSavedQuery != null) { - if (newSavedQuery.id !== savedQueryId) { - setSavedQueryId(newSavedQuery.id); - } - if (savedQueryServices != null && dataProvidersDsl !== '') { - const dataProviderFilterExists = - newSavedQuery.attributes.filters != null - ? newSavedQuery.attributes.filters.findIndex( - f => f.meta.controlledBy === timelineFilterDropArea - ) - : -1; - savedQueryServices.saveQuery( - { - ...newSavedQuery.attributes, - filters: - newSavedQuery.attributes.filters != null - ? dataProviderFilterExists > -1 - ? [ - ...newSavedQuery.attributes.filters.slice(0, dataProviderFilterExists), - getDataProviderFilter(dataProvidersDsl), - ...newSavedQuery.attributes.filters.slice(dataProviderFilterExists + 1), - ] - : [ - ...newSavedQuery.attributes.filters, - getDataProviderFilter(dataProvidersDsl), - ] - : [], - }, - { - overwrite: true, - } - ); - } - } else { - setSavedQueryId(null); - } - }, - [dataProvidersDsl, savedQueryId, savedQueryServices] - ); - - return ( - - ); - } -); - -export const getDataProviderFilter = (dataProviderDsl: string): Filter => { - const dslObject = JSON.parse(dataProviderDsl); - const key = Object.keys(dslObject); - return { - ...dslObject, - meta: { - alias: timelineFilterDropArea, - controlledBy: timelineFilterDropArea, - negate: false, - disabled: false, - type: 'custom', - key: isEmpty(key) ? 'bool' : key[0], - value: dataProviderDsl, - }, - $state: { - store: esFilters.FilterStateStore.APP_STATE, - }, - }; -}; diff --git a/x-pack/plugins/siem/public/components/timeline/search_or_filter/helpers.tsx b/x-pack/plugins/siem/public/components/timeline/search_or_filter/helpers.tsx deleted file mode 100644 index 5db453988cbb8e..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/search_or_filter/helpers.tsx +++ /dev/null @@ -1,90 +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 { EuiSpacer, EuiText } from '@elastic/eui'; -import React from 'react'; -import styled from 'styled-components'; - -import { AndOrBadge } from '../../and_or_badge'; - -import * as i18n from './translations'; -import { KqlMode } from '../../../store/timeline/model'; - -const AndOrContainer = styled.div` - position: relative; - top: -1px; -`; - -AndOrContainer.displayName = 'AndOrContainer'; - -interface ModeProperties { - mode: KqlMode; - description: string; - kqlBarTooltip: string; - placeholder: string; - selectText: string; -} - -export const modes: { [key in KqlMode]: ModeProperties } = { - filter: { - mode: 'filter', - description: i18n.FILTER_DESCRIPTION, - kqlBarTooltip: i18n.FILTER_KQL_TOOLTIP, - placeholder: i18n.FILTER_KQL_PLACEHOLDER, - selectText: i18n.FILTER_KQL_SELECTED_TEXT, - }, - search: { - mode: 'search', - description: i18n.SEARCH_DESCRIPTION, - kqlBarTooltip: i18n.SEARCH_KQL_TOOLTIP, - placeholder: i18n.SEARCH_KQL_PLACEHOLDER, - selectText: i18n.SEARCH_KQL_SELECTED_TEXT, - }, -}; - -export const options = [ - { - value: modes.filter.mode, - inputDisplay: ( - - - {modes.filter.selectText} - - ), - dropdownDisplay: ( - <> - - {modes.filter.selectText} - - -

{modes.filter.description}

-
- - ), - }, - { - value: modes.search.mode, - inputDisplay: ( - - - {modes.search.selectText} - - ), - dropdownDisplay: ( - <> - - {modes.search.selectText} - - -

{modes.search.description}

-
- - ), - }, -]; - -export const getPlaceholderText = (kqlMode: KqlMode): string => - kqlMode === 'filter' ? i18n.FILTER_KQL_PLACEHOLDER : i18n.SEARCH_KQL_PLACEHOLDER; diff --git a/x-pack/plugins/siem/public/components/timeline/search_or_filter/index.tsx b/x-pack/plugins/siem/public/components/timeline/search_or_filter/index.tsx deleted file mode 100644 index fa92ef9ce59652..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/search_or_filter/index.tsx +++ /dev/null @@ -1,239 +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 { getOr } from 'lodash/fp'; -import React, { useCallback } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import { Dispatch } from 'redux'; -import deepEqual from 'fast-deep-equal'; - -import { Filter, FilterManager, IIndexPattern } from '../../../../../../../src/plugins/data/public'; -import { BrowserFields } from '../../../containers/source'; -import { convertKueryToElasticSearchQuery } from '../../../lib/keury'; -import { - KueryFilterQuery, - SerializedFilterQuery, - State, - timelineSelectors, - inputsModel, - inputsSelectors, -} from '../../../store'; -import { timelineActions } from '../../../store/actions'; -import { KqlMode, TimelineModel, EventType } from '../../../store/timeline/model'; -import { timelineDefaults } from '../../../store/timeline/defaults'; -import { dispatchUpdateReduxTime } from '../../super_date_picker'; -import { SearchOrFilter } from './search_or_filter'; - -interface OwnProps { - browserFields: BrowserFields; - filterManager: FilterManager; - indexPattern: IIndexPattern; - timelineId: string; -} - -type Props = OwnProps & PropsFromRedux; - -const StatefulSearchOrFilterComponent = React.memo( - ({ - applyKqlFilterQuery, - browserFields, - dataProviders, - eventType, - filters, - filterManager, - filterQuery, - filterQueryDraft, - from, - fromStr, - indexPattern, - isRefreshPaused, - kqlMode, - refreshInterval, - savedQueryId, - setFilters, - setKqlFilterQueryDraft, - setSavedQueryId, - timelineId, - to, - toStr, - updateEventType, - updateKqlMode, - updateReduxTime, - }) => { - const applyFilterQueryFromKueryExpression = useCallback( - (expression: string, kind) => - applyKqlFilterQuery({ - id: timelineId, - filterQuery: { - kuery: { - kind, - expression, - }, - serializedQuery: convertKueryToElasticSearchQuery(expression, indexPattern), - }, - }), - [indexPattern, timelineId] - ); - - const setFilterQueryDraftFromKueryExpression = useCallback( - (expression: string, kind) => - setKqlFilterQueryDraft({ - id: timelineId, - filterQueryDraft: { - kind, - expression, - }, - }), - [timelineId] - ); - - const setFiltersInTimeline = useCallback( - (newFilters: Filter[]) => - setFilters({ - id: timelineId, - filters: newFilters, - }), - [timelineId] - ); - - const setSavedQueryInTimeline = useCallback( - (newSavedQueryId: string | null) => - setSavedQueryId({ - id: timelineId, - savedQueryId: newSavedQueryId, - }), - [timelineId] - ); - - const handleUpdateEventType = useCallback( - (newEventType: EventType) => - updateEventType({ - id: timelineId, - eventType: newEventType, - }), - [timelineId] - ); - - return ( - - ); - }, - (prevProps, nextProps) => { - return ( - prevProps.eventType === nextProps.eventType && - prevProps.filterManager === nextProps.filterManager && - prevProps.from === nextProps.from && - prevProps.fromStr === nextProps.fromStr && - prevProps.to === nextProps.to && - prevProps.toStr === nextProps.toStr && - prevProps.isRefreshPaused === nextProps.isRefreshPaused && - prevProps.refreshInterval === nextProps.refreshInterval && - prevProps.timelineId === nextProps.timelineId && - deepEqual(prevProps.browserFields, nextProps.browserFields) && - deepEqual(prevProps.dataProviders, nextProps.dataProviders) && - deepEqual(prevProps.filters, nextProps.filters) && - deepEqual(prevProps.filterQuery, nextProps.filterQuery) && - deepEqual(prevProps.filterQueryDraft, nextProps.filterQueryDraft) && - deepEqual(prevProps.indexPattern, nextProps.indexPattern) && - deepEqual(prevProps.kqlMode, nextProps.kqlMode) && - deepEqual(prevProps.savedQueryId, nextProps.savedQueryId) && - deepEqual(prevProps.timelineId, nextProps.timelineId) - ); - } -); -StatefulSearchOrFilterComponent.displayName = 'StatefulSearchOrFilterComponent'; - -const makeMapStateToProps = () => { - const getTimeline = timelineSelectors.getTimelineByIdSelector(); - const getKqlFilterQueryDraft = timelineSelectors.getKqlFilterQueryDraftSelector(); - const getKqlFilterQuery = timelineSelectors.getKqlFilterKuerySelector(); - const getInputsTimeline = inputsSelectors.getTimelineSelector(); - const getInputsPolicy = inputsSelectors.getTimelinePolicySelector(); - const mapStateToProps = (state: State, { timelineId }: OwnProps) => { - const timeline: TimelineModel = getTimeline(state, timelineId) ?? timelineDefaults; - const input: inputsModel.InputsRange = getInputsTimeline(state); - const policy: inputsModel.Policy = getInputsPolicy(state); - return { - dataProviders: timeline.dataProviders, - eventType: timeline.eventType ?? 'raw', - filterQuery: getKqlFilterQuery(state, timelineId)!, - filterQueryDraft: getKqlFilterQueryDraft(state, timelineId)!, - filters: timeline.filters!, - from: input.timerange.from, - fromStr: input.timerange.fromStr!, - isRefreshPaused: policy.kind === 'manual', - kqlMode: getOr('filter', 'kqlMode', timeline), - refreshInterval: policy.duration, - savedQueryId: getOr(null, 'savedQueryId', timeline), - to: input.timerange.to, - toStr: input.timerange.toStr!, - }; - }; - return mapStateToProps; -}; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - applyKqlFilterQuery: ({ id, filterQuery }: { id: string; filterQuery: SerializedFilterQuery }) => - dispatch( - timelineActions.applyKqlFilterQuery({ - id, - filterQuery, - }) - ), - updateEventType: ({ id, eventType }: { id: string; eventType: EventType }) => - dispatch(timelineActions.updateEventType({ id, eventType })), - updateKqlMode: ({ id, kqlMode }: { id: string; kqlMode: KqlMode }) => - dispatch(timelineActions.updateKqlMode({ id, kqlMode })), - setKqlFilterQueryDraft: ({ - id, - filterQueryDraft, - }: { - id: string; - filterQueryDraft: KueryFilterQuery; - }) => - dispatch( - timelineActions.setKqlFilterQueryDraft({ - id, - filterQueryDraft, - }) - ), - setSavedQueryId: ({ id, savedQueryId }: { id: string; savedQueryId: string | null }) => - dispatch(timelineActions.setSavedQueryId({ id, savedQueryId })), - setFilters: ({ id, filters }: { id: string; filters: Filter[] }) => - dispatch(timelineActions.setFilters({ id, filters })), - updateReduxTime: dispatchUpdateReduxTime(dispatch), -}); - -export const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const StatefulSearchOrFilter = connector(StatefulSearchOrFilterComponent); diff --git a/x-pack/plugins/siem/public/components/timeline/selectable_timeline/index.tsx b/x-pack/plugins/siem/public/components/timeline/selectable_timeline/index.tsx deleted file mode 100644 index 964bb2061333d4..00000000000000 --- a/x-pack/plugins/siem/public/components/timeline/selectable_timeline/index.tsx +++ /dev/null @@ -1,288 +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 { - EuiSelectable, - EuiHighlight, - EuiFlexGroup, - EuiFlexItem, - EuiIcon, - EuiTextColor, - EuiSelectableOption, - EuiPortal, - EuiFilterGroup, - EuiFilterButton, -} from '@elastic/eui'; -import { isEmpty } from 'lodash/fp'; -import React, { memo, useCallback, useMemo, useState, useEffect } from 'react'; -import { ListProps } from 'react-virtualized'; -import styled from 'styled-components'; - -import { useGetAllTimeline } from '../../../containers/timeline/all'; -import { SortFieldTimeline, Direction } from '../../../graphql/types'; -import { TimelineType, TimelineTypeLiteralWithNull } from '../../../../common/types/timeline'; - -import { isUntitled } from '../../open_timeline/helpers'; -import * as i18nTimeline from '../../open_timeline/translations'; -import { OpenTimelineResult } from '../../open_timeline/types'; -import { getEmptyTagValue } from '../../empty_value'; - -import * as i18n from '../translations'; - -const MyEuiFlexItem = styled(EuiFlexItem)` - display: inline-block; - max-width: 296px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -`; - -const MyEuiFlexGroup = styled(EuiFlexGroup)` - padding 0px 4px; -`; - -const EuiSelectableContainer = styled.div<{ isLoading: boolean }>` - .euiSelectable { - .euiFormControlLayout__childrenWrapper { - display: flex; - } - ${({ isLoading }) => `${ - isLoading - ? ` - .euiFormControlLayoutIcons { - display: none; - } - .euiFormControlLayoutIcons.euiFormControlLayoutIcons--right { - display: block; - left: 12px; - top: 12px; - }` - : '' - } - `} - } -`; - -const ORIGINAL_PAGE_SIZE = 50; -const POPOVER_HEIGHT = 260; -const TIMELINE_ITEM_HEIGHT = 50; - -export interface GetSelectableOptions { - timelines: OpenTimelineResult[]; - onlyFavorites: boolean; - timelineType?: TimelineTypeLiteralWithNull; - searchTimelineValue: string; -} - -interface SelectableTimelineProps { - hideUntitled?: boolean; - getSelectableOptions: ({ - timelines, - onlyFavorites, - timelineType, - searchTimelineValue, - }: GetSelectableOptions) => EuiSelectableOption[]; - onClosePopover: () => void; - onTimelineChange: (timelineTitle: string, timelineId: string | null) => void; -} - -const SelectableTimelineComponent: React.FC = ({ - hideUntitled = false, - getSelectableOptions, - onClosePopover, - onTimelineChange, -}) => { - const [pageSize, setPageSize] = useState(ORIGINAL_PAGE_SIZE); - const [heightTrigger, setHeightTrigger] = useState(0); - const [searchTimelineValue, setSearchTimelineValue] = useState(''); - const [onlyFavorites, setOnlyFavorites] = useState(false); - const [searchRef, setSearchRef] = useState(null); - const { fetchAllTimeline, timelines, loading, totalCount: timelineCount } = useGetAllTimeline(); - - const onSearchTimeline = useCallback(val => { - setSearchTimelineValue(val); - }, []); - - const handleOnToggleOnlyFavorites = useCallback(() => { - setOnlyFavorites(!onlyFavorites); - }, [onlyFavorites]); - - const handleOnScroll = useCallback( - ( - totalTimelines: number, - totalCount: number, - { - clientHeight, - scrollHeight, - scrollTop, - }: { - clientHeight: number; - scrollHeight: number; - scrollTop: number; - } - ) => { - if (totalTimelines < totalCount) { - const clientHeightTrigger = clientHeight * 1.2; - if ( - scrollTop > 10 && - scrollHeight - scrollTop < clientHeightTrigger && - scrollHeight > heightTrigger - ) { - setHeightTrigger(scrollHeight); - setPageSize(pageSize + ORIGINAL_PAGE_SIZE); - } - } - }, - [heightTrigger, pageSize] - ); - - const renderTimelineOption = useCallback((option, searchValue) => { - return ( - - - - - - - - - {isUntitled(option) ? i18nTimeline.UNTITLED_TIMELINE : option.title} - - - - - - {option.description != null && option.description.trim().length > 0 - ? option.description - : getEmptyTagValue()} - - - - - - - - - - ); - }, []); - - const handleTimelineChange = useCallback( - options => { - const selectedTimeline = options.filter( - (option: { checked: string }) => option.checked === 'on' - ); - if (selectedTimeline != null && selectedTimeline.length > 0) { - onTimelineChange( - isEmpty(selectedTimeline[0].title) - ? i18nTimeline.UNTITLED_TIMELINE - : selectedTimeline[0].title, - selectedTimeline[0].id === '-1' ? null : selectedTimeline[0].id - ); - } - onClosePopover(); - }, - [onClosePopover, onTimelineChange] - ); - - const favoritePortal = useMemo( - () => - searchRef != null ? ( - - - - - - {i18nTimeline.ONLY_FAVORITES} - - - - - - ) : null, - [searchRef, onlyFavorites, handleOnToggleOnlyFavorites] - ); - - useEffect(() => { - fetchAllTimeline({ - pageInfo: { - pageIndex: 1, - pageSize, - }, - search: searchTimelineValue, - sort: { - sortField: SortFieldTimeline.updated, - sortOrder: Direction.desc, - }, - onlyUserFavorite: onlyFavorites, - timelineType: TimelineType.default, - }); - }, [onlyFavorites, pageSize, searchTimelineValue]); - - return ( - - !hideUntitled || t.title !== '').length, - timelineCount - ), - } as unknown) as ListProps, - }} - renderOption={renderTimelineOption} - onChange={handleTimelineChange} - searchable - searchProps={{ - 'data-test-subj': 'timeline-super-select-search-box', - isLoading: loading, - placeholder: i18n.SEARCH_BOX_TIMELINE_PLACEHOLDER, - onSearch: onSearchTimeline, - incremental: false, - inputRef: (ref: HTMLElement) => { - setSearchRef(ref); - }, - }} - singleSelection={true} - options={getSelectableOptions({ - timelines, - onlyFavorites, - searchTimelineValue, - timelineType: TimelineType.default, - })} - > - {(list, search) => ( - <> - {search} - {favoritePortal} - {list} - - )} - - - ); -}; - -export const SelectableTimeline = memo(SelectableTimelineComponent); diff --git a/x-pack/plugins/siem/public/components/top_n/helpers.ts b/x-pack/plugins/siem/public/components/top_n/helpers.ts deleted file mode 100644 index 8d9ae48d29b691..00000000000000 --- a/x-pack/plugins/siem/public/components/top_n/helpers.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EventType } from '../../store/timeline/model'; - -import * as i18n from './translations'; - -export interface TopNOption { - inputDisplay: string; - value: EventType; - 'data-test-subj': string; -} - -/** A (stable) array containing only the 'All events' option */ -export const allEvents: TopNOption[] = [ - { - value: 'all', - inputDisplay: i18n.ALL_EVENTS, - 'data-test-subj': 'option-all', - }, -]; - -/** A (stable) array containing only the 'Raw events' option */ -export const rawEvents: TopNOption[] = [ - { - value: 'raw', - inputDisplay: i18n.RAW_EVENTS, - 'data-test-subj': 'option-raw', - }, -]; - -/** A (stable) array containing only the 'Signal events' option */ -export const signalEvents: TopNOption[] = [ - { - value: 'signal', - inputDisplay: i18n.SIGNAL_EVENTS, - 'data-test-subj': 'option-signal', - }, -]; - -/** A (stable) array containing the default Top N options */ -export const defaultOptions = [...rawEvents, ...signalEvents]; - -/** - * Returns the options to be displayed in a Top N view select. When - * an `activeTimelineEventType` is provided, an array containing - * just one option (corresponding to `activeTimelineEventType`) - * will be returned, to ensure the data displayed in the Top N - * is always in sync with the `EventType` chosen by the user in - * the active timeline. - */ -export const getOptions = (activeTimelineEventType?: EventType): TopNOption[] => { - switch (activeTimelineEventType) { - case 'all': - return allEvents; - case 'raw': - return rawEvents; - case 'signal': - return signalEvents; - default: - return defaultOptions; - } -}; diff --git a/x-pack/plugins/siem/public/components/top_n/index.test.tsx b/x-pack/plugins/siem/public/components/top_n/index.test.tsx deleted file mode 100644 index 9325dcf499b2b4..00000000000000 --- a/x-pack/plugins/siem/public/components/top_n/index.test.tsx +++ /dev/null @@ -1,379 +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 { mount, ReactWrapper } from 'enzyme'; -import React from 'react'; - -import { mockBrowserFields } from '../../containers/source/mock'; -import { apolloClientObservable, mockGlobalState, TestProviders } from '../../mock'; -import { createKibanaCoreStartMock } from '../../mock/kibana_core'; -import { FilterManager } from '../../../../../../src/plugins/data/public'; -import { createStore, State } from '../../store'; -import { TimelineContext, TimelineTypeContext } from '../timeline/timeline_context'; - -import { Props } from './top_n'; -import { ACTIVE_TIMELINE_REDUX_ID, StatefulTopN } from '.'; - -jest.mock('../../lib/kibana'); - -const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; - -const field = 'process.name'; -const value = 'nice'; - -const state: State = { - ...mockGlobalState, - inputs: { - ...mockGlobalState.inputs, - global: { - ...mockGlobalState.inputs.global, - query: { - query: 'host.name : end*', - language: 'kuery', - }, - filters: [ - { - meta: { - alias: null, - negate: false, - disabled: false, - type: 'phrase', - key: 'host.os.name', - params: { - query: 'Linux', - }, - }, - query: { - match: { - 'host.os.name': { - query: 'Linux', - type: 'phrase', - }, - }, - }, - }, - ], - }, - timeline: { - ...mockGlobalState.inputs.timeline, - timerange: { - kind: 'relative', - fromStr: 'now-24h', - toStr: 'now', - from: 1586835969047, - to: 1586922369047, - }, - }, - }, - timeline: { - ...mockGlobalState.timeline, - timelineById: { - [ACTIVE_TIMELINE_REDUX_ID]: { - ...mockGlobalState.timeline.timelineById.test, - id: ACTIVE_TIMELINE_REDUX_ID, - dataProviders: [ - { - id: - 'draggable-badge-default-draggable-netflow-renderer-timeline-1-_qpBe3EBD7k-aQQL7v7--_qpBe3EBD7k-aQQL7v7--network_transport-tcp', - name: 'tcp', - enabled: true, - excluded: false, - kqlQuery: '', - queryMatch: { - field: 'network.transport', - value: 'tcp', - operator: ':', - }, - and: [], - }, - ], - eventType: 'all', - filters: [ - { - meta: { - alias: null, - disabled: false, - key: 'source.port', - negate: false, - params: { - query: '30045', - }, - type: 'phrase', - }, - query: { - match: { - 'source.port': { - query: '30045', - type: 'phrase', - }, - }, - }, - }, - ], - kqlMode: 'filter', - kqlQuery: { - filterQuery: { - kuery: { - kind: 'kuery', - expression: 'host.name : *', - }, - serializedQuery: - '{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}}', - }, - filterQueryDraft: { - kind: 'kuery', - expression: 'host.name : *', - }, - }, - }, - }, - }, -}; -const store = createStore(state, apolloClientObservable); - -describe('StatefulTopN', () => { - // Suppress warnings about "react-beautiful-dnd" - /* eslint-disable no-console */ - const originalError = console.error; - const originalWarn = console.warn; - beforeAll(() => { - console.warn = jest.fn(); - console.error = jest.fn(); - }); - afterAll(() => { - console.error = originalError; - console.warn = originalWarn; - }); - - describe('rendering in a global NON-timeline context', () => { - let wrapper: ReactWrapper; - - beforeEach(() => { - wrapper = mount( - - - - ); - }); - - test('it has undefined combinedQueries when rendering in a global context', () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.combinedQueries).toBeUndefined(); - }); - - test(`defaults to the 'Raw events' view when rendering in a global context`, () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.defaultView).toEqual('raw'); - }); - - test(`provides a 'deleteQuery' when rendering in a global context`, () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.deleteQuery).toBeDefined(); - }); - - test(`provides filters from Redux state (inputs > global > filters) when rendering in a global context`, () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.filters).toEqual([ - { - meta: { - alias: null, - negate: false, - disabled: false, - type: 'phrase', - key: 'host.os.name', - params: { query: 'Linux' }, - }, - query: { match: { 'host.os.name': { query: 'Linux', type: 'phrase' } } }, - }, - ]); - }); - - test(`provides 'from' via GlobalTime when rendering in a global context`, () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.from).toEqual(0); - }); - - test('provides the global query from Redux state (inputs > global > query) when rendering in a global context', () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.query).toEqual({ query: 'host.name : end*', language: 'kuery' }); - }); - - test(`provides a 'global' 'setAbsoluteRangeDatePickerTarget' when rendering in a global context`, () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.setAbsoluteRangeDatePickerTarget).toEqual('global'); - }); - - test(`provides 'to' via GlobalTime when rendering in a global context`, () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.to).toEqual(1); - }); - }); - - describe('rendering in a timeline context', () => { - let filterManager: FilterManager; - let wrapper: ReactWrapper; - - beforeEach(() => { - filterManager = new FilterManager(mockUiSettingsForFilterManager); - - wrapper = mount( - - - - - - - - ); - }); - - test('it has a combinedQueries value from Redux state composed of the timeline [data providers + kql + filter-bar-filters] when rendering in a timeline context', () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.combinedQueries).toEqual( - '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"network.transport":"tcp"}}],"minimum_should_match":1}},{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}}]}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1586835969047}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1586922369047}}}],"minimum_should_match":1}}]}}]}},{"match_phrase":{"source.port":{"query":"30045"}}}],"should":[],"must_not":[]}}' - ); - }); - - test('it provides only one view option that matches the `eventType` from redux when rendering in the context of the active timeline', () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.defaultView).toEqual('all'); - }); - - test(`provides an undefined 'deleteQuery' when rendering in a timeline context`, () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.deleteQuery).toBeUndefined(); - }); - - test(`provides empty filters when rendering in a timeline context`, () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.filters).toEqual([]); - }); - - test(`provides 'from' via redux state (inputs > timeline > timerange) when rendering in a timeline context`, () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.from).toEqual(1586835969047); - }); - - test('provides an empty query when rendering in a timeline context', () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.query).toEqual({ query: '', language: 'kuery' }); - }); - - test(`provides a 'timeline' 'setAbsoluteRangeDatePickerTarget' when rendering in a timeline context`, () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.setAbsoluteRangeDatePickerTarget).toEqual('timeline'); - }); - - test(`provides 'to' via redux state (inputs > timeline > timerange) when rendering in a timeline context`, () => { - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.to).toEqual(1586922369047); - }); - }); - - test(`defaults to the 'Signals events' option when rendering in a NON-active timeline context (e.g. the Signals table on the Detections page) when 'documentType' from 'useTimelineTypeContext()' is 'signals'`, () => { - const filterManager = new FilterManager(mockUiSettingsForFilterManager); - const wrapper = mount( - - - - - - - - ); - - const props = wrapper - .find('[data-test-subj="top-n"]') - .first() - .props() as Props; - - expect(props.defaultView).toEqual('signal'); - }); -}); diff --git a/x-pack/plugins/siem/public/components/top_n/index.tsx b/x-pack/plugins/siem/public/components/top_n/index.tsx deleted file mode 100644 index 9863df42f101d3..00000000000000 --- a/x-pack/plugins/siem/public/components/top_n/index.tsx +++ /dev/null @@ -1,166 +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 { connect, ConnectedProps } from 'react-redux'; - -import { GlobalTime } from '../../containers/global_time'; -import { BrowserFields, WithSource } from '../../containers/source'; -import { useKibana } from '../../lib/kibana'; -import { esQuery, Filter, Query } from '../../../../../../src/plugins/data/public'; -import { inputsModel, inputsSelectors, State, timelineSelectors } from '../../store'; -import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions'; -import { timelineDefaults } from '../../store/timeline/defaults'; -import { TimelineModel } from '../../store/timeline/model'; -import { combineQueries } from '../timeline/helpers'; -import { useTimelineTypeContext } from '../timeline/timeline_context'; - -import { getOptions } from './helpers'; -import { TopN } from './top_n'; - -/** The currently active timeline always has this Redux ID */ -export const ACTIVE_TIMELINE_REDUX_ID = 'timeline-1'; - -const EMPTY_FILTERS: Filter[] = []; -const EMPTY_QUERY: Query = { query: '', language: 'kuery' }; - -const makeMapStateToProps = () => { - const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); - const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); - const getTimeline = timelineSelectors.getTimelineByIdSelector(); - const getInputsTimeline = inputsSelectors.getTimelineSelector(); - const getKqlQueryTimeline = timelineSelectors.getKqlFilterQuerySelector(); - - // The mapped Redux state provided to this component includes the global - // filters that appear at the top of most views in the app, and all the - // filters in the active timeline: - const mapStateToProps = (state: State) => { - const activeTimeline: TimelineModel = - getTimeline(state, ACTIVE_TIMELINE_REDUX_ID) ?? timelineDefaults; - const activeTimelineFilters = activeTimeline.filters ?? EMPTY_FILTERS; - const activeTimelineInput: inputsModel.InputsRange = getInputsTimeline(state); - - return { - activeTimelineEventType: activeTimeline.eventType, - activeTimelineFilters, - activeTimelineFrom: activeTimelineInput.timerange.from, - activeTimelineKqlQueryExpression: getKqlQueryTimeline(state, ACTIVE_TIMELINE_REDUX_ID), - activeTimelineTo: activeTimelineInput.timerange.to, - dataProviders: activeTimeline.dataProviders, - globalQuery: getGlobalQuerySelector(state), - globalFilters: getGlobalFiltersQuerySelector(state), - kqlMode: activeTimeline.kqlMode, - }; - }; - - return mapStateToProps; -}; - -const mapDispatchToProps = { setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker }; - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -interface OwnProps { - browserFields: BrowserFields; - field: string; - toggleTopN: () => void; - onFilterAdded?: () => void; - value?: string[] | string | null; -} -type PropsFromRedux = ConnectedProps; -type Props = OwnProps & PropsFromRedux; - -const StatefulTopNComponent: React.FC = ({ - activeTimelineEventType, - activeTimelineFilters, - activeTimelineFrom, - activeTimelineKqlQueryExpression, - activeTimelineTo, - browserFields, - dataProviders, - field, - globalFilters = EMPTY_FILTERS, - globalQuery = EMPTY_QUERY, - kqlMode, - onFilterAdded, - setAbsoluteRangeDatePicker, - toggleTopN, - value, -}) => { - const kibana = useKibana(); - - // Regarding data from useTimelineTypeContext: - // * `documentType` (e.g. 'signals') may only be populated in some views, - // e.g. the `Signals` view on the `Detections` page. - // * `id` (`timelineId`) may only be populated when we are rendered in the - // context of the active timeline. - // * `indexToAdd`, which enables the signals index to be appended to - // the `indexPattern` returned by `WithSource`, may only be populated when - // this component is rendered in the context of the active timeline. This - // behavior enables the 'All events' view by appending the signals index - // to the index pattern. - const { documentType, id: timelineId, indexToAdd } = useTimelineTypeContext(); - - const options = getOptions( - timelineId === ACTIVE_TIMELINE_REDUX_ID ? activeTimelineEventType : undefined - ); - - return ( - - {({ from, deleteQuery, setQuery, to }) => ( - - {({ indexPattern }) => ( - - )} - - )} - - ); -}; - -StatefulTopNComponent.displayName = 'StatefulTopNComponent'; - -export const StatefulTopN = connector(React.memo(StatefulTopNComponent)); diff --git a/x-pack/plugins/siem/public/components/url_state/helpers.test.ts b/x-pack/plugins/siem/public/components/url_state/helpers.test.ts deleted file mode 100644 index c6c18d4c25a747..00000000000000 --- a/x-pack/plugins/siem/public/components/url_state/helpers.test.ts +++ /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 { navTabs } from '../../pages/home/home_navigations'; -import { getTitle } from './helpers'; -import { HostsType } from '../../store/hosts/model'; - -describe('Helpers Url_State', () => { - describe('getTitle', () => { - test('host page name', () => { - const result = getTitle('hosts', undefined, navTabs); - expect(result).toEqual('Hosts'); - }); - test('network page name', () => { - const result = getTitle('network', undefined, navTabs); - expect(result).toEqual('Network'); - }); - test('overview page name', () => { - const result = getTitle('overview', undefined, navTabs); - expect(result).toEqual('Overview'); - }); - test('timelines page name', () => { - const result = getTitle('timelines', undefined, navTabs); - expect(result).toEqual('Timelines'); - }); - test('details page name', () => { - const result = getTitle('hosts', HostsType.details, navTabs); - expect(result).toEqual(HostsType.details); - }); - test('Not existing', () => { - const result = getTitle('IamHereButNotReally', undefined, navTabs); - expect(result).toEqual(''); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/components/url_state/helpers.ts b/x-pack/plugins/siem/public/components/url_state/helpers.ts deleted file mode 100644 index 62196b90e5e6f2..00000000000000 --- a/x-pack/plugins/siem/public/components/url_state/helpers.ts +++ /dev/null @@ -1,260 +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 { isEmpty } from 'lodash/fp'; -import { parse, stringify } from 'query-string'; -import { decode, encode } from 'rison-node'; -import * as H from 'history'; - -import { Query, Filter } from '../../../../../../src/plugins/data/public'; -import { url } from '../../../../../../src/plugins/kibana_utils/public'; - -import { SiemPageName } from '../../pages/home/types'; -import { inputsSelectors, State, timelineSelectors } from '../../store'; -import { UrlInputsModel } from '../../store/inputs/model'; -import { TimelineUrl } from '../../store/timeline/model'; -import { formatDate } from '../super_date_picker'; -import { NavTab } from '../navigation/types'; -import { CONSTANTS, UrlStateType } from './constants'; -import { ReplaceStateInLocation, UpdateUrlStateString } from './types'; - -export const decodeRisonUrlState = (value: string | undefined): T | null => { - try { - return value ? ((decode(value) as unknown) as T) : null; - } catch (error) { - if (error instanceof Error && error.message.startsWith('rison decoder error')) { - return null; - } - throw error; - } -}; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const encodeRisonUrlState = (state: any) => encode(state); - -export const getQueryStringFromLocation = (search: string) => search.substring(1); - -export const getParamFromQueryString = (queryString: string, key: string) => { - const parsedQueryString = parse(queryString, { sort: false }); - const queryParam = parsedQueryString[key]; - - return Array.isArray(queryParam) ? queryParam[0] : queryParam; -}; - -export const replaceStateKeyInQueryString = (stateKey: string, urlState: T) => ( - queryString: string -): string => { - const previousQueryValues = parse(queryString, { sort: false }); - if (urlState == null || (typeof urlState === 'string' && urlState === '')) { - delete previousQueryValues[stateKey]; - - return stringify(url.encodeQuery(previousQueryValues), { sort: false, encode: false }); - } - - // ಠ_ಠ Code was copied from x-pack/legacy/plugins/infra/public/utils/url_state.tsx ಠ_ಠ - // Remove this if these utilities are promoted to kibana core - const encodedUrlState = - typeof urlState !== 'undefined' ? encodeRisonUrlState(urlState) : undefined; - - return stringify( - url.encodeQuery({ - ...previousQueryValues, - [stateKey]: encodedUrlState, - }), - { sort: false, encode: false } - ); -}; - -export const replaceQueryStringInLocation = ( - location: H.Location, - queryString: string -): H.Location => { - if (queryString === getQueryStringFromLocation(location.search)) { - return location; - } else { - return { - ...location, - search: `?${queryString}`, - }; - } -}; - -export const getUrlType = (pageName: string): UrlStateType => { - if (pageName === SiemPageName.overview) { - return 'overview'; - } else if (pageName === SiemPageName.hosts) { - return 'host'; - } else if (pageName === SiemPageName.network) { - return 'network'; - } else if (pageName === SiemPageName.detections) { - return 'detections'; - } else if (pageName === SiemPageName.timelines) { - return 'timeline'; - } else if (pageName === SiemPageName.case) { - return 'case'; - } - return 'overview'; -}; - -export const getTitle = ( - pageName: string, - detailName: string | undefined, - navTabs: Record -): string => { - if (detailName != null) return detailName; - return navTabs[pageName] != null ? navTabs[pageName].name : ''; -}; - -export const makeMapStateToProps = () => { - const getInputsSelector = inputsSelectors.inputsSelector(); - const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); - const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); - const getGlobalSavedQuerySelector = inputsSelectors.globalSavedQuerySelector(); - const getTimelines = timelineSelectors.getTimelines(); - const mapStateToProps = (state: State) => { - const inputState = getInputsSelector(state); - const { linkTo: globalLinkTo, timerange: globalTimerange } = inputState.global; - const { linkTo: timelineLinkTo, timerange: timelineTimerange } = inputState.timeline; - - const timeline = Object.entries(getTimelines(state)).reduce( - (obj, [timelineId, timelineObj]) => ({ - id: timelineObj.savedObjectId != null ? timelineObj.savedObjectId : '', - isOpen: timelineObj.show, - }), - { id: '', isOpen: false } - ); - - let searchAttr: { - [CONSTANTS.appQuery]?: Query; - [CONSTANTS.filters]?: Filter[]; - [CONSTANTS.savedQuery]?: string; - } = { - [CONSTANTS.appQuery]: getGlobalQuerySelector(state), - [CONSTANTS.filters]: getGlobalFiltersQuerySelector(state), - }; - const savedQuery = getGlobalSavedQuerySelector(state); - if (savedQuery != null && savedQuery.id !== '') { - searchAttr = { - [CONSTANTS.savedQuery]: savedQuery.id, - }; - } - - return { - urlState: { - ...searchAttr, - [CONSTANTS.timerange]: { - global: { - [CONSTANTS.timerange]: globalTimerange, - linkTo: globalLinkTo, - }, - timeline: { - [CONSTANTS.timerange]: timelineTimerange, - linkTo: timelineLinkTo, - }, - }, - [CONSTANTS.timeline]: timeline, - }, - }; - }; - - return mapStateToProps; -}; - -export const updateTimerangeUrl = ( - timeRange: UrlInputsModel, - isInitializing: boolean -): UrlInputsModel => { - if (timeRange.global.timerange.kind === 'relative') { - timeRange.global.timerange.from = formatDate(timeRange.global.timerange.fromStr); - timeRange.global.timerange.to = formatDate(timeRange.global.timerange.toStr, { roundUp: true }); - } - if (timeRange.timeline.timerange.kind === 'relative' && isInitializing) { - timeRange.timeline.timerange.from = formatDate(timeRange.timeline.timerange.fromStr); - timeRange.timeline.timerange.to = formatDate(timeRange.timeline.timerange.toStr, { - roundUp: true, - }); - } - return timeRange; -}; - -export const updateUrlStateString = ({ - isInitializing, - history, - newUrlStateString, - pathName, - search, - updateTimerange, - urlKey, -}: UpdateUrlStateString): string => { - if (urlKey === CONSTANTS.appQuery) { - const queryState = decodeRisonUrlState(newUrlStateString); - if (queryState != null && queryState.query === '') { - return replaceStateInLocation({ - history, - pathName, - search, - urlStateToReplace: '', - urlStateKey: urlKey, - }); - } - } else if (urlKey === CONSTANTS.timerange && updateTimerange) { - const queryState = decodeRisonUrlState(newUrlStateString); - if (queryState != null && queryState.global != null) { - return replaceStateInLocation({ - history, - pathName, - search, - urlStateToReplace: updateTimerangeUrl(queryState, isInitializing), - urlStateKey: urlKey, - }); - } - } else if (urlKey === CONSTANTS.filters) { - const queryState = decodeRisonUrlState(newUrlStateString); - if (isEmpty(queryState)) { - return replaceStateInLocation({ - history, - pathName, - search, - urlStateToReplace: '', - urlStateKey: urlKey, - }); - } - } else if (urlKey === CONSTANTS.timeline) { - const queryState = decodeRisonUrlState(newUrlStateString); - if (queryState != null && queryState.id === '') { - return replaceStateInLocation({ - history, - pathName, - search, - urlStateToReplace: '', - urlStateKey: urlKey, - }); - } - } - return search; -}; - -export const replaceStateInLocation = ({ - history, - urlStateToReplace, - urlStateKey, - pathName, - search, -}: ReplaceStateInLocation) => { - const newLocation = replaceQueryStringInLocation( - { - hash: '', - pathname: pathName, - search, - state: '', - }, - replaceStateKeyInQueryString(urlStateKey, urlStateToReplace)(getQueryStringFromLocation(search)) - ); - if (history) { - history.replace(newLocation); - } - return newLocation.search; -}; diff --git a/x-pack/plugins/siem/public/components/url_state/index.test.tsx b/x-pack/plugins/siem/public/components/url_state/index.test.tsx deleted file mode 100644 index 4d2a717153894e..00000000000000 --- a/x-pack/plugins/siem/public/components/url_state/index.test.tsx +++ /dev/null @@ -1,221 +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 { mount } from 'enzyme'; -import React from 'react'; - -import { HookWrapper } from '../../mock'; -import { SiemPageName } from '../../pages/home/types'; -import { RouteSpyState } from '../../utils/route/types'; -import { CONSTANTS } from './constants'; -import { - getMockPropsObj, - mockHistory, - mockSetFilterQuery, - mockSetAbsoluteRangeDatePicker, - mockSetRelativeRangeDatePicker, - testCases, -} from './test_dependencies'; -import { UrlStateContainerPropTypes } from './types'; -import { useUrlStateHooks } from './use_url_state'; -import { wait } from '../../lib/helpers'; - -let mockProps: UrlStateContainerPropTypes; - -const mockRouteSpy: RouteSpyState = { - pageName: SiemPageName.network, - detailName: undefined, - tabName: undefined, - search: '', - pathName: '/network', -}; -jest.mock('../../utils/route/use_route_spy', () => ({ - useRouteSpy: () => [mockRouteSpy], -})); - -jest.mock('../super_date_picker', () => ({ - formatDate: (date: string) => { - return 11223344556677; - }, -})); - -jest.mock('../../lib/kibana', () => ({ - useKibana: () => ({ - services: { - data: { - query: { - filterManager: {}, - savedQueries: {}, - }, - }, - }, - }), -})); - -describe('UrlStateContainer', () => { - afterEach(() => { - jest.resetAllMocks(); - }); - describe('handleInitialize', () => { - describe('URL state updates redux', () => { - describe('relative timerange actions are called with correct data on component mount', () => { - test.each(testCases)( - '%o', - (page, namespaceLower, namespaceUpper, examplePath, type, pageName, detailName) => { - mockProps = getMockPropsObj({ - page, - examplePath, - namespaceLower, - pageName, - detailName, - }).relativeTimeSearch.undefinedQuery; - mount( useUrlStateHooks(args)} />); - - expect(mockSetRelativeRangeDatePicker.mock.calls[1][0]).toEqual({ - from: 11223344556677, - fromStr: 'now-1d/d', - kind: 'relative', - to: 11223344556677, - toStr: 'now-1d/d', - id: 'global', - }); - - expect(mockSetRelativeRangeDatePicker.mock.calls[0][0]).toEqual({ - from: 11223344556677, - fromStr: 'now-15m', - kind: 'relative', - to: 11223344556677, - toStr: 'now', - id: 'timeline', - }); - } - ); - }); - - describe('absolute timerange actions are called with correct data on component mount', () => { - test.each(testCases)( - '%o', - (page, namespaceLower, namespaceUpper, examplePath, type, pageName, detailName) => { - mockProps = getMockPropsObj({ page, examplePath, namespaceLower, pageName, detailName }) - .absoluteTimeSearch.undefinedQuery; - mount( useUrlStateHooks(args)} />); - - expect(mockSetAbsoluteRangeDatePicker.mock.calls[1][0]).toEqual({ - from: 1556736012685, - kind: 'absolute', - to: 1556822416082, - id: 'global', - }); - - expect(mockSetAbsoluteRangeDatePicker.mock.calls[0][0]).toEqual({ - from: 1556736012685, - kind: 'absolute', - to: 1556822416082, - id: 'timeline', - }); - } - ); - }); - - describe('appQuery action is called with correct data on component mount', () => { - test.each(testCases.slice(0, 4))( - ' %o', - (page, namespaceLower, namespaceUpper, examplePath, type, pageName, detailName) => { - mockProps = getMockPropsObj({ page, examplePath, namespaceLower, pageName, detailName }) - .relativeTimeSearch.undefinedQuery; - mount( useUrlStateHooks(args)} />); - - expect(mockSetFilterQuery.mock.calls[0][0]).toEqual({ - id: 'global', - language: 'kuery', - query: 'host.name:"siem-es"', - }); - } - ); - }); - }); - - describe('Redux updates URL state', () => { - describe('appQuery url state is set from redux data on component mount', () => { - test.each(testCases)( - '%o', - (page, namespaceLower, namespaceUpper, examplePath, type, pageName, detailName) => { - mockProps = getMockPropsObj({ - page, - examplePath, - namespaceLower, - pageName, - detailName, - }).noSearch.definedQuery; - mount( useUrlStateHooks(args)} />); - - expect( - mockHistory.replace.mock.calls[mockHistory.replace.mock.calls.length - 1][0] - ).toEqual({ - hash: '', - pathname: examplePath, - search: `?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)),timeline:(linkTo:!(global),timerange:(from:1558048243696,fromStr:now-24h,kind:relative,to:1558134643697,toStr:now)))`, - state: '', - }); - } - ); - }); - }); - }); - - describe('After Initialization, keep Relative Date up to date for global only on detections page', () => { - test.each(testCases)( - '%o', - async (page, namespaceLower, namespaceUpper, examplePath, type, pageName, detailName) => { - mockProps = getMockPropsObj({ - page, - examplePath, - namespaceLower, - pageName, - detailName, - }).relativeTimeSearch.undefinedQuery; - const wrapper = mount( - useUrlStateHooks(args)} /> - ); - - wrapper.setProps({ - hookProps: getMockPropsObj({ - page: CONSTANTS.hostsPage, - examplePath: '/hosts', - namespaceLower: 'hosts', - pageName: SiemPageName.hosts, - detailName: undefined, - }).relativeTimeSearch.undefinedQuery, - }); - wrapper.update(); - await wait(); - - if (CONSTANTS.detectionsPage === page) { - expect(mockSetRelativeRangeDatePicker.mock.calls[3][0]).toEqual({ - from: 11223344556677, - fromStr: 'now-1d/d', - kind: 'relative', - to: 11223344556677, - toStr: 'now-1d/d', - id: 'global', - }); - - expect(mockSetRelativeRangeDatePicker.mock.calls[2][0]).toEqual({ - from: 1558732849370, - fromStr: 'now-15m', - kind: 'relative', - to: 1558733749370, - toStr: 'now', - id: 'timeline', - }); - } else { - // There is no change in url state, so that's expected we only have two actions - expect(mockSetRelativeRangeDatePicker.mock.calls.length).toEqual(2); - } - } - ); - }); -}); diff --git a/x-pack/plugins/siem/public/components/url_state/index.tsx b/x-pack/plugins/siem/public/components/url_state/index.tsx deleted file mode 100644 index 294e41a1faa7b4..00000000000000 --- a/x-pack/plugins/siem/public/components/url_state/index.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. - */ - -import React from 'react'; -import { compose, Dispatch } from 'redux'; -import { connect } from 'react-redux'; -import deepEqual from 'fast-deep-equal'; - -import { timelineActions } from '../../store/actions'; -import { RouteSpyState } from '../../utils/route/types'; -import { useRouteSpy } from '../../utils/route/use_route_spy'; - -import { UrlStateContainerPropTypes, UrlStateProps } from './types'; -import { useUrlStateHooks } from './use_url_state'; -import { dispatchUpdateTimeline } from '../open_timeline/helpers'; -import { dispatchSetInitialStateFromUrl } from './initialize_redux_by_url'; -import { makeMapStateToProps } from './helpers'; - -export const UrlStateContainer: React.FC = ( - props: UrlStateContainerPropTypes -) => { - useUrlStateHooks(props); - return null; -}; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - setInitialStateFromUrl: dispatchSetInitialStateFromUrl(dispatch), - updateTimeline: dispatchUpdateTimeline(dispatch), - updateTimelineIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => - dispatch(timelineActions.updateIsLoading({ id, isLoading })), -}); - -export const UrlStateRedux = compose>( - connect(makeMapStateToProps, mapDispatchToProps) -)( - React.memo( - UrlStateContainer, - (prevProps, nextProps) => - prevProps.pathName === nextProps.pathName && deepEqual(prevProps.urlState, nextProps.urlState) - ) -); - -const UseUrlStateComponent: React.FC = props => { - const [routeProps] = useRouteSpy(); - const urlStateReduxProps: RouteSpyState & UrlStateProps = { - ...routeProps, - ...props, - }; - return ; -}; - -export const UseUrlState = React.memo(UseUrlStateComponent); diff --git a/x-pack/plugins/siem/public/components/url_state/types.ts b/x-pack/plugins/siem/public/components/url_state/types.ts deleted file mode 100644 index 9d8a4a8e6a9087..00000000000000 --- a/x-pack/plugins/siem/public/components/url_state/types.ts +++ /dev/null @@ -1,177 +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 ApolloClient from 'apollo-client'; -import * as H from 'history'; -import { ActionCreator } from 'typescript-fsa'; -import { - IIndexPattern, - Query, - Filter, - FilterManager, - SavedQueryService, -} from 'src/plugins/data/public'; - -import { UrlInputsModel } from '../../store/inputs/model'; -import { TimelineUrl } from '../../store/timeline/model'; -import { RouteSpyState } from '../../utils/route/types'; -import { DispatchUpdateTimeline } from '../open_timeline/types'; -import { NavTab } from '../navigation/types'; - -import { CONSTANTS, UrlStateType } from './constants'; - -export const ALL_URL_STATE_KEYS: KeyUrlState[] = [ - CONSTANTS.appQuery, - CONSTANTS.filters, - CONSTANTS.savedQuery, - CONSTANTS.timerange, - CONSTANTS.timeline, -]; - -export const URL_STATE_KEYS: Record = { - detections: [ - CONSTANTS.appQuery, - CONSTANTS.filters, - CONSTANTS.savedQuery, - CONSTANTS.timerange, - CONSTANTS.timeline, - ], - host: [ - CONSTANTS.appQuery, - CONSTANTS.filters, - CONSTANTS.savedQuery, - CONSTANTS.timerange, - CONSTANTS.timeline, - ], - network: [ - CONSTANTS.appQuery, - CONSTANTS.filters, - CONSTANTS.savedQuery, - CONSTANTS.timerange, - CONSTANTS.timeline, - ], - overview: [ - CONSTANTS.appQuery, - CONSTANTS.filters, - CONSTANTS.savedQuery, - CONSTANTS.timerange, - CONSTANTS.timeline, - ], - timeline: [ - CONSTANTS.appQuery, - CONSTANTS.filters, - CONSTANTS.savedQuery, - CONSTANTS.timerange, - CONSTANTS.timeline, - ], - case: [ - CONSTANTS.appQuery, - CONSTANTS.filters, - CONSTANTS.savedQuery, - CONSTANTS.timerange, - CONSTANTS.timeline, - ], -}; - -export type LocationTypes = - | CONSTANTS.caseDetails - | CONSTANTS.casePage - | CONSTANTS.detectionsPage - | CONSTANTS.hostsDetails - | CONSTANTS.hostsPage - | CONSTANTS.networkDetails - | CONSTANTS.networkPage - | CONSTANTS.overviewPage - | CONSTANTS.timelinePage - | CONSTANTS.unknown; - -export interface UrlState { - [CONSTANTS.appQuery]?: Query; - [CONSTANTS.filters]?: Filter[]; - [CONSTANTS.savedQuery]?: string; - [CONSTANTS.timerange]: UrlInputsModel; - [CONSTANTS.timeline]: TimelineUrl; -} -export type KeyUrlState = keyof UrlState; - -export interface UrlStateProps { - navTabs: Record; - indexPattern?: IIndexPattern; - mapToUrlState?: (value: string) => UrlState; - onChange?: (urlState: UrlState, previousUrlState: UrlState) => void; - onInitialize?: (urlState: UrlState) => void; -} - -export interface UrlStateStateToPropsType { - urlState: UrlState; -} - -export interface UpdateTimelineIsLoading { - id: string; - isLoading: boolean; -} - -export interface UrlStateDispatchToPropsType { - setInitialStateFromUrl: DispatchSetInitialStateFromUrl; - updateTimeline: DispatchUpdateTimeline; - updateTimelineIsLoading: ActionCreator; -} - -export type UrlStateContainerPropTypes = RouteSpyState & - UrlStateStateToPropsType & - UrlStateDispatchToPropsType & - UrlStateProps; - -export interface PreviousLocationUrlState { - pathName: string | undefined; - pageName: string | undefined; - urlState: UrlState; -} - -export interface UrlStateToRedux { - urlKey: KeyUrlState; - newUrlStateString: string; -} - -export interface SetInitialStateFromUrl { - apolloClient: ApolloClient | ApolloClient<{}> | undefined; - detailName: string | undefined; - filterManager: FilterManager; - indexPattern: IIndexPattern | undefined; - pageName: string; - savedQueries: SavedQueryService; - updateTimeline: DispatchUpdateTimeline; - updateTimelineIsLoading: ActionCreator; - urlStateToUpdate: UrlStateToRedux[]; -} - -export type DispatchSetInitialStateFromUrl = ({ - apolloClient, - detailName, - indexPattern, - pageName, - updateTimeline, - updateTimelineIsLoading, - urlStateToUpdate, -}: SetInitialStateFromUrl) => () => void; - -export interface ReplaceStateInLocation { - history?: H.History; - urlStateToReplace: T; - urlStateKey: string; - pathName: string; - search: string; -} - -export interface UpdateUrlStateString { - isInitializing: boolean; - history?: H.History; - newUrlStateString: string; - pathName: string; - search: string; - updateTimerange: boolean; - urlKey: KeyUrlState; -} diff --git a/x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts b/x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/histogram_configs.ts deleted file mode 100644 index f63349d3e573ad..00000000000000 --- a/x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/histogram_configs.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 * as i18n from './translations'; -import { - MatrixHistogramOption, - MatrixHisrogramConfigs, -} from '../../../components/matrix_histogram/types'; -import { HistogramType } from '../../../graphql/types'; - -export const anomaliesStackByOptions: MatrixHistogramOption[] = [ - { - text: i18n.ANOMALIES_STACK_BY_JOB_ID, - value: 'job_id', - }, -]; - -const DEFAULT_STACK_BY = i18n.ANOMALIES_STACK_BY_JOB_ID; - -export const histogramConfigs: MatrixHisrogramConfigs = { - defaultStackByOption: - anomaliesStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? anomaliesStackByOptions[0], - errorMessage: i18n.ERROR_FETCHING_ANOMALIES_DATA, - hideHistogramIfEmpty: true, - histogramType: HistogramType.anomalies, - stackByOptions: anomaliesStackByOptions, - subtitle: undefined, - title: i18n.ANOMALIES_TITLE, -}; diff --git a/x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx b/x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx deleted file mode 100644 index 2bbb4cde92b151..00000000000000 --- a/x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/index.tsx +++ /dev/null @@ -1,77 +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, { useEffect } from 'react'; - -import { DEFAULT_ANOMALY_SCORE } from '../../../../common/constants'; -import { AnomaliesQueryTabBodyProps } from './types'; -import { getAnomaliesFilterQuery } from './utils'; -import { useSiemJobs } from '../../../components/ml_popover/hooks/use_siem_jobs'; -import { useUiSetting$ } from '../../../lib/kibana'; -import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; -import { histogramConfigs } from './histogram_configs'; -const ID = 'anomaliesOverTimeQuery'; - -export const AnomaliesQueryTabBody = ({ - deleteQuery, - endDate, - setQuery, - skip, - startDate, - type, - narrowDateRange, - filterQuery, - anomaliesFilterQuery, - AnomaliesTableComponent, - flowTarget, - ip, -}: AnomaliesQueryTabBodyProps) => { - useEffect(() => { - return () => { - if (deleteQuery) { - deleteQuery({ id: ID }); - } - }; - }, []); - - const [, siemJobs] = useSiemJobs(true); - const [anomalyScore] = useUiSetting$(DEFAULT_ANOMALY_SCORE); - - const mergedFilterQuery = getAnomaliesFilterQuery( - filterQuery, - anomaliesFilterQuery, - siemJobs, - anomalyScore, - flowTarget, - ip - ); - - return ( - <> - - - - ); -}; - -AnomaliesQueryTabBody.displayName = 'AnomaliesQueryTabBody'; diff --git a/x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/types.ts b/x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/types.ts deleted file mode 100644 index f6cae81e3c6c4b..00000000000000 --- a/x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/types.ts +++ /dev/null @@ -1,35 +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 { ESTermQuery } from '../../../../common/typed_json'; -import { NarrowDateRange } from '../../../components/ml/types'; -import { UpdateDateRange } from '../../../components/charts/common'; -import { SetQuery } from '../../../pages/hosts/navigation/types'; -import { FlowTarget } from '../../../graphql/types'; -import { HostsType } from '../../../store/hosts/model'; -import { NetworkType } from '../../../store/network/model'; -import { AnomaliesHostTable } from '../../../components/ml/tables/anomalies_host_table'; -import { AnomaliesNetworkTable } from '../../../components/ml/tables/anomalies_network_table'; - -interface QueryTabBodyProps { - type: HostsType | NetworkType; - filterQuery?: string | ESTermQuery; -} - -export type AnomaliesQueryTabBodyProps = QueryTabBodyProps & { - anomaliesFilterQuery?: object; - AnomaliesTableComponent: typeof AnomaliesHostTable | typeof AnomaliesNetworkTable; - deleteQuery?: ({ id }: { id: string }) => void; - endDate: number; - flowTarget?: FlowTarget; - narrowDateRange: NarrowDateRange; - setQuery: SetQuery; - startDate: number; - skip: boolean; - updateDateRange?: UpdateDateRange; - hideHistogramIfEmpty?: boolean; - ip?: string; -}; diff --git a/x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/utils.ts b/x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/utils.ts deleted file mode 100644 index 790a797b2fead4..00000000000000 --- a/x-pack/plugins/siem/public/containers/anomalies/anomalies_query_tab_body/utils.ts +++ /dev/null @@ -1,69 +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 deepmerge from 'deepmerge'; - -import { ESTermQuery } from '../../../../common/typed_json'; -import { createFilter } from '../../helpers'; -import { SiemJob } from '../../../components/ml_popover/types'; -import { FlowTarget } from '../../../graphql/types'; - -export const getAnomaliesFilterQuery = ( - filterQuery: string | ESTermQuery | undefined, - anomaliesFilterQuery: object = {}, - siemJobs: SiemJob[] = [], - anomalyScore: number, - flowTarget?: FlowTarget, - ip?: string -): string => { - const siemJobIds = siemJobs - .filter(job => job.isInstalled) - .map(job => job.id) - .map(jobId => ({ - match_phrase: { - job_id: jobId, - }, - })); - - const filterQueryString = createFilter(filterQuery); - const filterQueryObject = filterQueryString ? JSON.parse(filterQueryString) : {}; - const mergedFilterQuery = deepmerge.all([ - filterQueryObject, - anomaliesFilterQuery, - { - bool: { - filter: [ - { - bool: { - should: siemJobIds, - minimum_should_match: 1, - }, - }, - { - match_phrase: { - result_type: 'record', - }, - }, - flowTarget && - ip && { - match_phrase: { - [`${flowTarget}.ip`]: ip, - }, - }, - { - range: { - record_score: { - gte: anomalyScore, - }, - }, - }, - ], - }, - }, - ]); - - return JSON.stringify(mergedFilterQuery); -}; diff --git a/x-pack/plugins/siem/public/containers/authentications/index.tsx b/x-pack/plugins/siem/public/containers/authentications/index.tsx deleted file mode 100644 index 6d4a88c45a7681..00000000000000 --- a/x-pack/plugins/siem/public/containers/authentications/index.tsx +++ /dev/null @@ -1,150 +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 { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; - -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; -import { - AuthenticationsEdges, - GetAuthenticationsQuery, - PageInfoPaginated, -} from '../../graphql/types'; -import { hostsModel, hostsSelectors, inputsModel, State, inputsSelectors } from '../../store'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; -import { withKibana, WithKibanaProps } from '../../lib/kibana'; -import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; - -import { authenticationsQuery } from './index.gql_query'; - -const ID = 'authenticationQuery'; - -export interface AuthenticationArgs { - authentications: AuthenticationsEdges[]; - id: string; - inspect: inputsModel.InspectQuery; - isInspected: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - pageInfo: PageInfoPaginated; - refetch: inputsModel.Refetch; - totalCount: number; -} - -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: AuthenticationArgs) => React.ReactNode; - type: hostsModel.HostsType; -} - -export interface AuthenticationsComponentReduxProps { - activePage: number; - isInspected: boolean; - limit: number; -} - -type AuthenticationsProps = OwnProps & AuthenticationsComponentReduxProps & WithKibanaProps; - -class AuthenticationsComponentQuery extends QueryTemplatePaginated< - AuthenticationsProps, - GetAuthenticationsQuery.Query, - GetAuthenticationsQuery.Variables -> { - public render() { - const { - activePage, - children, - endDate, - filterQuery, - id = ID, - isInspected, - kibana, - limit, - skip, - sourceId, - startDate, - } = this.props; - const variables: GetAuthenticationsQuery.Variables = { - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - pagination: generateTablePaginationOptions(activePage, limit), - filterQuery: createFilter(filterQuery), - defaultIndex: kibana.services.uiSettings.get(DEFAULT_INDEX_KEY), - inspect: isInspected, - }; - return ( - - query={authenticationsQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const authentications = getOr([], 'source.Authentications.edges', data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), - }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; - } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - Authentications: { - ...fetchMoreResult.source.Authentications, - edges: [...fetchMoreResult.source.Authentications.edges], - }, - }, - }; - }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - authentications, - id, - inspect: getOr(null, 'source.Authentications.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - pageInfo: getOr({}, 'source.Authentications.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - totalCount: getOr(-1, 'source.Authentications.totalCount', data), - }); - }} - - ); - } -} - -const makeMapStateToProps = () => { - const getAuthenticationsSelector = hostsSelectors.authenticationsSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { type, id = ID }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getAuthenticationsSelector(state, type), - isInspected, - }; - }; - return mapStateToProps; -}; - -export const AuthenticationsQuery = compose>( - connect(makeMapStateToProps), - withKibana -)(AuthenticationsComponentQuery); diff --git a/x-pack/plugins/siem/public/containers/case/api.ts b/x-pack/plugins/siem/public/containers/case/api.ts deleted file mode 100644 index 438eae9d88a448..00000000000000 --- a/x-pack/plugins/siem/public/containers/case/api.ts +++ /dev/null @@ -1,268 +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 { - CaseResponse, - CasesResponse, - CasesFindResponse, - CasePatchRequest, - CasePostRequest, - CasesStatusResponse, - CommentRequest, - User, - CaseUserActionsResponse, - CaseExternalServiceRequest, - ServiceConnectorCaseParams, - ServiceConnectorCaseResponse, - ActionTypeExecutorResult, -} from '../../../../case/common/api'; - -import { - CASE_STATUS_URL, - CASES_URL, - CASE_TAGS_URL, - CASE_REPORTERS_URL, - ACTION_TYPES_URL, - ACTION_URL, -} from '../../../../case/common/constants'; - -import { - getCaseDetailsUrl, - getCaseUserActionUrl, - getCaseCommentsUrl, -} from '../../../../case/common/api/helpers'; - -import { KibanaServices } from '../../lib/kibana'; - -import { - ActionLicense, - AllCases, - BulkUpdateStatus, - Case, - CasesStatus, - FetchCasesProps, - SortFieldCase, - CaseUserActions, -} from './types'; - -import { - convertToCamelCase, - convertAllCasesToCamel, - convertArrayToCamelCase, - decodeCaseResponse, - decodeCasesResponse, - decodeCasesFindResponse, - decodeCasesStatusResponse, - decodeCaseUserActionsResponse, - decodeServiceConnectorCaseResponse, -} from './utils'; - -import * as i18n from './translations'; - -export const getCase = async ( - caseId: string, - includeComments: boolean = true, - signal: AbortSignal -): Promise => { - const response = await KibanaServices.get().http.fetch(getCaseDetailsUrl(caseId), { - method: 'GET', - query: { - includeComments, - }, - signal, - }); - return convertToCamelCase(decodeCaseResponse(response)); -}; - -export const getCasesStatus = async (signal: AbortSignal): Promise => { - const response = await KibanaServices.get().http.fetch(CASE_STATUS_URL, { - method: 'GET', - signal, - }); - return convertToCamelCase(decodeCasesStatusResponse(response)); -}; - -export const getTags = async (signal: AbortSignal): Promise => { - const response = await KibanaServices.get().http.fetch(CASE_TAGS_URL, { - method: 'GET', - signal, - }); - return response ?? []; -}; - -export const getReporters = async (signal: AbortSignal): Promise => { - const response = await KibanaServices.get().http.fetch(CASE_REPORTERS_URL, { - method: 'GET', - signal, - }); - return response ?? []; -}; - -export const getCaseUserActions = async ( - caseId: string, - signal: AbortSignal -): Promise => { - const response = await KibanaServices.get().http.fetch( - getCaseUserActionUrl(caseId), - { - method: 'GET', - signal, - } - ); - return convertArrayToCamelCase(decodeCaseUserActionsResponse(response)) as CaseUserActions[]; -}; - -export const getCases = async ({ - filterOptions = { - search: '', - reporters: [], - status: 'open', - tags: [], - }, - queryParams = { - page: 1, - perPage: 20, - sortField: SortFieldCase.createdAt, - sortOrder: 'desc', - }, - signal, -}: FetchCasesProps): Promise => { - const query = { - reporters: filterOptions.reporters.map(r => r.username ?? '').filter(r => r !== ''), - tags: filterOptions.tags, - ...(filterOptions.status !== '' ? { status: filterOptions.status } : {}), - ...(filterOptions.search.length > 0 ? { search: filterOptions.search } : {}), - ...queryParams, - }; - const response = await KibanaServices.get().http.fetch(`${CASES_URL}/_find`, { - method: 'GET', - query, - signal, - }); - return convertAllCasesToCamel(decodeCasesFindResponse(response)); -}; - -export const postCase = async (newCase: CasePostRequest, signal: AbortSignal): Promise => { - const response = await KibanaServices.get().http.fetch(CASES_URL, { - method: 'POST', - body: JSON.stringify(newCase), - signal, - }); - return convertToCamelCase(decodeCaseResponse(response)); -}; - -export const patchCase = async ( - caseId: string, - updatedCase: Pick, - version: string, - signal: AbortSignal -): Promise => { - const response = await KibanaServices.get().http.fetch(CASES_URL, { - method: 'PATCH', - body: JSON.stringify({ cases: [{ ...updatedCase, id: caseId, version }] }), - signal, - }); - return convertToCamelCase(decodeCasesResponse(response)); -}; - -export const patchCasesStatus = async ( - cases: BulkUpdateStatus[], - signal: AbortSignal -): Promise => { - const response = await KibanaServices.get().http.fetch(CASES_URL, { - method: 'PATCH', - body: JSON.stringify({ cases }), - signal, - }); - return convertToCamelCase(decodeCasesResponse(response)); -}; - -export const postComment = async ( - newComment: CommentRequest, - caseId: string, - signal: AbortSignal -): Promise => { - const response = await KibanaServices.get().http.fetch( - `${CASES_URL}/${caseId}/comments`, - { - method: 'POST', - body: JSON.stringify(newComment), - signal, - } - ); - return convertToCamelCase(decodeCaseResponse(response)); -}; - -export const patchComment = async ( - caseId: string, - commentId: string, - commentUpdate: string, - version: string, - signal: AbortSignal -): Promise => { - const response = await KibanaServices.get().http.fetch(getCaseCommentsUrl(caseId), { - method: 'PATCH', - body: JSON.stringify({ comment: commentUpdate, id: commentId, version }), - signal, - }); - return convertToCamelCase(decodeCaseResponse(response)); -}; - -export const deleteCases = async (caseIds: string[], signal: AbortSignal): Promise => { - const response = await KibanaServices.get().http.fetch(CASES_URL, { - method: 'DELETE', - query: { ids: JSON.stringify(caseIds) }, - signal, - }); - return response; -}; - -export const pushCase = async ( - caseId: string, - push: CaseExternalServiceRequest, - signal: AbortSignal -): Promise => { - const response = await KibanaServices.get().http.fetch( - `${getCaseDetailsUrl(caseId)}/_push`, - { - method: 'POST', - body: JSON.stringify(push), - signal, - } - ); - return convertToCamelCase(decodeCaseResponse(response)); -}; - -export const pushToService = async ( - connectorId: string, - casePushParams: ServiceConnectorCaseParams, - signal: AbortSignal -): Promise => { - const response = await KibanaServices.get().http.fetch( - `${ACTION_URL}/${connectorId}/_execute`, - { - method: 'POST', - body: JSON.stringify({ - params: { subAction: 'pushToService', subActionParams: casePushParams }, - }), - signal, - } - ); - - if (response.status === 'error') { - throw new Error(response.serviceMessage ?? response.message ?? i18n.ERROR_PUSH_TO_SERVICE); - } - - return decodeServiceConnectorCaseResponse(response.data); -}; - -export const getActionLicense = async (signal: AbortSignal): Promise => { - const response = await KibanaServices.get().http.fetch(ACTION_TYPES_URL, { - method: 'GET', - signal, - }); - return response; -}; diff --git a/x-pack/plugins/siem/public/containers/case/configure/api.test.ts b/x-pack/plugins/siem/public/containers/case/configure/api.test.ts deleted file mode 100644 index ef0e51fb1c24db..00000000000000 --- a/x-pack/plugins/siem/public/containers/case/configure/api.test.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 { KibanaServices } from '../../../lib/kibana'; -import { fetchConnectors, getCaseConfigure, postCaseConfigure, patchCaseConfigure } from './api'; -import { - connectorsMock, - caseConfigurationMock, - caseConfigurationResposeMock, - caseConfigurationCamelCaseResponseMock, -} from './mock'; - -const abortCtrl = new AbortController(); -const mockKibanaServices = KibanaServices.get as jest.Mock; -jest.mock('../../../lib/kibana'); - -const fetchMock = jest.fn(); -mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } }); - -describe('Case Configuration API', () => { - describe('fetch connectors', () => { - beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(connectorsMock); - }); - - test('check url, method, signal', async () => { - await fetchConnectors({ signal: abortCtrl.signal }); - expect(fetchMock).toHaveBeenCalledWith('/api/cases/configure/connectors/_find', { - method: 'GET', - signal: abortCtrl.signal, - }); - }); - - test('happy path', async () => { - const resp = await fetchConnectors({ signal: abortCtrl.signal }); - expect(resp).toEqual(connectorsMock); - }); - }); - - describe('fetch configuration', () => { - beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(caseConfigurationResposeMock); - }); - - test('check url, method, signal', async () => { - await getCaseConfigure({ signal: abortCtrl.signal }); - expect(fetchMock).toHaveBeenCalledWith('/api/cases/configure', { - method: 'GET', - signal: abortCtrl.signal, - }); - }); - - test('happy path', async () => { - const resp = await getCaseConfigure({ signal: abortCtrl.signal }); - expect(resp).toEqual(caseConfigurationCamelCaseResponseMock); - }); - - test('return null on empty response', async () => { - fetchMock.mockResolvedValue({}); - const resp = await getCaseConfigure({ signal: abortCtrl.signal }); - expect(resp).toBe(null); - }); - }); - - describe('create configuration', () => { - beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(caseConfigurationResposeMock); - }); - - test('check url, body, method, signal', async () => { - await postCaseConfigure(caseConfigurationMock, abortCtrl.signal); - expect(fetchMock).toHaveBeenCalledWith('/api/cases/configure', { - body: - '{"connector_id":"123","connector_name":"My Connector","closure_type":"close-by-user"}', - method: 'POST', - signal: abortCtrl.signal, - }); - }); - - test('happy path', async () => { - const resp = await postCaseConfigure(caseConfigurationMock, abortCtrl.signal); - expect(resp).toEqual(caseConfigurationCamelCaseResponseMock); - }); - }); - - describe('update configuration', () => { - beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(caseConfigurationResposeMock); - }); - - test('check url, body, method, signal', async () => { - await patchCaseConfigure({ connector_id: '456', version: 'WzHJ12' }, abortCtrl.signal); - expect(fetchMock).toHaveBeenCalledWith('/api/cases/configure', { - body: '{"connector_id":"456","version":"WzHJ12"}', - method: 'PATCH', - signal: abortCtrl.signal, - }); - }); - - test('happy path', async () => { - const resp = await patchCaseConfigure( - { connector_id: '456', version: 'WzHJ12' }, - abortCtrl.signal - ); - expect(resp).toEqual(caseConfigurationCamelCaseResponseMock); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/containers/case/configure/api.ts b/x-pack/plugins/siem/public/containers/case/configure/api.ts deleted file mode 100644 index 4f516764e46f3d..00000000000000 --- a/x-pack/plugins/siem/public/containers/case/configure/api.ts +++ /dev/null @@ -1,82 +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 { isEmpty } from 'lodash/fp'; -import { - Connector, - CasesConfigurePatch, - CasesConfigureResponse, - CasesConfigureRequest, -} from '../../../../../case/common/api'; -import { KibanaServices } from '../../../lib/kibana'; - -import { - CASE_CONFIGURE_CONNECTORS_URL, - CASE_CONFIGURE_URL, -} from '../../../../../case/common/constants'; - -import { ApiProps } from '../types'; -import { convertToCamelCase, decodeCaseConfigureResponse } from '../utils'; -import { CaseConfigure } from './types'; - -export const fetchConnectors = async ({ signal }: ApiProps): Promise => { - const response = await KibanaServices.get().http.fetch(`${CASE_CONFIGURE_CONNECTORS_URL}/_find`, { - method: 'GET', - signal, - }); - - return response; -}; - -export const getCaseConfigure = async ({ signal }: ApiProps): Promise => { - const response = await KibanaServices.get().http.fetch( - CASE_CONFIGURE_URL, - { - method: 'GET', - signal, - } - ); - - return !isEmpty(response) - ? convertToCamelCase( - decodeCaseConfigureResponse(response) - ) - : null; -}; - -export const postCaseConfigure = async ( - caseConfiguration: CasesConfigureRequest, - signal: AbortSignal -): Promise => { - const response = await KibanaServices.get().http.fetch( - CASE_CONFIGURE_URL, - { - method: 'POST', - body: JSON.stringify(caseConfiguration), - signal, - } - ); - return convertToCamelCase( - decodeCaseConfigureResponse(response) - ); -}; - -export const patchCaseConfigure = async ( - caseConfiguration: CasesConfigurePatch, - signal: AbortSignal -): Promise => { - const response = await KibanaServices.get().http.fetch( - CASE_CONFIGURE_URL, - { - method: 'PATCH', - body: JSON.stringify(caseConfiguration), - signal, - } - ); - return convertToCamelCase( - decodeCaseConfigureResponse(response) - ); -}; diff --git a/x-pack/plugins/siem/public/containers/case/utils.ts b/x-pack/plugins/siem/public/containers/case/utils.ts deleted file mode 100644 index 15e514d6ea8b30..00000000000000 --- a/x-pack/plugins/siem/public/containers/case/utils.ts +++ /dev/null @@ -1,107 +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 { camelCase, isArray, isObject, set } from 'lodash'; -import { fold } from 'fp-ts/lib/Either'; -import { identity } from 'fp-ts/lib/function'; -import { pipe } from 'fp-ts/lib/pipeable'; - -import { - CasesFindResponse, - CasesFindResponseRt, - CaseResponse, - CaseResponseRt, - CasesResponse, - CasesResponseRt, - CasesStatusResponseRt, - CasesStatusResponse, - throwErrors, - CasesConfigureResponse, - CaseConfigureResponseRt, - CaseUserActionsResponse, - CaseUserActionsResponseRt, - ServiceConnectorCaseResponseRt, - ServiceConnectorCaseResponse, -} from '../../../../case/common/api'; -import { ToasterError } from '../../components/toasters'; -import { AllCases, Case } from './types'; - -export const getTypedPayload = (a: unknown): T => a as T; - -export const parseString = (params: string) => { - try { - return JSON.parse(params); - } catch { - return null; - } -}; - -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: CasesFindResponse): AllCases => ({ - cases: snakeCases.cases.map(snakeCase => convertToCamelCase(snakeCase)), - countClosedCases: snakeCases.count_closed_cases, - countOpenCases: snakeCases.count_open_cases, - page: snakeCases.page, - perPage: snakeCases.per_page, - total: snakeCases.total, -}); - -export const decodeCasesStatusResponse = (respCase?: CasesStatusResponse) => - pipe( - CasesStatusResponseRt.decode(respCase), - fold(throwErrors(createToasterPlainError), identity) - ); - -export const createToasterPlainError = (message: string) => new ToasterError([message]); - -export const decodeCaseResponse = (respCase?: CaseResponse) => - pipe(CaseResponseRt.decode(respCase), fold(throwErrors(createToasterPlainError), identity)); - -export const decodeCasesResponse = (respCase?: CasesResponse) => - pipe(CasesResponseRt.decode(respCase), fold(throwErrors(createToasterPlainError), identity)); - -export const decodeCasesFindResponse = (respCases?: CasesFindResponse) => - pipe(CasesFindResponseRt.decode(respCases), fold(throwErrors(createToasterPlainError), identity)); - -export const decodeCaseConfigureResponse = (respCase?: CasesConfigureResponse) => - pipe( - CaseConfigureResponseRt.decode(respCase), - fold(throwErrors(createToasterPlainError), identity) - ); - -export const decodeCaseUserActionsResponse = (respUserActions?: CaseUserActionsResponse) => - pipe( - CaseUserActionsResponseRt.decode(respUserActions), - fold(throwErrors(createToasterPlainError), identity) - ); - -export const decodeServiceConnectorCaseResponse = (respPushCase?: ServiceConnectorCaseResponse) => - pipe( - ServiceConnectorCaseResponseRt.decode(respPushCase), - fold(throwErrors(createToasterPlainError), identity) - ); diff --git a/x-pack/plugins/siem/public/containers/detection_engine/rules/api.test.ts b/x-pack/plugins/siem/public/containers/detection_engine/rules/api.test.ts deleted file mode 100644 index 9eb4acbdb61641..00000000000000 --- a/x-pack/plugins/siem/public/containers/detection_engine/rules/api.test.ts +++ /dev/null @@ -1,559 +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 { KibanaServices } from '../../../lib/kibana'; -import { - addRule, - fetchRules, - fetchRuleById, - enableRules, - deleteRules, - duplicateRules, - createPrepackagedRules, - importRules, - exportRules, - getRuleStatusById, - fetchTags, - getPrePackagedRulesStatus, -} from './api'; -import { ruleMock, rulesMock } from './mock'; - -const abortCtrl = new AbortController(); -const mockKibanaServices = KibanaServices.get as jest.Mock; -jest.mock('../../../lib/kibana'); - -const fetchMock = jest.fn(); -mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } }); - -describe('Detections Rules API', () => { - describe('addRule', () => { - beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(ruleMock); - }); - - test('check parameter url, body', async () => { - await addRule({ rule: ruleMock, signal: abortCtrl.signal }); - expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules', { - body: - '{"description":"some desc","enabled":true,"false_positives":[],"filters":[],"from":"now-360s","index":["apm-*-transaction*","auditbeat-*","endgame-*","filebeat-*","packetbeat-*","winlogbeat-*"],"interval":"5m","rule_id":"bbd3106e-b4b5-4d7c-a1a2-47531d6a2baf","language":"kuery","risk_score":75,"name":"Test rule","query":"user.email: \'root@elastic.co\'","references":[],"severity":"high","tags":["APM"],"to":"now","type":"query","threat":[],"throttle":null}', - method: 'POST', - signal: abortCtrl.signal, - }); - }); - - test('happy path', async () => { - const ruleResp = await addRule({ rule: ruleMock, signal: abortCtrl.signal }); - expect(ruleResp).toEqual(ruleMock); - }); - }); - - describe('fetchRules', () => { - beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(rulesMock); - }); - - test('check parameter url, query without any options', async () => { - await fetchRules({ signal: abortCtrl.signal }); - expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { - method: 'GET', - query: { - page: 1, - per_page: 20, - sort_field: 'enabled', - sort_order: 'desc', - }, - signal: abortCtrl.signal, - }); - }); - - test('check parameter url, query with a filter', async () => { - await fetchRules({ - filterOptions: { - filter: 'hello world', - sortField: 'enabled', - sortOrder: 'desc', - showCustomRules: false, - showElasticRules: false, - tags: [], - }, - signal: abortCtrl.signal, - }); - - expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { - method: 'GET', - query: { - filter: 'alert.attributes.name: hello world', - page: 1, - per_page: 20, - sort_field: 'enabled', - sort_order: 'desc', - }, - signal: abortCtrl.signal, - }); - }); - - test('check parameter url, query with showCustomRules', async () => { - await fetchRules({ - filterOptions: { - filter: '', - sortField: 'enabled', - sortOrder: 'desc', - showCustomRules: true, - showElasticRules: false, - tags: [], - }, - signal: abortCtrl.signal, - }); - - expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { - method: 'GET', - query: { - filter: 'alert.attributes.tags: "__internal_immutable:false"', - page: 1, - per_page: 20, - sort_field: 'enabled', - sort_order: 'desc', - }, - signal: abortCtrl.signal, - }); - }); - - test('check parameter url, query with showElasticRules', async () => { - await fetchRules({ - filterOptions: { - filter: '', - sortField: 'enabled', - sortOrder: 'desc', - showCustomRules: false, - showElasticRules: true, - tags: [], - }, - signal: abortCtrl.signal, - }); - - expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { - method: 'GET', - query: { - filter: 'alert.attributes.tags: "__internal_immutable:true"', - page: 1, - per_page: 20, - sort_field: 'enabled', - sort_order: 'desc', - }, - signal: abortCtrl.signal, - }); - }); - - test('check parameter url, query with tags', async () => { - await fetchRules({ - filterOptions: { - filter: '', - sortField: 'enabled', - sortOrder: 'desc', - showCustomRules: false, - showElasticRules: false, - tags: ['hello', 'world'], - }, - signal: abortCtrl.signal, - }); - - expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { - method: 'GET', - query: { - filter: 'alert.attributes.tags: hello AND alert.attributes.tags: world', - page: 1, - per_page: 20, - sort_field: 'enabled', - sort_order: 'desc', - }, - signal: abortCtrl.signal, - }); - }); - - test('check parameter url, query with all options', async () => { - await fetchRules({ - filterOptions: { - filter: 'ruleName', - sortField: 'enabled', - sortOrder: 'desc', - showCustomRules: true, - showElasticRules: true, - tags: ['hello', 'world'], - }, - signal: abortCtrl.signal, - }); - expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { - method: 'GET', - query: { - filter: - 'alert.attributes.name: ruleName AND alert.attributes.tags: "__internal_immutable:false" AND alert.attributes.tags: "__internal_immutable:true" AND alert.attributes.tags: hello AND alert.attributes.tags: world', - page: 1, - per_page: 20, - sort_field: 'enabled', - sort_order: 'desc', - }, - signal: abortCtrl.signal, - }); - }); - - test('happy path', async () => { - const rulesResp = await fetchRules({ signal: abortCtrl.signal }); - expect(rulesResp).toEqual(rulesMock); - }); - }); - - describe('fetchRuleById', () => { - beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(ruleMock); - }); - - test('check parameter url, query', async () => { - await fetchRuleById({ id: 'mySuperRuleId', signal: abortCtrl.signal }); - expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules', { - query: { - id: 'mySuperRuleId', - }, - method: 'GET', - signal: abortCtrl.signal, - }); - }); - - test('happy path', async () => { - const ruleResp = await fetchRuleById({ id: 'mySuperRuleId', signal: abortCtrl.signal }); - expect(ruleResp).toEqual(ruleMock); - }); - }); - - describe('enableRules', () => { - beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(rulesMock); - }); - - test('check parameter url, body when enabling rules', async () => { - await enableRules({ ids: ['mySuperRuleId', 'mySuperRuleId_II'], enabled: true }); - expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_bulk_update', { - body: '[{"id":"mySuperRuleId","enabled":true},{"id":"mySuperRuleId_II","enabled":true}]', - method: 'PATCH', - }); - }); - test('check parameter url, body when disabling rules', async () => { - await enableRules({ ids: ['mySuperRuleId', 'mySuperRuleId_II'], enabled: false }); - expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_bulk_update', { - body: '[{"id":"mySuperRuleId","enabled":false},{"id":"mySuperRuleId_II","enabled":false}]', - method: 'PATCH', - }); - }); - test('happy path', async () => { - const ruleResp = await enableRules({ - ids: ['mySuperRuleId', 'mySuperRuleId_II'], - enabled: true, - }); - expect(ruleResp).toEqual(rulesMock); - }); - }); - - describe('deleteRules', () => { - beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(rulesMock); - }); - - test('check parameter url, body when deleting rules', async () => { - await deleteRules({ ids: ['mySuperRuleId', 'mySuperRuleId_II'] }); - expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_bulk_delete', { - body: '[{"id":"mySuperRuleId"},{"id":"mySuperRuleId_II"}]', - method: 'DELETE', - }); - }); - - test('happy path', async () => { - const ruleResp = await deleteRules({ - ids: ['mySuperRuleId', 'mySuperRuleId_II'], - }); - expect(ruleResp).toEqual(rulesMock); - }); - }); - - describe('duplicateRules', () => { - beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(rulesMock); - }); - - test('check parameter url, body when duplicating rules', async () => { - await duplicateRules({ rules: rulesMock.data }); - expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_bulk_create', { - body: - '[{"actions":[],"description":"Elastic Endpoint detected Credential Dumping. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.","enabled":false,"false_positives":[],"from":"now-660s","index":["endgame-*"],"interval":"10m","language":"kuery","output_index":".siem-signals-default","max_signals":100,"risk_score":73,"name":"Credential Dumping - Detected - Elastic Endpoint [Duplicate]","query":"event.kind:alert and event.module:endgame and event.action:cred_theft_event and endgame.metadata.type:detection","filters":[],"references":[],"severity":"high","tags":["Elastic","Endpoint"],"to":"now","type":"query","threat":[],"throttle":null,"version":1},{"actions":[],"description":"Elastic Endpoint detected an Adversary Behavior. Click the Elastic Endpoint icon in the event.module column or the link in the rule.reference column in the External Alerts tab of the SIEM Detections page for additional information.","enabled":false,"false_positives":[],"from":"now-660s","index":["endgame-*"],"interval":"10m","language":"kuery","output_index":".siem-signals-default","max_signals":100,"risk_score":47,"name":"Adversary Behavior - Detected - Elastic Endpoint [Duplicate]","query":"event.kind:alert and event.module:endgame and event.action:rules_engine_event","filters":[],"references":[],"severity":"medium","tags":["Elastic","Endpoint"],"to":"now","type":"query","threat":[],"throttle":null,"version":1}]', - method: 'POST', - }); - }); - - test('happy path', async () => { - const ruleResp = await duplicateRules({ rules: rulesMock.data }); - expect(ruleResp).toEqual(rulesMock); - }); - }); - - describe('createPrepackagedRules', () => { - beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue('unknown'); - }); - - test('check parameter url when creating pre-packaged rules', async () => { - await createPrepackagedRules({ signal: abortCtrl.signal }); - expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/prepackaged', { - signal: abortCtrl.signal, - method: 'PUT', - }); - }); - test('happy path', async () => { - const resp = await createPrepackagedRules({ signal: abortCtrl.signal }); - expect(resp).toEqual(true); - }); - }); - - describe('importRules', () => { - const fileToImport: File = { - lastModified: 33, - name: 'fileToImport', - size: 89, - type: 'json', - arrayBuffer: jest.fn(), - slice: jest.fn(), - stream: jest.fn(), - text: jest.fn(), - } as File; - const formData = new FormData(); - formData.append('file', fileToImport); - - beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue('unknown'); - }); - - test('check parameter url, body and query when importing rules', async () => { - await importRules({ fileToImport, signal: abortCtrl.signal }); - expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_import', { - signal: abortCtrl.signal, - method: 'POST', - body: formData, - headers: { - 'Content-Type': undefined, - }, - query: { - overwrite: false, - }, - }); - }); - - test('check parameter url, body and query when importing rules with overwrite', async () => { - await importRules({ fileToImport, overwrite: true, signal: abortCtrl.signal }); - expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_import', { - signal: abortCtrl.signal, - method: 'POST', - body: formData, - headers: { - 'Content-Type': undefined, - }, - query: { - overwrite: true, - }, - }); - }); - - test('happy path', async () => { - fetchMock.mockResolvedValue({ - success: true, - success_count: 33, - errors: [], - }); - const resp = await importRules({ fileToImport, signal: abortCtrl.signal }); - expect(resp).toEqual({ - success: true, - success_count: 33, - errors: [], - }); - }); - }); - - describe('exportRules', () => { - const blob: Blob = { - size: 89, - type: 'json', - arrayBuffer: jest.fn(), - slice: jest.fn(), - stream: jest.fn(), - text: jest.fn(), - } as Blob; - - beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(blob); - }); - - test('check parameter url, body and query when exporting rules', async () => { - await exportRules({ - ids: ['mySuperRuleId', 'mySuperRuleId_II'], - signal: abortCtrl.signal, - }); - expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_export', { - signal: abortCtrl.signal, - method: 'POST', - body: '{"objects":[{"rule_id":"mySuperRuleId"},{"rule_id":"mySuperRuleId_II"}]}', - query: { - exclude_export_details: false, - file_name: 'rules_export.ndjson', - }, - }); - }); - - test('check parameter url, body and query when exporting rules with excludeExportDetails', async () => { - await exportRules({ - excludeExportDetails: true, - ids: ['mySuperRuleId', 'mySuperRuleId_II'], - signal: abortCtrl.signal, - }); - expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_export', { - signal: abortCtrl.signal, - method: 'POST', - body: '{"objects":[{"rule_id":"mySuperRuleId"},{"rule_id":"mySuperRuleId_II"}]}', - query: { - exclude_export_details: true, - file_name: 'rules_export.ndjson', - }, - }); - }); - - test('check parameter url, body and query when exporting rules with fileName', async () => { - await exportRules({ - filename: 'myFileName.ndjson', - ids: ['mySuperRuleId', 'mySuperRuleId_II'], - signal: abortCtrl.signal, - }); - expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_export', { - signal: abortCtrl.signal, - method: 'POST', - body: '{"objects":[{"rule_id":"mySuperRuleId"},{"rule_id":"mySuperRuleId_II"}]}', - query: { - exclude_export_details: false, - file_name: 'myFileName.ndjson', - }, - }); - }); - - test('check parameter url, body and query when exporting rules with all options', async () => { - await exportRules({ - excludeExportDetails: true, - filename: 'myFileName.ndjson', - ids: ['mySuperRuleId', 'mySuperRuleId_II'], - signal: abortCtrl.signal, - }); - expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_export', { - signal: abortCtrl.signal, - method: 'POST', - body: '{"objects":[{"rule_id":"mySuperRuleId"},{"rule_id":"mySuperRuleId_II"}]}', - query: { - exclude_export_details: true, - file_name: 'myFileName.ndjson', - }, - }); - }); - - test('happy path', async () => { - const resp = await exportRules({ - ids: ['mySuperRuleId', 'mySuperRuleId_II'], - signal: abortCtrl.signal, - }); - expect(resp).toEqual(blob); - }); - }); - - describe('getRuleStatusById', () => { - const statusMock = { - myRule: { - current_status: { - alert_id: 'alertId', - status_date: 'mm/dd/yyyyTHH:MM:sssz', - status: 'succeeded', - last_failure_at: null, - last_success_at: 'mm/dd/yyyyTHH:MM:sssz', - last_failure_message: null, - last_success_message: 'it is a success', - }, - failures: [], - }, - }; - - beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(statusMock); - }); - - test('check parameter url, query', async () => { - await getRuleStatusById({ id: 'mySuperRuleId', signal: abortCtrl.signal }); - expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find_statuses', { - body: '{"ids":["mySuperRuleId"]}', - method: 'POST', - signal: abortCtrl.signal, - }); - }); - - test('happy path', async () => { - const ruleResp = await getRuleStatusById({ id: 'mySuperRuleId', signal: abortCtrl.signal }); - expect(ruleResp).toEqual(statusMock); - }); - }); - - describe('fetchTags', () => { - beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(['some', 'tags']); - }); - - test('check parameter url when fetching tags', async () => { - await fetchTags({ signal: abortCtrl.signal }); - expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/tags', { - signal: abortCtrl.signal, - method: 'GET', - }); - }); - - test('happy path', async () => { - const resp = await fetchTags({ signal: abortCtrl.signal }); - expect(resp).toEqual(['some', 'tags']); - }); - }); - - describe('getPrePackagedRulesStatus', () => { - const prePackagedRulesStatus = { - rules_custom_installed: 33, - rules_installed: 12, - rules_not_installed: 0, - rules_not_updated: 2, - }; - beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(prePackagedRulesStatus); - }); - test('check parameter url when fetching tags', async () => { - await getPrePackagedRulesStatus({ signal: abortCtrl.signal }); - expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/prepackaged/_status', { - signal: abortCtrl.signal, - method: 'GET', - }); - }); - test('happy path', async () => { - const resp = await getPrePackagedRulesStatus({ signal: abortCtrl.signal }); - expect(resp).toEqual(prePackagedRulesStatus); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/containers/detection_engine/rules/api.ts b/x-pack/plugins/siem/public/containers/detection_engine/rules/api.ts deleted file mode 100644 index c1fadf289ef4d9..00000000000000 --- a/x-pack/plugins/siem/public/containers/detection_engine/rules/api.ts +++ /dev/null @@ -1,331 +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 { - DETECTION_ENGINE_RULES_URL, - DETECTION_ENGINE_PREPACKAGED_URL, - DETECTION_ENGINE_RULES_STATUS_URL, - DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL, - DETECTION_ENGINE_TAGS_URL, -} from '../../../../common/constants'; -import { - AddRulesProps, - DeleteRulesProps, - DuplicateRulesProps, - EnableRulesProps, - FetchRulesProps, - FetchRulesResponse, - NewRule, - Rule, - FetchRuleProps, - BasicFetchProps, - ImportDataProps, - ExportDocumentsProps, - RuleStatusResponse, - ImportDataResponse, - PrePackagedRulesStatusResponse, - BulkRuleResponse, -} from './types'; -import { KibanaServices } from '../../../lib/kibana'; -import * as i18n from '../../../pages/detection_engine/rules/translations'; - -/** - * Add provided Rule - * - * @param rule to add - * @param signal to cancel request - * - * @throws An error if response is not OK - */ -export const addRule = async ({ rule, signal }: AddRulesProps): Promise => - KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_URL, { - method: rule.id != null ? 'PUT' : 'POST', - body: JSON.stringify(rule), - signal, - }); - -/** - * Fetches all rules from the Detection Engine API - * - * @param filterOptions desired filters (e.g. filter/sortField/sortOrder) - * @param pagination desired pagination options (e.g. page/perPage) - * @param signal to cancel request - * - * @throws An error if response is not OK - */ -export const fetchRules = async ({ - filterOptions = { - filter: '', - sortField: 'enabled', - sortOrder: 'desc', - showCustomRules: false, - showElasticRules: false, - tags: [], - }, - pagination = { - page: 1, - perPage: 20, - total: 0, - }, - signal, -}: FetchRulesProps): Promise => { - const filters = [ - ...(filterOptions.filter.length ? [`alert.attributes.name: ${filterOptions.filter}`] : []), - ...(filterOptions.showCustomRules - ? [`alert.attributes.tags: "__internal_immutable:false"`] - : []), - ...(filterOptions.showElasticRules - ? [`alert.attributes.tags: "__internal_immutable:true"`] - : []), - ...(filterOptions.tags?.map(t => `alert.attributes.tags: ${t}`) ?? []), - ]; - - const query = { - page: pagination.page, - per_page: pagination.perPage, - sort_field: filterOptions.sortField, - sort_order: filterOptions.sortOrder, - ...(filters.length ? { filter: filters.join(' AND ') } : {}), - }; - - return KibanaServices.get().http.fetch( - `${DETECTION_ENGINE_RULES_URL}/_find`, - { - method: 'GET', - query, - signal, - } - ); -}; - -/** - * Fetch a Rule by providing a Rule ID - * - * @param id Rule ID's (not rule_id) - * @param signal to cancel request - * - * @throws An error if response is not OK - */ -export const fetchRuleById = async ({ id, signal }: FetchRuleProps): Promise => - KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_URL, { - method: 'GET', - query: { id }, - signal, - }); - -/** - * Enables/Disables provided Rule ID's - * - * @param ids array of Rule ID's (not rule_id) to enable/disable - * @param enabled to enable or disable - * - * @throws An error if response is not OK - */ -export const enableRules = async ({ ids, enabled }: EnableRulesProps): Promise => - KibanaServices.get().http.fetch(`${DETECTION_ENGINE_RULES_URL}/_bulk_update`, { - method: 'PATCH', - body: JSON.stringify(ids.map(id => ({ id, enabled }))), - }); - -/** - * Deletes provided Rule ID's - * - * @param ids array of Rule ID's (not rule_id) to delete - * - * @throws An error if response is not OK - */ -export const deleteRules = async ({ ids }: DeleteRulesProps): Promise => - KibanaServices.get().http.fetch(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`, { - method: 'DELETE', - body: JSON.stringify(ids.map(id => ({ id }))), - }); - -/** - * Duplicates provided Rules - * - * @param rules to duplicate - * - * @throws An error if response is not OK - */ -export const duplicateRules = async ({ rules }: DuplicateRulesProps): Promise => - KibanaServices.get().http.fetch(`${DETECTION_ENGINE_RULES_URL}/_bulk_create`, { - method: 'POST', - body: JSON.stringify( - rules.map(rule => ({ - ...rule, - name: `${rule.name} [${i18n.DUPLICATE}]`, - created_at: undefined, - created_by: undefined, - id: undefined, - rule_id: undefined, - updated_at: undefined, - updated_by: undefined, - enabled: rule.enabled, - immutable: undefined, - last_success_at: undefined, - last_success_message: undefined, - last_failure_at: undefined, - last_failure_message: undefined, - status: undefined, - status_date: undefined, - })) - ), - }); - -/** - * Create Prepackaged Rules - * - * @param signal AbortSignal for cancelling request - * - * @throws An error if response is not OK - */ -export const createPrepackagedRules = async ({ signal }: BasicFetchProps): Promise => { - await KibanaServices.get().http.fetch(DETECTION_ENGINE_PREPACKAGED_URL, { - method: 'PUT', - signal, - }); - - return true; -}; - -/** - * Imports rules in the same format as exported via the _export API - * - * @param fileToImport File to upload containing rules to import - * @param overwrite whether or not to overwrite rules with the same ruleId - * @param signal AbortSignal for cancelling request - * - * @throws An error if response is not OK - */ -export const importRules = async ({ - fileToImport, - overwrite = false, - signal, -}: ImportDataProps): Promise => { - const formData = new FormData(); - formData.append('file', fileToImport); - - return KibanaServices.get().http.fetch( - `${DETECTION_ENGINE_RULES_URL}/_import`, - { - method: 'POST', - headers: { 'Content-Type': undefined }, - query: { overwrite }, - body: formData, - signal, - } - ); -}; - -/** - * Export rules from the server as a file download - * - * @param excludeExportDetails whether or not to exclude additional details at bottom of exported file (defaults to false) - * @param filename of exported rules. Be sure to include `.ndjson` extension! (defaults to localized `rules_export.ndjson`) - * @param ruleIds array of rule_id's (not id!) to export (empty array exports _all_ rules) - * @param signal AbortSignal for cancelling request - * - * @throws An error if response is not OK - */ -export const exportRules = async ({ - excludeExportDetails = false, - filename = `${i18n.EXPORT_FILENAME}.ndjson`, - ids = [], - signal, -}: ExportDocumentsProps): Promise => { - const body = - ids.length > 0 ? JSON.stringify({ objects: ids.map(rule => ({ rule_id: rule })) }) : undefined; - - return KibanaServices.get().http.fetch(`${DETECTION_ENGINE_RULES_URL}/_export`, { - method: 'POST', - body, - query: { - exclude_export_details: excludeExportDetails, - file_name: filename, - }, - signal, - }); -}; - -/** - * Get Rule Status provided Rule ID - * - * @param id string of Rule ID's (not rule_id) - * @param signal AbortSignal for cancelling request - * - * @throws An error if response is not OK - */ -export const getRuleStatusById = async ({ - id, - signal, -}: { - id: string; - signal: AbortSignal; -}): Promise => - KibanaServices.get().http.fetch(DETECTION_ENGINE_RULES_STATUS_URL, { - method: 'POST', - body: JSON.stringify({ ids: [id] }), - signal, - }); - -/** - * Return rule statuses given list of alert ids - * - * @param ids array of string of Rule ID's (not rule_id) - * @param signal AbortSignal for cancelling request - * - * @throws An error if response is not OK - */ -export const getRulesStatusByIds = async ({ - ids, - signal, -}: { - ids: string[]; - signal: AbortSignal; -}): Promise => { - const res = await KibanaServices.get().http.fetch( - DETECTION_ENGINE_RULES_STATUS_URL, - { - method: 'POST', - body: JSON.stringify({ ids }), - signal, - } - ); - return res; -}; - -/** - * Fetch all unique Tags used by Rules - * - * @param signal to cancel request - * - * @throws An error if response is not OK - */ -export const fetchTags = async ({ signal }: { signal: AbortSignal }): Promise => - KibanaServices.get().http.fetch(DETECTION_ENGINE_TAGS_URL, { - method: 'GET', - signal, - }); - -/** - * Get pre packaged rules Status - * - * @param signal AbortSignal for cancelling request - * - * @throws An error if response is not OK - */ -export const getPrePackagedRulesStatus = async ({ - signal, -}: { - signal: AbortSignal; -}): Promise => - KibanaServices.get().http.fetch( - DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL, - { - method: 'GET', - signal, - } - ); diff --git a/x-pack/plugins/siem/public/containers/detection_engine/rules/types.ts b/x-pack/plugins/siem/public/containers/detection_engine/rules/types.ts deleted file mode 100644 index f89d21ef1aeb19..00000000000000 --- a/x-pack/plugins/siem/public/containers/detection_engine/rules/types.ts +++ /dev/null @@ -1,246 +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 t from 'io-ts'; - -import { RuleTypeSchema } from '../../../../common/detection_engine/types'; - -/** - * Params is an "record", since it is a type of AlertActionParams which is action templates. - * @see x-pack/plugins/alerting/common/alert.ts - */ -export const action = t.exact( - t.type({ - group: t.string, - id: t.string, - action_type_id: t.string, - params: t.record(t.string, t.any), - }) -); - -export const NewRuleSchema = t.intersection([ - t.type({ - description: t.string, - enabled: t.boolean, - interval: t.string, - name: t.string, - risk_score: t.number, - severity: t.string, - type: RuleTypeSchema, - }), - t.partial({ - actions: t.array(action), - anomaly_threshold: t.number, - created_by: t.string, - false_positives: t.array(t.string), - filters: t.array(t.unknown), - from: t.string, - id: t.string, - index: t.array(t.string), - language: t.string, - machine_learning_job_id: t.string, - max_signals: t.number, - query: t.string, - references: t.array(t.string), - rule_id: t.string, - saved_id: t.string, - tags: t.array(t.string), - threat: t.array(t.unknown), - throttle: t.union([t.string, t.null]), - to: t.string, - updated_by: t.string, - note: t.string, - }), -]); - -export const NewRulesSchema = t.array(NewRuleSchema); -export type NewRule = t.TypeOf; - -export interface AddRulesProps { - rule: NewRule; - signal: AbortSignal; -} - -const MetaRule = t.intersection([ - t.type({ - from: t.string, - }), - t.partial({ - throttle: t.string, - kibana_siem_app_url: t.string, - }), -]); - -export const RuleSchema = t.intersection([ - t.type({ - created_at: t.string, - created_by: t.string, - description: t.string, - enabled: t.boolean, - false_positives: t.array(t.string), - from: t.string, - id: t.string, - interval: t.string, - immutable: t.boolean, - name: t.string, - max_signals: t.number, - references: t.array(t.string), - risk_score: t.number, - rule_id: t.string, - severity: t.string, - tags: t.array(t.string), - type: RuleTypeSchema, - to: t.string, - threat: t.array(t.unknown), - updated_at: t.string, - updated_by: t.string, - actions: t.array(action), - throttle: t.union([t.string, t.null]), - }), - t.partial({ - anomaly_threshold: t.number, - filters: t.array(t.unknown), - index: t.array(t.string), - language: t.string, - last_failure_at: t.string, - last_failure_message: t.string, - meta: MetaRule, - machine_learning_job_id: t.string, - output_index: t.string, - query: t.string, - saved_id: t.string, - status: t.string, - status_date: t.string, - timeline_id: t.string, - timeline_title: t.string, - note: t.string, - version: t.number, - }), -]); - -export const RulesSchema = t.array(RuleSchema); - -export type Rule = t.TypeOf; -export type Rules = t.TypeOf; - -export interface RuleError { - id?: string; - rule_id?: string; - error: { status_code: number; message: string }; -} - -export type BulkRuleResponse = Array; - -export interface RuleResponseBuckets { - rules: Rule[]; - errors: RuleError[]; -} - -export interface PaginationOptions { - page: number; - perPage: number; - total: number; -} - -export interface FetchRulesProps { - pagination?: PaginationOptions; - filterOptions?: FilterOptions; - signal: AbortSignal; -} - -export interface FilterOptions { - filter: string; - sortField: string; - sortOrder: 'asc' | 'desc'; - showCustomRules?: boolean; - showElasticRules?: boolean; - tags?: string[]; -} - -export interface FetchRulesResponse { - page: number; - perPage: number; - total: number; - data: Rule[]; -} - -export interface FetchRuleProps { - id: string; - signal: AbortSignal; -} - -export interface EnableRulesProps { - ids: string[]; - enabled: boolean; -} - -export interface DeleteRulesProps { - ids: string[]; -} - -export interface DuplicateRulesProps { - rules: Rule[]; -} - -export interface BasicFetchProps { - signal: AbortSignal; -} - -export interface ImportDataProps { - fileToImport: File; - overwrite?: boolean; - signal: AbortSignal; -} - -export interface ImportRulesResponseError { - rule_id: string; - error: { - status_code: number; - message: string; - }; -} - -export interface ImportDataResponse { - success: boolean; - success_count: number; - errors: ImportRulesResponseError[]; -} - -export interface ExportDocumentsProps { - ids: string[]; - filename?: string; - excludeExportDetails?: boolean; - signal: AbortSignal; -} - -export interface RuleStatus { - current_status: RuleInfoStatus; - failures: RuleInfoStatus[]; -} - -export type RuleStatusType = 'executing' | 'failed' | 'going to run' | 'succeeded'; -export interface RuleInfoStatus { - alert_id: string; - status_date: string; - status: RuleStatusType | null; - last_failure_at: string | null; - last_success_at: string | null; - last_failure_message: string | null; - last_success_message: string | null; - last_look_back_date: string | null | undefined; - gap: string | null | undefined; - bulk_create_time_durations: string[] | null | undefined; - search_after_time_durations: string[] | null | undefined; -} - -export type RuleStatusResponse = Record; - -export interface PrePackagedRulesStatusResponse { - rules_custom_installed: number; - rules_installed: number; - rules_not_installed: number; - rules_not_updated: number; -} diff --git a/x-pack/plugins/siem/public/containers/detection_engine/signals/api.test.ts b/x-pack/plugins/siem/public/containers/detection_engine/signals/api.test.ts deleted file mode 100644 index c011ecffb35bcd..00000000000000 --- a/x-pack/plugins/siem/public/containers/detection_engine/signals/api.test.ts +++ /dev/null @@ -1,165 +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 { KibanaServices } from '../../../lib/kibana'; -import { - signalsMock, - mockSignalsQuery, - mockStatusSignalQuery, - mockSignalIndex, - mockUserPrivilege, -} from './mock'; -import { - fetchQuerySignals, - updateSignalStatus, - getSignalIndex, - getUserPrivilege, - createSignalIndex, -} from './api'; - -const abortCtrl = new AbortController(); -const mockKibanaServices = KibanaServices.get as jest.Mock; -jest.mock('../../../lib/kibana'); - -const fetchMock = jest.fn(); -mockKibanaServices.mockReturnValue({ http: { fetch: fetchMock } }); - -describe('Detections Signals API', () => { - describe('fetchQuerySignals', () => { - beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(signalsMock); - }); - - test('check parameter url, body', async () => { - await fetchQuerySignals({ query: mockSignalsQuery, signal: abortCtrl.signal }); - expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/signals/search', { - body: - '{"aggs":{"signalsByGrouping":{"terms":{"field":"signal.rule.risk_score","missing":"All others","order":{"_count":"desc"},"size":10},"aggs":{"signals":{"date_histogram":{"field":"@timestamp","fixed_interval":"81000000ms","min_doc_count":0,"extended_bounds":{"min":1579644343954,"max":1582236343955}}}}}},"query":{"bool":{"filter":[{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}},{"range":{"@timestamp":{"gte":1579644343954,"lte":1582236343955}}}]}}}', - method: 'POST', - signal: abortCtrl.signal, - }); - }); - - test('happy path', async () => { - const signalsResp = await fetchQuerySignals({ - query: mockSignalsQuery, - signal: abortCtrl.signal, - }); - expect(signalsResp).toEqual(signalsMock); - }); - }); - - describe('updateSignalStatus', () => { - beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue({}); - }); - - test('check parameter url, body when closing a signal', async () => { - await updateSignalStatus({ - query: mockStatusSignalQuery, - signal: abortCtrl.signal, - status: 'closed', - }); - expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/signals/status', { - body: - '{"status":"closed","bool":{"filter":{"terms":{"_id":["b4ee5c32e3a321057edcc953ca17228c6fdfe5ba43fdbbdaffa8cefa11605cc5"]}}}}', - method: 'POST', - signal: abortCtrl.signal, - }); - }); - - test('check parameter url, body when opening a signal', async () => { - await updateSignalStatus({ - query: mockStatusSignalQuery, - signal: abortCtrl.signal, - status: 'open', - }); - expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/signals/status', { - body: - '{"status":"open","bool":{"filter":{"terms":{"_id":["b4ee5c32e3a321057edcc953ca17228c6fdfe5ba43fdbbdaffa8cefa11605cc5"]}}}}', - method: 'POST', - signal: abortCtrl.signal, - }); - }); - - test('happy path', async () => { - const signalsResp = await updateSignalStatus({ - query: mockStatusSignalQuery, - signal: abortCtrl.signal, - status: 'open', - }); - expect(signalsResp).toEqual({}); - }); - }); - - describe('getSignalIndex', () => { - beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(mockSignalIndex); - }); - - test('check parameter url', async () => { - await getSignalIndex({ signal: abortCtrl.signal }); - expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/index', { - method: 'GET', - signal: abortCtrl.signal, - }); - }); - - test('happy path', async () => { - const signalsResp = await getSignalIndex({ - signal: abortCtrl.signal, - }); - expect(signalsResp).toEqual(mockSignalIndex); - }); - }); - - describe('getUserPrivilege', () => { - beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(mockUserPrivilege); - }); - - test('check parameter url', async () => { - await getUserPrivilege({ signal: abortCtrl.signal }); - expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/privileges', { - method: 'GET', - signal: abortCtrl.signal, - }); - }); - - test('happy path', async () => { - const signalsResp = await getUserPrivilege({ - signal: abortCtrl.signal, - }); - expect(signalsResp).toEqual(mockUserPrivilege); - }); - }); - - describe('createSignalIndex', () => { - beforeEach(() => { - fetchMock.mockClear(); - fetchMock.mockResolvedValue(mockSignalIndex); - }); - - test('check parameter url', async () => { - await createSignalIndex({ signal: abortCtrl.signal }); - expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/index', { - method: 'POST', - signal: abortCtrl.signal, - }); - }); - - test('happy path', async () => { - const signalsResp = await createSignalIndex({ - signal: abortCtrl.signal, - }); - expect(signalsResp).toEqual(mockSignalIndex); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/containers/detection_engine/signals/api.ts b/x-pack/plugins/siem/public/containers/detection_engine/signals/api.ts deleted file mode 100644 index 1397e4a8696be8..00000000000000 --- a/x-pack/plugins/siem/public/containers/detection_engine/signals/api.ts +++ /dev/null @@ -1,101 +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 { - DETECTION_ENGINE_QUERY_SIGNALS_URL, - DETECTION_ENGINE_SIGNALS_STATUS_URL, - DETECTION_ENGINE_INDEX_URL, - DETECTION_ENGINE_PRIVILEGES_URL, -} from '../../../../common/constants'; -import { KibanaServices } from '../../../lib/kibana'; -import { - BasicSignals, - Privilege, - QuerySignals, - SignalSearchResponse, - SignalsIndex, - UpdateSignalStatusProps, -} from './types'; - -/** - * Fetch Signals by providing a query - * - * @param query String to match a dsl - * @param signal to cancel request - * - * @throws An error if response is not OK - */ -export const fetchQuerySignals = async ({ - query, - signal, -}: QuerySignals): Promise> => - KibanaServices.get().http.fetch>( - DETECTION_ENGINE_QUERY_SIGNALS_URL, - { - method: 'POST', - body: JSON.stringify(query), - signal, - } - ); - -/** - * Update signal status by query - * - * @param query of signals to update - * @param status to update to('open' / 'closed') - * @param signal AbortSignal for cancelling request - * - * @throws An error if response is not OK - */ -export const updateSignalStatus = async ({ - query, - status, - signal, -}: UpdateSignalStatusProps): Promise => - KibanaServices.get().http.fetch(DETECTION_ENGINE_SIGNALS_STATUS_URL, { - method: 'POST', - body: JSON.stringify({ status, ...query }), - signal, - }); - -/** - * Fetch Signal Index - * - * @param signal AbortSignal for cancelling request - * - * @throws An error if response is not OK - */ -export const getSignalIndex = async ({ signal }: BasicSignals): Promise => - KibanaServices.get().http.fetch(DETECTION_ENGINE_INDEX_URL, { - method: 'GET', - signal, - }); - -/** - * Get User Privileges - * - * @param signal AbortSignal for cancelling request - * - * @throws An error if response is not OK - */ -export const getUserPrivilege = async ({ signal }: BasicSignals): Promise => - KibanaServices.get().http.fetch(DETECTION_ENGINE_PRIVILEGES_URL, { - method: 'GET', - signal, - }); - -/** - * Create Signal Index if needed it - * - * @param signal AbortSignal for cancelling request - * - * @throws An error if response is not OK - */ -export const createSignalIndex = async ({ signal }: BasicSignals): Promise => - KibanaServices.get().http.fetch(DETECTION_ENGINE_INDEX_URL, { - method: 'POST', - signal, - }); diff --git a/x-pack/plugins/siem/public/containers/events/last_event_time/index.ts b/x-pack/plugins/siem/public/containers/events/last_event_time/index.ts deleted file mode 100644 index 9cae503d309408..00000000000000 --- a/x-pack/plugins/siem/public/containers/events/last_event_time/index.ts +++ /dev/null @@ -1,86 +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 { get } from 'lodash/fp'; -import React, { useEffect, useState } from 'react'; - -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; -import { GetLastEventTimeQuery, LastEventIndexKey, LastTimeDetails } from '../../../graphql/types'; -import { inputsModel } from '../../../store'; -import { QueryTemplateProps } from '../../query_template'; -import { useUiSetting$ } from '../../../lib/kibana'; - -import { LastEventTimeGqlQuery } from './last_event_time.gql_query'; -import { useApolloClient } from '../../../utils/apollo_context'; - -export interface LastEventTimeArgs { - id: string; - errorMessage: string; - lastSeen: Date; - loading: boolean; - refetch: inputsModel.Refetch; -} - -export interface OwnProps extends QueryTemplateProps { - children: (args: LastEventTimeArgs) => React.ReactNode; - indexKey: LastEventIndexKey; -} - -export function useLastEventTimeQuery( - indexKey: LastEventIndexKey, - details: LastTimeDetails, - sourceId: string -) { - const [loading, updateLoading] = useState(false); - const [lastSeen, updateLastSeen] = useState(null); - const [errorMessage, updateErrorMessage] = useState(null); - const [currentIndexKey, updateCurrentIndexKey] = useState(null); - const [defaultIndex] = useUiSetting$(DEFAULT_INDEX_KEY); - const apolloClient = useApolloClient(); - async function fetchLastEventTime(signal: AbortSignal) { - updateLoading(true); - if (apolloClient) { - apolloClient - .query({ - query: LastEventTimeGqlQuery, - fetchPolicy: 'cache-first', - variables: { - sourceId, - indexKey, - details, - defaultIndex, - }, - context: { - fetchOptions: { - signal, - }, - }, - }) - .then( - result => { - updateLoading(false); - updateLastSeen(get('data.source.LastEventTime.lastSeen', result)); - updateErrorMessage(null); - updateCurrentIndexKey(currentIndexKey); - }, - error => { - updateLoading(false); - updateLastSeen(null); - updateErrorMessage(error.message); - } - ); - } - } - - useEffect(() => { - const abortCtrl = new AbortController(); - const signal = abortCtrl.signal; - fetchLastEventTime(signal); - return () => abortCtrl.abort(); - }, [apolloClient, indexKey, details.hostName, details.ip]); - - return { lastSeen, loading, errorMessage }; -} diff --git a/x-pack/plugins/siem/public/containers/events/last_event_time/mock.ts b/x-pack/plugins/siem/public/containers/events/last_event_time/mock.ts deleted file mode 100644 index 43f55dfcf27776..00000000000000 --- a/x-pack/plugins/siem/public/containers/events/last_event_time/mock.ts +++ /dev/null @@ -1,61 +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_INDEX_PATTERN } from '../../../../common/constants'; -import { GetLastEventTimeQuery, LastEventIndexKey } from '../../../graphql/types'; - -import { LastEventTimeGqlQuery } from './last_event_time.gql_query'; - -interface MockLastEventTimeQuery { - request: { - query: GetLastEventTimeQuery.Query; - variables: GetLastEventTimeQuery.Variables; - }; - result: { - data?: { - source: { - id: string; - LastEventTime: { - lastSeen: string | null; - errorMessage: string | null; - }; - }; - }; - errors?: [{ message: string }]; - }; -} - -const getTimeTwelveMinutesAgo = () => { - const d = new Date(); - const ts = d.getTime(); - const twelveMinutes = ts - 12 * 60 * 1000; - return new Date(twelveMinutes).toISOString(); -}; - -export const mockLastEventTimeQuery: MockLastEventTimeQuery[] = [ - { - request: { - query: LastEventTimeGqlQuery, - variables: { - sourceId: 'default', - indexKey: LastEventIndexKey.hosts, - details: {}, - defaultIndex: DEFAULT_INDEX_PATTERN, - }, - }, - result: { - data: { - source: { - id: 'default', - LastEventTime: { - lastSeen: getTimeTwelveMinutesAgo(), - errorMessage: null, - }, - }, - }, - }, - }, -]; diff --git a/x-pack/plugins/siem/public/containers/helpers.test.ts b/x-pack/plugins/siem/public/containers/helpers.test.ts deleted file mode 100644 index 5d378d79acc7a4..00000000000000 --- a/x-pack/plugins/siem/public/containers/helpers.test.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 { ESQuery } from '../../common/typed_json'; - -import { createFilter } from './helpers'; - -describe('Helpers', () => { - describe('#createFilter', () => { - test('if it is a string it returns untouched', () => { - const filter = createFilter('even invalid strings return the same'); - expect(filter).toBe('even invalid strings return the same'); - }); - - test('if it is an ESQuery object it will be returned as a string', () => { - const query: ESQuery = { term: { 'host.id': 'host-value' } }; - const filter = createFilter(query); - expect(filter).toBe(JSON.stringify(query)); - }); - - test('if it is undefined, then undefined is returned', () => { - const filter = createFilter(undefined); - expect(filter).toBe(undefined); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/containers/helpers.ts b/x-pack/plugins/siem/public/containers/helpers.ts deleted file mode 100644 index 5f66e3f4b88d4d..00000000000000 --- a/x-pack/plugins/siem/public/containers/helpers.ts +++ /dev/null @@ -1,15 +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 { FetchPolicy } from 'apollo-client'; -import { isString } from 'lodash/fp'; - -import { ESQuery } from '../../common/typed_json'; - -export const createFilter = (filterQuery: ESQuery | string | undefined) => - isString(filterQuery) ? filterQuery : JSON.stringify(filterQuery); - -export const getDefaultFetchPolicy = (): FetchPolicy => 'cache-and-network'; diff --git a/x-pack/plugins/siem/public/containers/hosts/first_last_seen/index.ts b/x-pack/plugins/siem/public/containers/hosts/first_last_seen/index.ts deleted file mode 100644 index a460fa8999b57b..00000000000000 --- a/x-pack/plugins/siem/public/containers/hosts/first_last_seen/index.ts +++ /dev/null @@ -1,85 +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 ApolloClient from 'apollo-client'; -import { get } from 'lodash/fp'; -import React, { useEffect, useState } from 'react'; - -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; -import { useUiSetting$ } from '../../../lib/kibana'; -import { GetHostFirstLastSeenQuery } from '../../../graphql/types'; -import { inputsModel } from '../../../store'; -import { QueryTemplateProps } from '../../query_template'; - -import { HostFirstLastSeenGqlQuery } from './first_last_seen.gql_query'; - -export interface FirstLastSeenHostArgs { - id: string; - errorMessage: string; - firstSeen: Date; - lastSeen: Date; - loading: boolean; - refetch: inputsModel.Refetch; -} - -export interface OwnProps extends QueryTemplateProps { - children: (args: FirstLastSeenHostArgs) => React.ReactNode; - hostName: string; -} - -export function useFirstLastSeenHostQuery( - hostName: string, - sourceId: string, - apolloClient: ApolloClient -) { - const [loading, updateLoading] = useState(false); - const [firstSeen, updateFirstSeen] = useState(null); - const [lastSeen, updateLastSeen] = useState(null); - const [errorMessage, updateErrorMessage] = useState(null); - const [defaultIndex] = useUiSetting$(DEFAULT_INDEX_KEY); - - async function fetchFirstLastSeenHost(signal: AbortSignal) { - updateLoading(true); - return apolloClient - .query({ - query: HostFirstLastSeenGqlQuery, - fetchPolicy: 'cache-first', - variables: { - sourceId, - hostName, - defaultIndex, - }, - context: { - fetchOptions: { - signal, - }, - }, - }) - .then( - result => { - updateLoading(false); - updateFirstSeen(get('data.source.HostFirstLastSeen.firstSeen', result)); - updateLastSeen(get('data.source.HostFirstLastSeen.lastSeen', result)); - updateErrorMessage(null); - }, - error => { - updateLoading(false); - updateFirstSeen(null); - updateLastSeen(null); - updateErrorMessage(error.message); - } - ); - } - - useEffect(() => { - const abortCtrl = new AbortController(); - const signal = abortCtrl.signal; - fetchFirstLastSeenHost(signal); - return () => abortCtrl.abort(); - }, []); - - return { firstSeen, lastSeen, loading, errorMessage }; -} diff --git a/x-pack/plugins/siem/public/containers/hosts/first_last_seen/mock.ts b/x-pack/plugins/siem/public/containers/hosts/first_last_seen/mock.ts deleted file mode 100644 index f59df84dacc1b1..00000000000000 --- a/x-pack/plugins/siem/public/containers/hosts/first_last_seen/mock.ts +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { DEFAULT_INDEX_PATTERN } from '../../../../common/constants'; -import { GetHostFirstLastSeenQuery } from '../../../graphql/types'; - -import { HostFirstLastSeenGqlQuery } from './first_last_seen.gql_query'; - -interface MockedProvidedQuery { - request: { - query: GetHostFirstLastSeenQuery.Query; - variables: GetHostFirstLastSeenQuery.Variables; - }; - result: { - data?: { - source: { - id: string; - HostFirstLastSeen: { - firstSeen: string | null; - lastSeen: string | null; - }; - }; - }; - errors?: [{ message: string }]; - }; -} -export const mockFirstLastSeenHostQuery: MockedProvidedQuery[] = [ - { - request: { - query: HostFirstLastSeenGqlQuery, - variables: { - sourceId: 'default', - hostName: 'kibana-siem', - defaultIndex: DEFAULT_INDEX_PATTERN, - }, - }, - result: { - data: { - source: { - id: 'default', - HostFirstLastSeen: { - firstSeen: '2019-04-08T16:09:40.692Z', - lastSeen: '2019-04-08T18:35:45.064Z', - }, - }, - }, - }, - }, -]; diff --git a/x-pack/plugins/siem/public/containers/hosts/index.tsx b/x-pack/plugins/siem/public/containers/hosts/index.tsx deleted file mode 100644 index 733c2224d840a7..00000000000000 --- a/x-pack/plugins/siem/public/containers/hosts/index.tsx +++ /dev/null @@ -1,183 +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 { get, getOr } from 'lodash/fp'; -import memoizeOne from 'memoize-one'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; - -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; -import { - Direction, - GetHostsTableQuery, - HostsEdges, - HostsFields, - PageInfoPaginated, -} from '../../graphql/types'; -import { hostsModel, hostsSelectors, inputsModel, State, inputsSelectors } from '../../store'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; -import { withKibana, WithKibanaProps } from '../../lib/kibana'; - -import { HostsTableQuery } from './hosts_table.gql_query'; -import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; - -const ID = 'hostsQuery'; - -export interface HostsArgs { - endDate: number; - hosts: HostsEdges[]; - id: string; - inspect: inputsModel.InspectQuery; - isInspected: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - pageInfo: PageInfoPaginated; - refetch: inputsModel.Refetch; - startDate: number; - totalCount: number; -} - -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: HostsArgs) => React.ReactNode; - type: hostsModel.HostsType; - startDate: number; - endDate: number; -} - -export interface HostsComponentReduxProps { - activePage: number; - isInspected: boolean; - limit: number; - sortField: HostsFields; - direction: Direction; -} - -type HostsProps = OwnProps & HostsComponentReduxProps & WithKibanaProps; - -class HostsComponentQuery extends QueryTemplatePaginated< - HostsProps, - GetHostsTableQuery.Query, - GetHostsTableQuery.Variables -> { - private memoizedHosts: ( - variables: string, - data: GetHostsTableQuery.Source | undefined - ) => HostsEdges[]; - - constructor(props: HostsProps) { - super(props); - this.memoizedHosts = memoizeOne(this.getHosts); - } - - public render() { - const { - activePage, - id = ID, - isInspected, - children, - direction, - filterQuery, - endDate, - kibana, - limit, - startDate, - skip, - sourceId, - sortField, - } = this.props; - const defaultIndex = kibana.services.uiSettings.get(DEFAULT_INDEX_KEY); - - const variables: GetHostsTableQuery.Variables = { - sourceId, - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, - sort: { - direction, - field: sortField, - }, - pagination: generateTablePaginationOptions(activePage, limit), - filterQuery: createFilter(filterQuery), - defaultIndex, - inspect: isInspected, - }; - return ( - - query={HostsTableQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - variables={variables} - skip={skip} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), - }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; - } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - Hosts: { - ...fetchMoreResult.source.Hosts, - edges: [...fetchMoreResult.source.Hosts.edges], - }, - }, - }; - }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - endDate, - hosts: this.memoizedHosts(JSON.stringify(variables), get('source', data)), - id, - inspect: getOr(null, 'source.Hosts.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - pageInfo: getOr({}, 'source.Hosts.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - startDate, - totalCount: getOr(-1, 'source.Hosts.totalCount', data), - }); - }} -
- ); - } - - private getHosts = ( - variables: string, - source: GetHostsTableQuery.Source | undefined - ): HostsEdges[] => getOr([], 'Hosts.edges', source); -} - -const makeMapStateToProps = () => { - const getHostsSelector = hostsSelectors.hostsSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { type, id = ID }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getHostsSelector(state, type), - isInspected, - }; - }; - return mapStateToProps; -}; - -export const HostsQuery = compose>( - connect(makeMapStateToProps), - withKibana -)(HostsComponentQuery); diff --git a/x-pack/plugins/siem/public/containers/hosts/overview/index.tsx b/x-pack/plugins/siem/public/containers/hosts/overview/index.tsx deleted file mode 100644 index 5057e872b53131..00000000000000 --- a/x-pack/plugins/siem/public/containers/hosts/overview/index.tsx +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; -import { inputsModel, inputsSelectors, State } from '../../../store'; -import { getDefaultFetchPolicy } from '../../helpers'; -import { QueryTemplate, QueryTemplateProps } from '../../query_template'; -import { withKibana, WithKibanaProps } from '../../../lib/kibana'; - -import { HostOverviewQuery } from './host_overview.gql_query'; -import { GetHostOverviewQuery, HostItem } from '../../../graphql/types'; - -const ID = 'hostOverviewQuery'; - -export interface HostOverviewArgs { - id: string; - inspect: inputsModel.InspectQuery; - hostOverview: HostItem; - loading: boolean; - refetch: inputsModel.Refetch; - startDate: number; - endDate: number; -} - -export interface HostOverviewReduxProps { - isInspected: boolean; -} - -export interface OwnProps extends QueryTemplateProps { - children: (args: HostOverviewArgs) => React.ReactNode; - hostName: string; - startDate: number; - endDate: number; -} - -type HostsOverViewProps = OwnProps & HostOverviewReduxProps & WithKibanaProps; - -class HostOverviewByNameComponentQuery extends QueryTemplate< - HostsOverViewProps, - GetHostOverviewQuery.Query, - GetHostOverviewQuery.Variables -> { - public render() { - const { - id = ID, - isInspected, - children, - hostName, - kibana, - skip, - sourceId, - startDate, - endDate, - } = this.props; - return ( - - query={HostOverviewQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={{ - sourceId, - hostName, - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, - defaultIndex: kibana.services.uiSettings.get(DEFAULT_INDEX_KEY), - inspect: isInspected, - }} - > - {({ data, loading, refetch }) => { - const hostOverview = getOr([], 'source.HostOverview', data); - return children({ - id, - inspect: getOr(null, 'source.HostOverview.inspect', data), - refetch, - loading, - hostOverview, - startDate, - endDate, - }); - }} - - ); - } -} - -const makeMapStateToProps = () => { - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = ID }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - isInspected, - }; - }; - return mapStateToProps; -}; - -export const HostOverviewByNameQuery = compose>( - connect(makeMapStateToProps), - withKibana -)(HostOverviewByNameComponentQuery); diff --git a/x-pack/plugins/siem/public/containers/ip_overview/index.tsx b/x-pack/plugins/siem/public/containers/ip_overview/index.tsx deleted file mode 100644 index ade94c430c6efb..00000000000000 --- a/x-pack/plugins/siem/public/containers/ip_overview/index.tsx +++ /dev/null @@ -1,84 +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 { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect, ConnectedProps } from 'react-redux'; - -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; -import { GetIpOverviewQuery, IpOverviewData } from '../../graphql/types'; -import { networkModel, inputsModel, inputsSelectors, State } from '../../store'; -import { useUiSetting } from '../../lib/kibana'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { QueryTemplateProps } from '../query_template'; - -import { ipOverviewQuery } from './index.gql_query'; - -const ID = 'ipOverviewQuery'; - -export interface IpOverviewArgs { - id: string; - inspect: inputsModel.InspectQuery; - ipOverviewData: IpOverviewData; - loading: boolean; - refetch: inputsModel.Refetch; -} - -export interface IpOverviewProps extends QueryTemplateProps { - children: (args: IpOverviewArgs) => React.ReactNode; - type: networkModel.NetworkType; - ip: string; -} - -const IpOverviewComponentQuery = React.memo( - ({ id = ID, isInspected, children, filterQuery, skip, sourceId, ip }) => ( - - query={ipOverviewQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={{ - sourceId, - filterQuery: createFilter(filterQuery), - ip, - defaultIndex: useUiSetting(DEFAULT_INDEX_KEY), - inspect: isInspected, - }} - > - {({ data, loading, refetch }) => { - const init: IpOverviewData = { host: {} }; - const ipOverviewData: IpOverviewData = getOr(init, 'source.IpOverview', data); - return children({ - id, - inspect: getOr(null, 'source.IpOverview.inspect', data), - ipOverviewData, - loading, - refetch, - }); - }} - - ) -); - -IpOverviewComponentQuery.displayName = 'IpOverviewComponentQuery'; - -const makeMapStateToProps = () => { - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = ID }: IpOverviewProps) => { - const { isInspected } = getQuery(state, id); - return { - isInspected, - }; - }; - return mapStateToProps; -}; - -const connector = connect(makeMapStateToProps); - -type PropsFromRedux = ConnectedProps; - -export const IpOverviewQuery = connector(IpOverviewComponentQuery); diff --git a/x-pack/plugins/siem/public/containers/kpi_host_details/index.tsx b/x-pack/plugins/siem/public/containers/kpi_host_details/index.tsx deleted file mode 100644 index de9d54b1a185c8..00000000000000 --- a/x-pack/plugins/siem/public/containers/kpi_host_details/index.tsx +++ /dev/null @@ -1,85 +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 { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect, ConnectedProps } from 'react-redux'; - -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; -import { KpiHostDetailsData, GetKpiHostDetailsQuery } from '../../graphql/types'; -import { inputsModel, inputsSelectors, State } from '../../store'; -import { useUiSetting } from '../../lib/kibana'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { QueryTemplateProps } from '../query_template'; - -import { kpiHostDetailsQuery } from './index.gql_query'; - -const ID = 'kpiHostDetailsQuery'; - -export interface KpiHostDetailsArgs { - id: string; - inspect: inputsModel.InspectQuery; - kpiHostDetails: KpiHostDetailsData; - loading: boolean; - refetch: inputsModel.Refetch; -} - -export interface QueryKpiHostDetailsProps extends QueryTemplateProps { - children: (args: KpiHostDetailsArgs) => React.ReactNode; -} - -const KpiHostDetailsComponentQuery = React.memo( - ({ id = ID, children, endDate, filterQuery, isInspected, skip, sourceId, startDate }) => ( - - query={kpiHostDetailsQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={{ - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - filterQuery: createFilter(filterQuery), - defaultIndex: useUiSetting(DEFAULT_INDEX_KEY), - inspect: isInspected, - }} - > - {({ data, loading, refetch }) => { - const kpiHostDetails = getOr({}, `source.KpiHostDetails`, data); - return children({ - id, - inspect: getOr(null, 'source.KpiHostDetails.inspect', data), - kpiHostDetails, - loading, - refetch, - }); - }} - - ) -); - -KpiHostDetailsComponentQuery.displayName = 'KpiHostDetailsComponentQuery'; - -const makeMapStateToProps = () => { - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = ID }: QueryKpiHostDetailsProps) => { - const { isInspected } = getQuery(state, id); - return { - isInspected, - }; - }; - return mapStateToProps; -}; - -const connector = connect(makeMapStateToProps); - -type PropsFromRedux = ConnectedProps; - -export const KpiHostDetailsQuery = connector(KpiHostDetailsComponentQuery); diff --git a/x-pack/plugins/siem/public/containers/kpi_hosts/index.tsx b/x-pack/plugins/siem/public/containers/kpi_hosts/index.tsx deleted file mode 100644 index 5be2423e8a162a..00000000000000 --- a/x-pack/plugins/siem/public/containers/kpi_hosts/index.tsx +++ /dev/null @@ -1,85 +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 { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect, ConnectedProps } from 'react-redux'; - -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; -import { GetKpiHostsQuery, KpiHostsData } from '../../graphql/types'; -import { inputsModel, inputsSelectors, State } from '../../store'; -import { useUiSetting } from '../../lib/kibana'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { QueryTemplateProps } from '../query_template'; - -import { kpiHostsQuery } from './index.gql_query'; - -const ID = 'kpiHostsQuery'; - -export interface KpiHostsArgs { - id: string; - inspect: inputsModel.InspectQuery; - kpiHosts: KpiHostsData; - loading: boolean; - refetch: inputsModel.Refetch; -} - -export interface KpiHostsProps extends QueryTemplateProps { - children: (args: KpiHostsArgs) => React.ReactNode; -} - -const KpiHostsComponentQuery = React.memo( - ({ id = ID, children, endDate, filterQuery, isInspected, skip, sourceId, startDate }) => ( - - query={kpiHostsQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={{ - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - filterQuery: createFilter(filterQuery), - defaultIndex: useUiSetting(DEFAULT_INDEX_KEY), - inspect: isInspected, - }} - > - {({ data, loading, refetch }) => { - const kpiHosts = getOr({}, `source.KpiHosts`, data); - return children({ - id, - inspect: getOr(null, 'source.KpiHosts.inspect', data), - kpiHosts, - loading, - refetch, - }); - }} - - ) -); - -KpiHostsComponentQuery.displayName = 'KpiHostsComponentQuery'; - -const makeMapStateToProps = () => { - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = ID }: KpiHostsProps) => { - const { isInspected } = getQuery(state, id); - return { - isInspected, - }; - }; - return mapStateToProps; -}; - -const connector = connect(makeMapStateToProps); - -type PropsFromRedux = ConnectedProps; - -export const KpiHostsQuery = connector(KpiHostsComponentQuery); diff --git a/x-pack/plugins/siem/public/containers/kpi_network/index.tsx b/x-pack/plugins/siem/public/containers/kpi_network/index.tsx deleted file mode 100644 index 338cdc39b178c1..00000000000000 --- a/x-pack/plugins/siem/public/containers/kpi_network/index.tsx +++ /dev/null @@ -1,85 +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 { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect, ConnectedProps } from 'react-redux'; - -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; -import { GetKpiNetworkQuery, KpiNetworkData } from '../../graphql/types'; -import { inputsModel, inputsSelectors, State } from '../../store'; -import { useUiSetting } from '../../lib/kibana'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { QueryTemplateProps } from '../query_template'; - -import { kpiNetworkQuery } from './index.gql_query'; - -const ID = 'kpiNetworkQuery'; - -export interface KpiNetworkArgs { - id: string; - inspect: inputsModel.InspectQuery; - kpiNetwork: KpiNetworkData; - loading: boolean; - refetch: inputsModel.Refetch; -} - -export interface KpiNetworkProps extends QueryTemplateProps { - children: (args: KpiNetworkArgs) => React.ReactNode; -} - -const KpiNetworkComponentQuery = React.memo( - ({ id = ID, children, filterQuery, isInspected, skip, sourceId, startDate, endDate }) => ( - - query={kpiNetworkQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={{ - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - filterQuery: createFilter(filterQuery), - defaultIndex: useUiSetting(DEFAULT_INDEX_KEY), - inspect: isInspected, - }} - > - {({ data, loading, refetch }) => { - const kpiNetwork = getOr({}, `source.KpiNetwork`, data); - return children({ - id, - inspect: getOr(null, 'source.KpiNetwork.inspect', data), - kpiNetwork, - loading, - refetch, - }); - }} - - ) -); - -KpiNetworkComponentQuery.displayName = 'KpiNetworkComponentQuery'; - -const makeMapStateToProps = () => { - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = ID }: KpiNetworkProps) => { - const { isInspected } = getQuery(state, id); - return { - isInspected, - }; - }; - return mapStateToProps; -}; - -const connector = connect(makeMapStateToProps); - -type PropsFromRedux = ConnectedProps; - -export const KpiNetworkQuery = connector(KpiNetworkComponentQuery); diff --git a/x-pack/plugins/siem/public/containers/kuery_autocompletion/index.tsx b/x-pack/plugins/siem/public/containers/kuery_autocompletion/index.tsx deleted file mode 100644 index 6120538a01e78b..00000000000000 --- a/x-pack/plugins/siem/public/containers/kuery_autocompletion/index.tsx +++ /dev/null @@ -1,84 +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, { useState } from 'react'; -import { QuerySuggestion, IIndexPattern } from '../../../../../../src/plugins/data/public'; -import { useKibana } from '../../lib/kibana'; - -type RendererResult = React.ReactElement | null; -type RendererFunction = (args: RenderArgs) => Result; - -interface KueryAutocompletionLifecycleProps { - children: RendererFunction<{ - isLoadingSuggestions: boolean; - loadSuggestions: (expression: string, cursorPosition: number, maxSuggestions?: number) => void; - suggestions: QuerySuggestion[]; - }>; - indexPattern: IIndexPattern; -} - -interface KueryAutocompletionCurrentRequest { - expression: string; - cursorPosition: number; -} - -export const KueryAutocompletion = React.memo( - ({ children, indexPattern }) => { - const [currentRequest, setCurrentRequest] = useState( - null - ); - const [suggestions, setSuggestions] = useState([]); - const kibana = useKibana(); - const loadSuggestions = async ( - expression: string, - cursorPosition: number, - maxSuggestions?: number - ) => { - const language = 'kuery'; - - if (!kibana.services.data.autocomplete.hasQuerySuggestions(language)) { - return; - } - - const futureRequest = { - expression, - cursorPosition, - }; - setCurrentRequest({ - expression, - cursorPosition, - }); - setSuggestions([]); - - if ( - futureRequest && - futureRequest.expression !== (currentRequest && currentRequest.expression) && - futureRequest.cursorPosition !== (currentRequest && currentRequest.cursorPosition) - ) { - const newSuggestions = - (await kibana.services.data.autocomplete.getQuerySuggestions({ - language: 'kuery', - indexPatterns: [indexPattern], - boolFilter: [], - query: expression, - selectionStart: cursorPosition, - selectionEnd: cursorPosition, - })) || []; - - setCurrentRequest(null); - setSuggestions(maxSuggestions ? newSuggestions.slice(0, maxSuggestions) : newSuggestions); - } - }; - - return children({ - isLoadingSuggestions: currentRequest !== null, - loadSuggestions, - suggestions, - }); - } -); - -KueryAutocompletion.displayName = 'KueryAutocompletion'; diff --git a/x-pack/plugins/siem/public/containers/matrix_histogram/index.test.tsx b/x-pack/plugins/siem/public/containers/matrix_histogram/index.test.tsx deleted file mode 100644 index 80899a061e7c16..00000000000000 --- a/x-pack/plugins/siem/public/containers/matrix_histogram/index.test.tsx +++ /dev/null @@ -1,157 +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 { useQuery } from '.'; -import { mount } from 'enzyme'; -import React from 'react'; -import { useApolloClient } from '../../utils/apollo_context'; -import { errorToToaster } from '../../components/toasters'; -import { MatrixOverTimeHistogramData, HistogramType } from '../../graphql/types'; -import { InspectQuery, Refetch } from '../../store/inputs/model'; - -const mockQuery = jest.fn().mockResolvedValue({ - data: { - source: { - MatrixHistogram: { - matrixHistogramData: [{}], - totalCount: 1, - inspect: false, - }, - }, - }, -}); - -const mockRejectQuery = jest.fn().mockRejectedValue(new Error()); -jest.mock('../../utils/apollo_context', () => ({ - useApolloClient: jest.fn(), -})); - -jest.mock('../../lib/kibana', () => { - return { - useUiSetting$: jest.fn().mockReturnValue(['mockDefaultIndex']), - }; -}); - -jest.mock('./index.gql_query', () => { - return { - MatrixHistogramGqlQuery: 'mockGqlQuery', - }; -}); - -jest.mock('../../components/toasters/', () => ({ - useStateToaster: () => [jest.fn(), jest.fn()], - errorToToaster: jest.fn(), -})); - -describe('useQuery', () => { - let result: { - data: MatrixOverTimeHistogramData[] | null; - loading: boolean; - inspect: InspectQuery | null; - totalCount: number; - refetch: Refetch | undefined; - }; - describe('happy path', () => { - beforeAll(() => { - (useApolloClient as jest.Mock).mockReturnValue({ - query: mockQuery, - }); - const TestComponent = () => { - result = useQuery({ - endDate: 100, - errorMessage: 'fakeErrorMsg', - filterQuery: '', - histogramType: HistogramType.alerts, - isInspected: false, - stackByField: 'fakeField', - startDate: 0, - }); - - return
; - }; - - mount(); - }); - - test('should set variables', () => { - expect(mockQuery).toBeCalledWith({ - query: 'mockGqlQuery', - fetchPolicy: 'network-only', - variables: { - filterQuery: '', - sourceId: 'default', - timerange: { - interval: '12h', - from: 0, - to: 100, - }, - defaultIndex: 'mockDefaultIndex', - inspect: false, - stackByField: 'fakeField', - histogramType: 'alerts', - }, - context: { - fetchOptions: { - abortSignal: new AbortController().signal, - }, - }, - }); - }); - - test('should setData', () => { - expect(result.data).toEqual([{}]); - }); - - test('should set total count', () => { - expect(result.totalCount).toEqual(1); - }); - - test('should set inspect', () => { - expect(result.inspect).toEqual(false); - }); - }); - - describe('failure path', () => { - beforeAll(() => { - mockQuery.mockClear(); - (useApolloClient as jest.Mock).mockReset(); - (useApolloClient as jest.Mock).mockReturnValue({ - query: mockRejectQuery, - }); - const TestComponent = () => { - result = useQuery({ - endDate: 100, - errorMessage: 'fakeErrorMsg', - filterQuery: '', - histogramType: HistogramType.alerts, - isInspected: false, - stackByField: 'fakeField', - startDate: 0, - }); - - return
; - }; - - mount(); - }); - - test('should setData', () => { - expect(result.data).toEqual(null); - }); - - test('should set total count', () => { - expect(result.totalCount).toEqual(-1); - }); - - test('should set inspect', () => { - expect(result.inspect).toEqual(null); - }); - - test('should set error to toster', () => { - expect(errorToToaster).toHaveBeenCalled(); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/containers/matrix_histogram/index.ts b/x-pack/plugins/siem/public/containers/matrix_histogram/index.ts deleted file mode 100644 index 18bb611191bbc8..00000000000000 --- a/x-pack/plugins/siem/public/containers/matrix_histogram/index.ts +++ /dev/null @@ -1,119 +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 { isEmpty } from 'lodash/fp'; -import { useEffect, useMemo, useState, useRef } from 'react'; - -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; -import { MatrixHistogramQueryProps } from '../../components/matrix_histogram/types'; -import { errorToToaster, useStateToaster } from '../../components/toasters'; -import { useUiSetting$ } from '../../lib/kibana'; -import { createFilter } from '../helpers'; -import { useApolloClient } from '../../utils/apollo_context'; -import { inputsModel } from '../../store'; -import { MatrixHistogramGqlQuery } from './index.gql_query'; -import { GetMatrixHistogramQuery, MatrixOverTimeHistogramData } from '../../graphql/types'; - -export const useQuery = ({ - endDate, - errorMessage, - filterQuery, - histogramType, - indexToAdd, - isInspected, - stackByField, - startDate, -}: MatrixHistogramQueryProps) => { - const [configIndex] = useUiSetting$(DEFAULT_INDEX_KEY); - const defaultIndex = useMemo(() => { - if (indexToAdd != null && !isEmpty(indexToAdd)) { - return [...configIndex, ...indexToAdd]; - } - return configIndex; - }, [configIndex, indexToAdd]); - - const [, dispatchToaster] = useStateToaster(); - const refetch = useRef(); - const [loading, setLoading] = useState(false); - const [data, setData] = useState(null); - const [inspect, setInspect] = useState(null); - const [totalCount, setTotalCount] = useState(-1); - const apolloClient = useApolloClient(); - - useEffect(() => { - const matrixHistogramVariables: GetMatrixHistogramQuery.Variables = { - filterQuery: createFilter(filterQuery), - sourceId: 'default', - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - defaultIndex, - inspect: isInspected, - stackByField, - histogramType, - }; - let isSubscribed = true; - const abortCtrl = new AbortController(); - const abortSignal = abortCtrl.signal; - - async function fetchData() { - if (!apolloClient) return null; - setLoading(true); - return apolloClient - .query({ - query: MatrixHistogramGqlQuery, - fetchPolicy: 'network-only', - variables: matrixHistogramVariables, - context: { - fetchOptions: { - abortSignal, - }, - }, - }) - .then( - result => { - if (isSubscribed) { - const source = result?.data?.source?.MatrixHistogram ?? {}; - setData(source?.matrixHistogramData ?? []); - setTotalCount(source?.totalCount ?? -1); - setInspect(source?.inspect ?? null); - setLoading(false); - } - }, - error => { - if (isSubscribed) { - setData(null); - setTotalCount(-1); - setInspect(null); - setLoading(false); - errorToToaster({ title: errorMessage, error, dispatchToaster }); - } - } - ); - } - refetch.current = fetchData; - fetchData(); - return () => { - isSubscribed = false; - abortCtrl.abort(); - }; - }, [ - defaultIndex, - errorMessage, - filterQuery, - histogramType, - indexToAdd, - isInspected, - stackByField, - startDate, - endDate, - data, - ]); - - return { data, loading, inspect, totalCount, refetch: refetch.current }; -}; diff --git a/x-pack/plugins/siem/public/containers/network_dns/index.tsx b/x-pack/plugins/siem/public/containers/network_dns/index.tsx deleted file mode 100644 index 04c8783c30a0ff..00000000000000 --- a/x-pack/plugins/siem/public/containers/network_dns/index.tsx +++ /dev/null @@ -1,209 +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 { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; - -import { DocumentNode } from 'graphql'; -import { ScaleType } from '@elastic/charts'; -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; -import { - GetNetworkDnsQuery, - NetworkDnsEdges, - NetworkDnsSortField, - PageInfoPaginated, - MatrixOverOrdinalHistogramData, -} from '../../graphql/types'; -import { inputsModel, networkModel, networkSelectors, State, inputsSelectors } from '../../store'; -import { withKibana, WithKibanaProps } from '../../lib/kibana'; -import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; -import { networkDnsQuery } from './index.gql_query'; -import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from '../../store/constants'; -import { MatrixHistogram } from '../../components/matrix_histogram'; -import { MatrixHistogramOption, GetSubTitle } from '../../components/matrix_histogram/types'; -import { UpdateDateRange } from '../../components/charts/common'; -import { SetQuery } from '../../pages/hosts/navigation/types'; - -const ID = 'networkDnsQuery'; -export const HISTOGRAM_ID = 'networkDnsHistogramQuery'; -export interface NetworkDnsArgs { - id: string; - inspect: inputsModel.InspectQuery; - isInspected: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - networkDns: NetworkDnsEdges[]; - pageInfo: PageInfoPaginated; - refetch: inputsModel.Refetch; - stackByField?: string; - totalCount: number; - histogram: MatrixOverOrdinalHistogramData[]; -} - -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: NetworkDnsArgs) => React.ReactNode; - type: networkModel.NetworkType; -} - -interface DnsHistogramOwnProps extends QueryTemplatePaginatedProps { - dataKey: string | string[]; - defaultStackByOption: MatrixHistogramOption; - errorMessage: string; - isDnsHistogram?: boolean; - query: DocumentNode; - scaleType: ScaleType; - setQuery: SetQuery; - showLegend?: boolean; - stackByOptions: MatrixHistogramOption[]; - subtitle?: string | GetSubTitle; - title: string; - type: networkModel.NetworkType; - updateDateRange: UpdateDateRange; - yTickFormatter?: (value: number) => string; -} - -export interface NetworkDnsComponentReduxProps { - activePage: number; - sort: NetworkDnsSortField; - isInspected: boolean; - isPtrIncluded: boolean; - limit: number; -} - -type NetworkDnsProps = OwnProps & NetworkDnsComponentReduxProps & WithKibanaProps; - -export class NetworkDnsComponentQuery extends QueryTemplatePaginated< - NetworkDnsProps, - GetNetworkDnsQuery.Query, - GetNetworkDnsQuery.Variables -> { - public render() { - const { - activePage, - children, - sort, - endDate, - filterQuery, - id = ID, - isInspected, - isPtrIncluded, - kibana, - limit, - skip, - sourceId, - startDate, - } = this.props; - const variables: GetNetworkDnsQuery.Variables = { - defaultIndex: kibana.services.uiSettings.get(DEFAULT_INDEX_KEY), - filterQuery: createFilter(filterQuery), - inspect: isInspected, - isPtrIncluded, - pagination: generateTablePaginationOptions(activePage, limit), - sort, - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - }; - - return ( - - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - query={networkDnsQuery} - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const networkDns = getOr([], `source.NetworkDns.edges`, data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), - }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; - } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - NetworkDns: { - ...fetchMoreResult.source.NetworkDns, - edges: [...fetchMoreResult.source.NetworkDns.edges], - }, - }, - }; - }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - id, - inspect: getOr(null, 'source.NetworkDns.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - networkDns, - pageInfo: getOr({}, 'source.NetworkDns.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - totalCount: getOr(-1, 'source.NetworkDns.totalCount', data), - histogram: getOr(null, 'source.NetworkDns.histogram', data), - }); - }} - - ); - } -} - -const makeMapStateToProps = () => { - const getNetworkDnsSelector = networkSelectors.dnsSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = ID }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getNetworkDnsSelector(state), - isInspected, - id, - }; - }; - - return mapStateToProps; -}; - -const makeMapHistogramStateToProps = () => { - const getNetworkDnsSelector = networkSelectors.dnsSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = HISTOGRAM_ID }: DnsHistogramOwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getNetworkDnsSelector(state), - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - limit: DEFAULT_TABLE_LIMIT, - isInspected, - id, - }; - }; - - return mapStateToProps; -}; - -export const NetworkDnsQuery = compose>( - connect(makeMapStateToProps), - withKibana -)(NetworkDnsComponentQuery); - -export const NetworkDnsHistogramQuery = compose>( - connect(makeMapHistogramStateToProps), - withKibana -)(MatrixHistogram); diff --git a/x-pack/plugins/siem/public/containers/network_http/index.tsx b/x-pack/plugins/siem/public/containers/network_http/index.tsx deleted file mode 100644 index bf4e64f63d5599..00000000000000 --- a/x-pack/plugins/siem/public/containers/network_http/index.tsx +++ /dev/null @@ -1,156 +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 { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; - -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; -import { - GetNetworkHttpQuery, - NetworkHttpEdges, - NetworkHttpSortField, - PageInfoPaginated, -} from '../../graphql/types'; -import { inputsModel, inputsSelectors, networkModel, networkSelectors, State } from '../../store'; -import { withKibana, WithKibanaProps } from '../../lib/kibana'; -import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; -import { networkHttpQuery } from './index.gql_query'; - -const ID = 'networkHttpQuery'; - -export interface NetworkHttpArgs { - id: string; - ip?: string; - inspect: inputsModel.InspectQuery; - isInspected: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - networkHttp: NetworkHttpEdges[]; - pageInfo: PageInfoPaginated; - refetch: inputsModel.Refetch; - totalCount: number; -} - -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: NetworkHttpArgs) => React.ReactNode; - ip?: string; - type: networkModel.NetworkType; -} - -export interface NetworkHttpComponentReduxProps { - activePage: number; - isInspected: boolean; - limit: number; - sort: NetworkHttpSortField; -} - -type NetworkHttpProps = OwnProps & NetworkHttpComponentReduxProps & WithKibanaProps; - -class NetworkHttpComponentQuery extends QueryTemplatePaginated< - NetworkHttpProps, - GetNetworkHttpQuery.Query, - GetNetworkHttpQuery.Variables -> { - public render() { - const { - activePage, - children, - endDate, - filterQuery, - id = ID, - ip, - isInspected, - kibana, - limit, - skip, - sourceId, - sort, - startDate, - } = this.props; - const variables: GetNetworkHttpQuery.Variables = { - defaultIndex: kibana.services.uiSettings.get(DEFAULT_INDEX_KEY), - filterQuery: createFilter(filterQuery), - inspect: isInspected, - ip, - pagination: generateTablePaginationOptions(activePage, limit), - sort, - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - }; - return ( - - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - query={networkHttpQuery} - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const networkHttp = getOr([], `source.NetworkHttp.edges`, data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), - }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; - } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - NetworkHttp: { - ...fetchMoreResult.source.NetworkHttp, - edges: [...fetchMoreResult.source.NetworkHttp.edges], - }, - }, - }; - }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - id, - inspect: getOr(null, 'source.NetworkHttp.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - networkHttp, - pageInfo: getOr({}, 'source.NetworkHttp.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - totalCount: getOr(-1, 'source.NetworkHttp.totalCount', data), - }); - }} - - ); - } -} - -const makeMapStateToProps = () => { - const getHttpSelector = networkSelectors.httpSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - return (state: State, { id = ID, type }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getHttpSelector(state, type), - isInspected, - }; - }; -}; - -export const NetworkHttpQuery = compose>( - connect(makeMapStateToProps), - withKibana -)(NetworkHttpComponentQuery); diff --git a/x-pack/plugins/siem/public/containers/network_top_countries/index.tsx b/x-pack/plugins/siem/public/containers/network_top_countries/index.tsx deleted file mode 100644 index bd1e1a002bbcdc..00000000000000 --- a/x-pack/plugins/siem/public/containers/network_top_countries/index.tsx +++ /dev/null @@ -1,160 +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 { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; - -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; -import { - FlowTargetSourceDest, - GetNetworkTopCountriesQuery, - NetworkTopCountriesEdges, - NetworkTopTablesSortField, - PageInfoPaginated, -} from '../../graphql/types'; -import { inputsModel, inputsSelectors, networkModel, networkSelectors, State } from '../../store'; -import { withKibana, WithKibanaProps } from '../../lib/kibana'; -import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; -import { networkTopCountriesQuery } from './index.gql_query'; - -const ID = 'networkTopCountriesQuery'; - -export interface NetworkTopCountriesArgs { - id: string; - ip?: string; - inspect: inputsModel.InspectQuery; - isInspected: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - networkTopCountries: NetworkTopCountriesEdges[]; - pageInfo: PageInfoPaginated; - refetch: inputsModel.Refetch; - totalCount: number; -} - -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: NetworkTopCountriesArgs) => React.ReactNode; - flowTarget: FlowTargetSourceDest; - ip?: string; - type: networkModel.NetworkType; -} - -export interface NetworkTopCountriesComponentReduxProps { - activePage: number; - isInspected: boolean; - limit: number; - sort: NetworkTopTablesSortField; -} - -type NetworkTopCountriesProps = OwnProps & NetworkTopCountriesComponentReduxProps & WithKibanaProps; - -class NetworkTopCountriesComponentQuery extends QueryTemplatePaginated< - NetworkTopCountriesProps, - GetNetworkTopCountriesQuery.Query, - GetNetworkTopCountriesQuery.Variables -> { - public render() { - const { - activePage, - children, - endDate, - flowTarget, - filterQuery, - kibana, - id = `${ID}-${flowTarget}`, - ip, - isInspected, - limit, - skip, - sourceId, - startDate, - sort, - } = this.props; - const variables: GetNetworkTopCountriesQuery.Variables = { - defaultIndex: kibana.services.uiSettings.get(DEFAULT_INDEX_KEY), - filterQuery: createFilter(filterQuery), - flowTarget, - inspect: isInspected, - ip, - pagination: generateTablePaginationOptions(activePage, limit), - sort, - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - }; - return ( - - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - query={networkTopCountriesQuery} - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const networkTopCountries = getOr([], `source.NetworkTopCountries.edges`, data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), - }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; - } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - NetworkTopCountries: { - ...fetchMoreResult.source.NetworkTopCountries, - edges: [...fetchMoreResult.source.NetworkTopCountries.edges], - }, - }, - }; - }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - id, - inspect: getOr(null, 'source.NetworkTopCountries.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - networkTopCountries, - pageInfo: getOr({}, 'source.NetworkTopCountries.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - totalCount: getOr(-1, 'source.NetworkTopCountries.totalCount', data), - }); - }} - - ); - } -} - -const makeMapStateToProps = () => { - const getTopCountriesSelector = networkSelectors.topCountriesSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - return (state: State, { flowTarget, id = `${ID}-${flowTarget}`, type }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getTopCountriesSelector(state, type, flowTarget), - isInspected, - }; - }; -}; - -export const NetworkTopCountriesQuery = compose>( - connect(makeMapStateToProps), - withKibana -)(NetworkTopCountriesComponentQuery); diff --git a/x-pack/plugins/siem/public/containers/network_top_n_flow/index.tsx b/x-pack/plugins/siem/public/containers/network_top_n_flow/index.tsx deleted file mode 100644 index f0f1f8257f29f1..00000000000000 --- a/x-pack/plugins/siem/public/containers/network_top_n_flow/index.tsx +++ /dev/null @@ -1,160 +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 { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; - -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; -import { - FlowTargetSourceDest, - GetNetworkTopNFlowQuery, - NetworkTopNFlowEdges, - NetworkTopTablesSortField, - PageInfoPaginated, -} from '../../graphql/types'; -import { withKibana, WithKibanaProps } from '../../lib/kibana'; -import { inputsModel, inputsSelectors, networkModel, networkSelectors, State } from '../../store'; -import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; -import { networkTopNFlowQuery } from './index.gql_query'; - -const ID = 'networkTopNFlowQuery'; - -export interface NetworkTopNFlowArgs { - id: string; - ip?: string; - inspect: inputsModel.InspectQuery; - isInspected: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - networkTopNFlow: NetworkTopNFlowEdges[]; - pageInfo: PageInfoPaginated; - refetch: inputsModel.Refetch; - totalCount: number; -} - -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: NetworkTopNFlowArgs) => React.ReactNode; - flowTarget: FlowTargetSourceDest; - ip?: string; - type: networkModel.NetworkType; -} - -export interface NetworkTopNFlowComponentReduxProps { - activePage: number; - isInspected: boolean; - limit: number; - sort: NetworkTopTablesSortField; -} - -type NetworkTopNFlowProps = OwnProps & NetworkTopNFlowComponentReduxProps & WithKibanaProps; - -class NetworkTopNFlowComponentQuery extends QueryTemplatePaginated< - NetworkTopNFlowProps, - GetNetworkTopNFlowQuery.Query, - GetNetworkTopNFlowQuery.Variables -> { - public render() { - const { - activePage, - children, - endDate, - flowTarget, - filterQuery, - kibana, - id = `${ID}-${flowTarget}`, - ip, - isInspected, - limit, - skip, - sourceId, - startDate, - sort, - } = this.props; - const variables: GetNetworkTopNFlowQuery.Variables = { - defaultIndex: kibana.services.uiSettings.get(DEFAULT_INDEX_KEY), - filterQuery: createFilter(filterQuery), - flowTarget, - inspect: isInspected, - ip, - pagination: generateTablePaginationOptions(activePage, limit), - sort, - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - }; - return ( - - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - query={networkTopNFlowQuery} - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const networkTopNFlow = getOr([], `source.NetworkTopNFlow.edges`, data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), - }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; - } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - NetworkTopNFlow: { - ...fetchMoreResult.source.NetworkTopNFlow, - edges: [...fetchMoreResult.source.NetworkTopNFlow.edges], - }, - }, - }; - }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - id, - inspect: getOr(null, 'source.NetworkTopNFlow.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - networkTopNFlow, - pageInfo: getOr({}, 'source.NetworkTopNFlow.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - totalCount: getOr(-1, 'source.NetworkTopNFlow.totalCount', data), - }); - }} - - ); - } -} - -const makeMapStateToProps = () => { - const getTopNFlowSelector = networkSelectors.topNFlowSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - return (state: State, { flowTarget, id = `${ID}-${flowTarget}`, type }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getTopNFlowSelector(state, type, flowTarget), - isInspected, - }; - }; -}; - -export const NetworkTopNFlowQuery = compose>( - connect(makeMapStateToProps), - withKibana -)(NetworkTopNFlowComponentQuery); diff --git a/x-pack/plugins/siem/public/containers/overview/overview_host/index.tsx b/x-pack/plugins/siem/public/containers/overview/overview_host/index.tsx deleted file mode 100644 index 2dd9ccf24d802f..00000000000000 --- a/x-pack/plugins/siem/public/containers/overview/overview_host/index.tsx +++ /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. - */ - -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect, ConnectedProps } from 'react-redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; -import { GetOverviewHostQuery, OverviewHostData } from '../../../graphql/types'; -import { useUiSetting } from '../../../lib/kibana'; -import { inputsModel, inputsSelectors } from '../../../store/inputs'; -import { State } from '../../../store'; -import { createFilter, getDefaultFetchPolicy } from '../../helpers'; -import { QueryTemplateProps } from '../../query_template'; - -import { overviewHostQuery } from './index.gql_query'; - -export const ID = 'overviewHostQuery'; - -export interface OverviewHostArgs { - id: string; - inspect: inputsModel.InspectQuery; - loading: boolean; - overviewHost: OverviewHostData; - refetch: inputsModel.Refetch; -} - -export interface OverviewHostProps extends QueryTemplateProps { - children: (args: OverviewHostArgs) => React.ReactNode; - sourceId: string; - endDate: number; - startDate: number; -} - -const OverviewHostComponentQuery = React.memo( - ({ id = ID, children, filterQuery, isInspected, sourceId, startDate, endDate }) => { - return ( - - query={overviewHostQuery} - fetchPolicy={getDefaultFetchPolicy()} - variables={{ - sourceId, - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, - filterQuery: createFilter(filterQuery), - defaultIndex: useUiSetting(DEFAULT_INDEX_KEY), - inspect: isInspected, - }} - > - {({ data, loading, refetch }) => { - const overviewHost = getOr({}, `source.OverviewHost`, data); - return children({ - id, - inspect: getOr(null, 'source.OverviewHost.inspect', data), - overviewHost, - loading, - refetch, - }); - }} - - ); - } -); - -OverviewHostComponentQuery.displayName = 'OverviewHostComponentQuery'; - -const makeMapStateToProps = () => { - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = ID }: OverviewHostProps) => { - const { isInspected } = getQuery(state, id); - return { - isInspected, - }; - }; - return mapStateToProps; -}; - -const connector = connect(makeMapStateToProps); - -type PropsFromRedux = ConnectedProps; - -export const OverviewHostQuery = connector(OverviewHostComponentQuery); diff --git a/x-pack/plugins/siem/public/containers/overview/overview_network/index.tsx b/x-pack/plugins/siem/public/containers/overview/overview_network/index.tsx deleted file mode 100644 index d0acd41c224a5f..00000000000000 --- a/x-pack/plugins/siem/public/containers/overview/overview_network/index.tsx +++ /dev/null @@ -1,88 +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 { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect, ConnectedProps } from 'react-redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; -import { GetOverviewNetworkQuery, OverviewNetworkData } from '../../../graphql/types'; -import { useUiSetting } from '../../../lib/kibana'; -import { State } from '../../../store'; -import { inputsModel, inputsSelectors } from '../../../store/inputs'; -import { createFilter, getDefaultFetchPolicy } from '../../helpers'; -import { QueryTemplateProps } from '../../query_template'; - -import { overviewNetworkQuery } from './index.gql_query'; - -export const ID = 'overviewNetworkQuery'; - -export interface OverviewNetworkArgs { - id: string; - inspect: inputsModel.InspectQuery; - overviewNetwork: OverviewNetworkData; - loading: boolean; - refetch: inputsModel.Refetch; -} - -export interface OverviewNetworkProps extends QueryTemplateProps { - children: (args: OverviewNetworkArgs) => React.ReactNode; - sourceId: string; - endDate: number; - startDate: number; -} - -export const OverviewNetworkComponentQuery = React.memo( - ({ id = ID, children, filterQuery, isInspected, sourceId, startDate, endDate }) => ( - - query={overviewNetworkQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - variables={{ - sourceId, - timerange: { - interval: '12h', - from: startDate, - to: endDate, - }, - filterQuery: createFilter(filterQuery), - defaultIndex: useUiSetting(DEFAULT_INDEX_KEY), - inspect: isInspected, - }} - > - {({ data, loading, refetch }) => { - const overviewNetwork = getOr({}, `source.OverviewNetwork`, data); - return children({ - id, - inspect: getOr(null, 'source.OverviewNetwork.inspect', data), - overviewNetwork, - loading, - refetch, - }); - }} - - ) -); - -OverviewNetworkComponentQuery.displayName = 'OverviewNetworkComponentQuery'; - -const makeMapStateToProps = () => { - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = ID }: OverviewNetworkProps) => { - const { isInspected } = getQuery(state, id); - return { - isInspected, - }; - }; - return mapStateToProps; -}; - -const connector = connect(makeMapStateToProps); - -type PropsFromRedux = ConnectedProps; - -export const OverviewNetworkQuery = connector(OverviewNetworkComponentQuery); diff --git a/x-pack/plugins/siem/public/containers/source/index.tsx b/x-pack/plugins/siem/public/containers/source/index.tsx deleted file mode 100644 index e9359fdb195877..00000000000000 --- a/x-pack/plugins/siem/public/containers/source/index.tsx +++ /dev/null @@ -1,177 +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 { isUndefined } from 'lodash'; -import { get, keyBy, pick, set, isEmpty } from 'lodash/fp'; -import { Query } from 'react-apollo'; -import React, { useEffect, useMemo, useState } from 'react'; -import memoizeOne from 'memoize-one'; -import { IIndexPattern } from 'src/plugins/data/public'; - -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; -import { useUiSetting$ } from '../../lib/kibana'; - -import { IndexField, SourceQuery } from '../../graphql/types'; - -import { sourceQuery } from './index.gql_query'; -import { useApolloClient } from '../../utils/apollo_context'; - -export { sourceQuery }; - -export interface BrowserField { - aggregatable: boolean; - category: string; - description: string | null; - example: string | number | null; - fields: Readonly>>; - format: string; - indexes: string[]; - name: string; - searchable: boolean; - type: string; -} - -export type BrowserFields = Readonly>>; - -export const getAllBrowserFields = (browserFields: BrowserFields): Array> => - Object.values(browserFields).reduce>>( - (acc, namespace) => [ - ...acc, - ...Object.values(namespace.fields != null ? namespace.fields : {}), - ], - [] - ); - -export const getAllFieldsByName = ( - browserFields: BrowserFields -): { [fieldName: string]: Partial } => - keyBy('name', getAllBrowserFields(browserFields)); - -interface WithSourceArgs { - indicesExist: boolean; - browserFields: BrowserFields; - indexPattern: IIndexPattern; -} - -interface WithSourceProps { - children: (args: WithSourceArgs) => React.ReactNode; - indexToAdd?: string[] | null; - sourceId: string; -} - -export const getIndexFields = memoizeOne( - (title: string, fields: IndexField[]): IIndexPattern => - fields && fields.length > 0 - ? { - fields: fields.map(field => pick(['name', 'searchable', 'type', 'aggregatable'], field)), - title, - } - : { fields: [], title } -); - -export const getBrowserFields = memoizeOne( - (title: string, fields: IndexField[]): BrowserFields => - fields && fields.length > 0 - ? fields.reduce( - (accumulator: BrowserFields, field: IndexField) => - set([field.category, 'fields', field.name], field, accumulator), - {} - ) - : {} -); - -export const WithSource = React.memo(({ children, indexToAdd, sourceId }) => { - const [configIndex] = useUiSetting$(DEFAULT_INDEX_KEY); - const defaultIndex = useMemo(() => { - if (indexToAdd != null && !isEmpty(indexToAdd)) { - return [...configIndex, ...indexToAdd]; - } - return configIndex; - }, [configIndex, indexToAdd]); - - return ( - - query={sourceQuery} - fetchPolicy="cache-first" - notifyOnNetworkStatusChange - variables={{ - sourceId, - defaultIndex, - }} - > - {({ data }) => - children({ - indicesExist: get('source.status.indicesExist', data), - browserFields: getBrowserFields( - defaultIndex.join(), - get('source.status.indexFields', data) - ), - indexPattern: getIndexFields(defaultIndex.join(), get('source.status.indexFields', data)), - }) - } - - ); -}); - -WithSource.displayName = 'WithSource'; - -export const indicesExistOrDataTemporarilyUnavailable = (indicesExist: boolean | undefined) => - indicesExist || isUndefined(indicesExist); - -export const useWithSource = (sourceId: string, indices: string[]) => { - const [loading, updateLoading] = useState(false); - const [indicesExist, setIndicesExist] = useState(undefined); - const [browserFields, setBrowserFields] = useState(null); - const [indexPattern, setIndexPattern] = useState(null); - const [errorMessage, updateErrorMessage] = useState(null); - - const apolloClient = useApolloClient(); - async function fetchSource(signal: AbortSignal) { - updateLoading(true); - if (apolloClient) { - apolloClient - .query({ - query: sourceQuery, - fetchPolicy: 'cache-first', - variables: { - sourceId, - defaultIndex: indices, - }, - context: { - fetchOptions: { - signal, - }, - }, - }) - .then( - result => { - updateLoading(false); - updateErrorMessage(null); - setIndicesExist(get('data.source.status.indicesExist', result)); - setBrowserFields( - getBrowserFields(indices.join(), get('data.source.status.indexFields', result)) - ); - setIndexPattern( - getIndexFields(indices.join(), get('data.source.status.indexFields', result)) - ); - }, - error => { - updateLoading(false); - updateErrorMessage(error.message); - } - ); - } - } - - useEffect(() => { - const abortCtrl = new AbortController(); - const signal = abortCtrl.signal; - fetchSource(signal); - return () => abortCtrl.abort(); - }, [apolloClient, sourceId, indices]); - - return { indicesExist, browserFields, indexPattern, loading, errorMessage }; -}; diff --git a/x-pack/plugins/siem/public/containers/source/mock.ts b/x-pack/plugins/siem/public/containers/source/mock.ts deleted file mode 100644 index 092aad9e7400cc..00000000000000 --- a/x-pack/plugins/siem/public/containers/source/mock.ts +++ /dev/null @@ -1,699 +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_INDEX_PATTERN } from '../../../common/constants'; - -import { BrowserFields } from '.'; -import { sourceQuery } from './index.gql_query'; - -export const mocksSource = [ - { - request: { - query: sourceQuery, - variables: { - sourceId: 'default', - defaultIndex: DEFAULT_INDEX_PATTERN, - }, - }, - result: { - data: { - source: { - id: 'default', - configuration: {}, - status: { - indicesExist: true, - winlogbeatIndices: [ - 'winlogbeat-7.0.0-2019.02.17', - 'winlogbeat-7.0.0-2019.02.18', - 'winlogbeat-7.0.0-2019.02.19', - 'winlogbeat-7.0.0-2019.02.20', - 'winlogbeat-7.0.0-2019.02.21', - 'winlogbeat-7.0.0-2019.02.21-000001', - 'winlogbeat-7.0.0-2019.02.22', - 'winlogbeat-8.0.0-2019.02.19-000001', - ], - auditbeatIndices: [ - 'auditbeat-7.0.0-2019.02.17', - 'auditbeat-7.0.0-2019.02.18', - 'auditbeat-7.0.0-2019.02.19', - 'auditbeat-7.0.0-2019.02.20', - 'auditbeat-7.0.0-2019.02.21', - 'auditbeat-7.0.0-2019.02.21-000001', - 'auditbeat-7.0.0-2019.02.22', - 'auditbeat-8.0.0-2019.02.19-000001', - ], - filebeatIndices: [ - 'filebeat-7.0.0-iot-2019.06', - 'filebeat-7.0.0-iot-2019.07', - 'filebeat-7.0.0-iot-2019.08', - 'filebeat-7.0.0-iot-2019.09', - 'filebeat-7.0.0-iot-2019.10', - 'filebeat-8.0.0-2019.02.19-000001', - ], - indexFields: [ - { - category: 'base', - description: - 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', - example: '2016-05-23T08:05:34.853Z', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: '@timestamp', - searchable: true, - type: 'date', - aggregatable: true, - }, - { - category: 'agent', - description: - 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', - example: '8a4f500f', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.ephemeral_id', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'agent', - description: null, - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.hostname', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'agent', - description: - 'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.', - example: '8a4f500d', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.id', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'agent', - description: - 'Name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.', - example: 'foo', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.name', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'auditd', - description: null, - example: null, - format: '', - indexes: ['auditbeat'], - name: 'auditd.data.a0', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'auditd', - description: null, - example: null, - format: '', - indexes: ['auditbeat'], - name: 'auditd.data.a1', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'auditd', - description: null, - example: null, - format: '', - indexes: ['auditbeat'], - name: 'auditd.data.a2', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'client', - description: - 'Some event client addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.address', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'client', - description: 'Bytes sent from the client to the server.', - example: '184', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.bytes', - searchable: true, - type: 'number', - aggregatable: true, - }, - { - category: 'client', - description: 'Client domain.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.domain', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'client', - description: 'Country ISO code.', - example: 'CA', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.geo.country_iso_code', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'cloud', - description: - 'The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.', - example: '666777888999', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'cloud.account.id', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'cloud', - description: 'Availability zone in which this host is running.', - example: 'us-east-1c', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'cloud.availability_zone', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'container', - description: 'Unique container id.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'container.id', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'container', - description: 'Name of the image the container was built on.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'container.image.name', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'container', - description: 'Container image tag.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'container.image.tag', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'destination', - description: - 'Some event destination addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.address', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - category: 'destination', - description: 'Bytes sent from the destination to the source.', - example: '184', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.bytes', - searchable: true, - type: 'number', - aggregatable: true, - }, - { - category: 'destination', - description: 'Destination domain.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.domain', - searchable: true, - type: 'string', - aggregatable: true, - }, - { - aggregatable: true, - category: 'destination', - description: - 'IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.', - example: '', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.ip', - searchable: true, - type: 'ip', - }, - { - aggregatable: true, - category: 'destination', - description: 'Port of the destination.', - example: '', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.port', - searchable: true, - type: 'long', - }, - { - aggregatable: true, - category: 'source', - description: - 'IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.', - example: '', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'source.ip', - searchable: true, - type: 'ip', - }, - { - aggregatable: true, - category: 'source', - description: 'Port of the source.', - example: '', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'source.port', - searchable: true, - type: 'long', - }, - { - aggregatable: true, - category: 'event', - description: - 'event.end contains the date when the event ended or when the activity was last observed.', - example: null, - format: '', - indexes: DEFAULT_INDEX_PATTERN, - name: 'event.end', - searchable: true, - type: 'date', - }, - ], - }, - }, - }, - }, - }, -]; - -export const mockIndexFields = [ - { aggregatable: true, name: '@timestamp', searchable: true, type: 'date' }, - { aggregatable: true, name: 'agent.ephemeral_id', searchable: true, type: 'string' }, - { aggregatable: true, name: 'agent.hostname', searchable: true, type: 'string' }, - { aggregatable: true, name: 'agent.id', searchable: true, type: 'string' }, - { aggregatable: true, name: 'agent.name', searchable: true, type: 'string' }, - { aggregatable: true, name: 'auditd.data.a0', searchable: true, type: 'string' }, - { aggregatable: true, name: 'auditd.data.a1', searchable: true, type: 'string' }, - { aggregatable: true, name: 'auditd.data.a2', searchable: true, type: 'string' }, - { aggregatable: true, name: 'client.address', searchable: true, type: 'string' }, - { aggregatable: true, name: 'client.bytes', searchable: true, type: 'number' }, - { aggregatable: true, name: 'client.domain', searchable: true, type: 'string' }, - { aggregatable: true, name: 'client.geo.country_iso_code', searchable: true, type: 'string' }, - { aggregatable: true, name: 'cloud.account.id', searchable: true, type: 'string' }, - { aggregatable: true, name: 'cloud.availability_zone', searchable: true, type: 'string' }, - { aggregatable: true, name: 'container.id', searchable: true, type: 'string' }, - { aggregatable: true, name: 'container.image.name', searchable: true, type: 'string' }, - { aggregatable: true, name: 'container.image.tag', searchable: true, type: 'string' }, - { aggregatable: true, name: 'destination.address', searchable: true, type: 'string' }, - { aggregatable: true, name: 'destination.bytes', searchable: true, type: 'number' }, - { aggregatable: true, name: 'destination.domain', searchable: true, type: 'string' }, - { aggregatable: true, name: 'destination.ip', searchable: true, type: 'ip' }, - { aggregatable: true, name: 'destination.port', searchable: true, type: 'long' }, - { aggregatable: true, name: 'source.ip', searchable: true, type: 'ip' }, - { aggregatable: true, name: 'source.port', searchable: true, type: 'long' }, - { aggregatable: true, name: 'event.end', searchable: true, type: 'date' }, -]; - -export const mockBrowserFields: BrowserFields = { - agent: { - fields: { - 'agent.ephemeral_id': { - aggregatable: true, - category: 'agent', - description: - 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', - example: '8a4f500f', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.ephemeral_id', - searchable: true, - type: 'string', - }, - 'agent.hostname': { - aggregatable: true, - category: 'agent', - description: null, - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.hostname', - searchable: true, - type: 'string', - }, - 'agent.id': { - aggregatable: true, - category: 'agent', - description: - 'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.', - example: '8a4f500d', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.id', - searchable: true, - type: 'string', - }, - 'agent.name': { - aggregatable: true, - category: 'agent', - description: - 'Name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.', - example: 'foo', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'agent.name', - searchable: true, - type: 'string', - }, - }, - }, - auditd: { - fields: { - 'auditd.data.a0': { - aggregatable: true, - category: 'auditd', - description: null, - example: null, - format: '', - indexes: ['auditbeat'], - name: 'auditd.data.a0', - searchable: true, - type: 'string', - }, - 'auditd.data.a1': { - aggregatable: true, - category: 'auditd', - description: null, - example: null, - format: '', - indexes: ['auditbeat'], - name: 'auditd.data.a1', - searchable: true, - type: 'string', - }, - 'auditd.data.a2': { - aggregatable: true, - category: 'auditd', - description: null, - example: null, - format: '', - indexes: ['auditbeat'], - name: 'auditd.data.a2', - searchable: true, - type: 'string', - }, - }, - }, - base: { - fields: { - '@timestamp': { - aggregatable: true, - category: 'base', - description: - 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', - example: '2016-05-23T08:05:34.853Z', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: '@timestamp', - searchable: true, - type: 'date', - }, - }, - }, - client: { - fields: { - 'client.address': { - aggregatable: true, - category: 'client', - description: - 'Some event client addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.address', - searchable: true, - type: 'string', - }, - 'client.bytes': { - aggregatable: true, - category: 'client', - description: 'Bytes sent from the client to the server.', - example: '184', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.bytes', - searchable: true, - type: 'number', - }, - 'client.domain': { - aggregatable: true, - category: 'client', - description: 'Client domain.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.domain', - searchable: true, - type: 'string', - }, - 'client.geo.country_iso_code': { - aggregatable: true, - category: 'client', - description: 'Country ISO code.', - example: 'CA', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'client.geo.country_iso_code', - searchable: true, - type: 'string', - }, - }, - }, - cloud: { - fields: { - 'cloud.account.id': { - aggregatable: true, - category: 'cloud', - description: - 'The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.', - example: '666777888999', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'cloud.account.id', - searchable: true, - type: 'string', - }, - 'cloud.availability_zone': { - aggregatable: true, - category: 'cloud', - description: 'Availability zone in which this host is running.', - example: 'us-east-1c', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'cloud.availability_zone', - searchable: true, - type: 'string', - }, - }, - }, - container: { - fields: { - 'container.id': { - aggregatable: true, - category: 'container', - description: 'Unique container id.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'container.id', - searchable: true, - type: 'string', - }, - 'container.image.name': { - aggregatable: true, - category: 'container', - description: 'Name of the image the container was built on.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'container.image.name', - searchable: true, - type: 'string', - }, - 'container.image.tag': { - aggregatable: true, - category: 'container', - description: 'Container image tag.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'container.image.tag', - searchable: true, - type: 'string', - }, - }, - }, - destination: { - fields: { - 'destination.address': { - aggregatable: true, - category: 'destination', - description: - 'Some event destination addresses are defined ambiguously. The event will sometimes list an IP, a domain or a unix socket. You should always store the raw address in the `.address` field. Then it should be duplicated to `.ip` or `.domain`, depending on which one it is.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.address', - searchable: true, - type: 'string', - }, - 'destination.bytes': { - aggregatable: true, - category: 'destination', - description: 'Bytes sent from the destination to the source.', - example: '184', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.bytes', - searchable: true, - type: 'number', - }, - 'destination.domain': { - aggregatable: true, - category: 'destination', - description: 'Destination domain.', - example: null, - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.domain', - searchable: true, - type: 'string', - }, - 'destination.ip': { - aggregatable: true, - category: 'destination', - description: - 'IP address of the destination. Can be one or multiple IPv4 or IPv6 addresses.', - example: '', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.ip', - searchable: true, - type: 'ip', - }, - 'destination.port': { - aggregatable: true, - category: 'destination', - description: 'Port of the destination.', - example: '', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'destination.port', - searchable: true, - type: 'long', - }, - }, - }, - event: { - fields: { - 'event.end': { - category: 'event', - description: - 'event.end contains the date when the event ended or when the activity was last observed.', - example: null, - format: '', - indexes: DEFAULT_INDEX_PATTERN, - name: 'event.end', - searchable: true, - type: 'date', - aggregatable: true, - }, - }, - }, - source: { - fields: { - 'source.ip': { - aggregatable: true, - category: 'source', - description: 'IP address of the source. Can be one or multiple IPv4 or IPv6 addresses.', - example: '', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'source.ip', - searchable: true, - type: 'ip', - }, - 'source.port': { - aggregatable: true, - category: 'source', - description: 'Port of the source.', - example: '', - format: '', - indexes: ['auditbeat', 'filebeat', 'packetbeat'], - name: 'source.port', - searchable: true, - type: 'long', - }, - }, - }, -}; diff --git a/x-pack/plugins/siem/public/containers/timeline/all/index.tsx b/x-pack/plugins/siem/public/containers/timeline/all/index.tsx deleted file mode 100644 index e1d1edc1a8cecb..00000000000000 --- a/x-pack/plugins/siem/public/containers/timeline/all/index.tsx +++ /dev/null @@ -1,184 +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 { getOr, noop } from 'lodash/fp'; -import memoizeOne from 'memoize-one'; -import { useCallback, useState, useEffect } from 'react'; -import { useDispatch } from 'react-redux'; - -import { OpenTimelineResult } from '../../../components/open_timeline/types'; -import { errorToToaster, useStateToaster } from '../../../components/toasters'; -import { - GetAllTimeline, - PageInfoTimeline, - SortTimeline, - TimelineResult, -} from '../../../graphql/types'; -import { inputsActions } from '../../../store/inputs'; -import { useApolloClient } from '../../../utils/apollo_context'; - -import { allTimelinesQuery } from './index.gql_query'; -import * as i18n from '../../../pages/timelines/translations'; -import { TimelineTypeLiteralWithNull } from '../../../../common/types/timeline'; - -export interface AllTimelinesArgs { - fetchAllTimeline: ({ - onlyUserFavorite, - pageInfo, - search, - sort, - timelineType, - }: AllTimelinesVariables) => void; - timelines: OpenTimelineResult[]; - loading: boolean; - totalCount: number; -} - -export interface AllTimelinesVariables { - onlyUserFavorite: boolean; - pageInfo: PageInfoTimeline; - search: string; - sort: SortTimeline; - timelineType: TimelineTypeLiteralWithNull; -} - -export const ALL_TIMELINE_QUERY_ID = 'FETCH_ALL_TIMELINES'; - -export const getAllTimeline = memoizeOne( - (variables: string, timelines: TimelineResult[]): OpenTimelineResult[] => - timelines.map(timeline => ({ - created: timeline.created, - description: timeline.description, - eventIdToNoteIds: - timeline.eventIdToNoteIds != null - ? timeline.eventIdToNoteIds.reduce((acc, note) => { - if (note.eventId != null) { - const notes = getOr([], note.eventId, acc); - return { ...acc, [note.eventId]: [...notes, note.noteId] }; - } - return acc; - }, {}) - : null, - favorite: timeline.favorite, - noteIds: timeline.noteIds, - notes: - timeline.notes != null - ? timeline.notes.map(note => ({ ...note, savedObjectId: note.noteId })) - : null, - pinnedEventIds: - timeline.pinnedEventIds != null - ? timeline.pinnedEventIds.reduce( - (acc, pinnedEventId) => ({ ...acc, [pinnedEventId]: true }), - {} - ) - : null, - savedObjectId: timeline.savedObjectId, - title: timeline.title, - updated: timeline.updated, - updatedBy: timeline.updatedBy, - })) -); - -export const useGetAllTimeline = (): AllTimelinesArgs => { - const dispatch = useDispatch(); - const apolloClient = useApolloClient(); - const [, dispatchToaster] = useStateToaster(); - const [allTimelines, setAllTimelines] = useState({ - fetchAllTimeline: noop, - loading: false, - totalCount: 0, - timelines: [], - }); - - const fetchAllTimeline = useCallback( - async ({ onlyUserFavorite, pageInfo, search, sort, timelineType }: AllTimelinesVariables) => { - let didCancel = false; - const abortCtrl = new AbortController(); - - const fetchData = async () => { - try { - if (apolloClient != null) { - setAllTimelines({ - ...allTimelines, - loading: true, - }); - - const variables: GetAllTimeline.Variables = { - onlyUserFavorite, - pageInfo, - search, - sort, - timelineType, - }; - const response = await apolloClient.query< - GetAllTimeline.Query, - GetAllTimeline.Variables - >({ - query: allTimelinesQuery, - fetchPolicy: 'network-only', - variables, - context: { - fetchOptions: { - abortSignal: abortCtrl.signal, - }, - }, - }); - const totalCount = response?.data?.getAllTimeline?.totalCount ?? 0; - const timelines = response?.data?.getAllTimeline?.timeline ?? []; - if (!didCancel) { - dispatch( - inputsActions.setQuery({ - inputId: 'global', - id: ALL_TIMELINE_QUERY_ID, - loading: false, - refetch: fetchData, - inspect: null, - }) - ); - setAllTimelines({ - fetchAllTimeline, - loading: false, - totalCount, - timelines: getAllTimeline(JSON.stringify(variables), timelines as TimelineResult[]), - }); - } - } - } catch (error) { - if (!didCancel) { - errorToToaster({ - title: i18n.ERROR_FETCHING_TIMELINES_TITLE, - error: error.body && error.body.message ? new Error(error.body.message) : error, - dispatchToaster, - }); - setAllTimelines({ - fetchAllTimeline, - loading: false, - totalCount: 0, - timelines: [], - }); - } - } - }; - fetchData(); - return () => { - didCancel = true; - abortCtrl.abort(); - }; - }, - [apolloClient, allTimelines] - ); - - useEffect(() => { - return () => { - dispatch(inputsActions.deleteOneQuery({ inputId: 'global', id: ALL_TIMELINE_QUERY_ID })); - }; - }, [dispatch]); - - return { - ...allTimelines, - fetchAllTimeline, - }; -}; diff --git a/x-pack/plugins/siem/public/containers/timeline/api.ts b/x-pack/plugins/siem/public/containers/timeline/api.ts deleted file mode 100644 index 023e2e6af9f88e..00000000000000 --- a/x-pack/plugins/siem/public/containers/timeline/api.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 { fold } from 'fp-ts/lib/Either'; -import { identity } from 'fp-ts/lib/function'; -import { pipe } from 'fp-ts/lib/pipeable'; - -import { throwErrors } from '../../../../case/common/api'; -import { - SavedTimeline, - TimelineResponse, - TimelineResponseType, -} from '../../../common/types/timeline'; -import { TIMELINE_URL, TIMELINE_IMPORT_URL, TIMELINE_EXPORT_URL } from '../../../common/constants'; - -import { KibanaServices } from '../../lib/kibana'; -import { ExportSelectedData } from '../../components/generic_downloader'; - -import { createToasterPlainError } from '../case/utils'; -import { ImportDataProps, ImportDataResponse } from '../detection_engine/rules'; - -interface RequestPostTimeline { - timeline: SavedTimeline; - signal?: AbortSignal; -} - -interface RequestPatchTimeline extends RequestPostTimeline { - timelineId: T; - version: T; -} - -type RequestPersistTimeline = RequestPostTimeline & Partial>; - -const decodeTimelineResponse = (respTimeline?: TimelineResponse) => - pipe( - TimelineResponseType.decode(respTimeline), - fold(throwErrors(createToasterPlainError), identity) - ); - -const postTimeline = async ({ timeline }: RequestPostTimeline): Promise => { - const response = await KibanaServices.get().http.post(TIMELINE_URL, { - method: 'POST', - body: JSON.stringify({ timeline }), - }); - - return decodeTimelineResponse(response); -}; - -const patchTimeline = async ({ - timelineId, - timeline, - version, -}: RequestPatchTimeline): Promise => { - const response = await KibanaServices.get().http.patch(TIMELINE_URL, { - method: 'PATCH', - body: JSON.stringify({ timeline, timelineId, version }), - }); - - return decodeTimelineResponse(response); -}; - -export const persistTimeline = async ({ - timelineId, - timeline, - version, -}: RequestPersistTimeline): Promise => { - if (timelineId == null) { - return postTimeline({ timeline }); - } - return patchTimeline({ - timelineId, - timeline, - version: version ?? '', - }); -}; - -export const importTimelines = async ({ - fileToImport, - overwrite = false, - signal, -}: ImportDataProps): Promise => { - const formData = new FormData(); - formData.append('file', fileToImport); - - return KibanaServices.get().http.fetch(`${TIMELINE_IMPORT_URL}`, { - method: 'POST', - headers: { 'Content-Type': undefined }, - query: { overwrite }, - body: formData, - signal, - }); -}; - -export const exportSelectedTimeline: ExportSelectedData = async ({ - excludeExportDetails = false, - filename = `timelines_export.ndjson`, - ids = [], - signal, -}): Promise => { - const body = ids.length > 0 ? JSON.stringify({ ids }) : undefined; - const response = await KibanaServices.get().http.fetch(`${TIMELINE_EXPORT_URL}`, { - method: 'POST', - body, - query: { - exclude_export_details: excludeExportDetails, - file_name: filename, - }, - signal, - asResponse: true, - }); - - return response.body!; -}; diff --git a/x-pack/plugins/siem/public/containers/timeline/details/index.tsx b/x-pack/plugins/siem/public/containers/timeline/details/index.tsx deleted file mode 100644 index cf1b8954307e7b..00000000000000 --- a/x-pack/plugins/siem/public/containers/timeline/details/index.tsx +++ /dev/null @@ -1,70 +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 { getOr } from 'lodash/fp'; -import memoizeOne from 'memoize-one'; -import React from 'react'; -import { Query } from 'react-apollo'; - -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; -import { DetailItem, GetTimelineDetailsQuery } from '../../../graphql/types'; -import { useUiSetting } from '../../../lib/kibana'; - -import { timelineDetailsQuery } from './index.gql_query'; - -export interface EventsArgs { - detailsData: DetailItem[] | null; - loading: boolean; -} - -export interface TimelineDetailsProps { - children?: (args: EventsArgs) => React.ReactElement; - indexName: string; - eventId: string; - executeQuery: boolean; - sourceId: string; -} - -const getDetailsEvent = memoizeOne( - (variables: string, detail: DetailItem[]): DetailItem[] => detail -); - -const TimelineDetailsQueryComponent: React.FC = ({ - children, - indexName, - eventId, - executeQuery, - sourceId, -}) => { - const variables: GetTimelineDetailsQuery.Variables = { - sourceId, - indexName, - eventId, - defaultIndex: useUiSetting(DEFAULT_INDEX_KEY), - }; - return executeQuery ? ( - - query={timelineDetailsQuery} - fetchPolicy="network-only" - notifyOnNetworkStatusChange - variables={variables} - > - {({ data, loading, refetch }) => - children!({ - loading, - detailsData: getDetailsEvent( - JSON.stringify(variables), - getOr([], 'source.TimelineDetails.data', data) - ), - }) - } - - ) : ( - children!({ loading: false, detailsData: null }) - ); -}; - -export const TimelineDetailsQuery = React.memo(TimelineDetailsQueryComponent); diff --git a/x-pack/plugins/siem/public/containers/timeline/index.tsx b/x-pack/plugins/siem/public/containers/timeline/index.tsx deleted file mode 100644 index 6e09e124696b6f..00000000000000 --- a/x-pack/plugins/siem/public/containers/timeline/index.tsx +++ /dev/null @@ -1,199 +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 { getOr, uniqBy } from 'lodash/fp'; -import memoizeOne from 'memoize-one'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { compose, Dispatch } from 'redux'; -import { connect, ConnectedProps } from 'react-redux'; - -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; -import { IIndexPattern } from '../../../../../../src/plugins/data/common/index_patterns'; -import { - GetTimelineQuery, - PageInfo, - SortField, - TimelineEdges, - TimelineItem, -} from '../../graphql/types'; -import { inputsModel, inputsSelectors, State } from '../../store'; -import { withKibana, WithKibanaProps } from '../../lib/kibana'; -import { createFilter } from '../helpers'; -import { QueryTemplate, QueryTemplateProps } from '../query_template'; -import { EventType } from '../../store/timeline/model'; -import { timelineQuery } from './index.gql_query'; -import { timelineActions } from '../../store/timeline'; -import { SIGNALS_PAGE_TIMELINE_ID } from '../../pages/detection_engine/components/signals'; - -export interface TimelineArgs { - events: TimelineItem[]; - id: string; - inspect: inputsModel.InspectQuery; - loading: boolean; - loadMore: (cursor: string, tieBreaker: string) => void; - pageInfo: PageInfo; - refetch: inputsModel.Refetch; - totalCount: number; - getUpdatedAt: () => number; -} - -export interface CustomReduxProps { - clearSignalsState: ({ id }: { id?: string }) => void; -} - -export interface OwnProps extends QueryTemplateProps { - children?: (args: TimelineArgs) => React.ReactNode; - eventType?: EventType; - id: string; - indexPattern?: IIndexPattern; - indexToAdd?: string[]; - limit: number; - sortField: SortField; - fields: string[]; -} - -type TimelineQueryProps = OwnProps & PropsFromRedux & WithKibanaProps & CustomReduxProps; - -class TimelineQueryComponent extends QueryTemplate< - TimelineQueryProps, - GetTimelineQuery.Query, - GetTimelineQuery.Variables -> { - private updatedDate: number = Date.now(); - private memoizedTimelineEvents: (variables: string, events: TimelineEdges[]) => TimelineItem[]; - - constructor(props: TimelineQueryProps) { - super(props); - this.memoizedTimelineEvents = memoizeOne(this.getTimelineEvents); - } - - public render() { - const { - children, - clearSignalsState, - eventType = 'raw', - id, - indexPattern, - indexToAdd = [], - isInspected, - kibana, - limit, - fields, - filterQuery, - sourceId, - sortField, - } = this.props; - const defaultKibanaIndex = kibana.services.uiSettings.get(DEFAULT_INDEX_KEY); - const defaultIndex = - indexPattern == null || (indexPattern != null && indexPattern.title === '') - ? [ - ...(['all', 'raw'].includes(eventType) ? defaultKibanaIndex : []), - ...(['all', 'signal'].includes(eventType) ? indexToAdd : []), - ] - : indexPattern?.title.split(',') ?? []; - const variables: GetTimelineQuery.Variables = { - fieldRequested: fields, - filterQuery: createFilter(filterQuery), - sourceId, - pagination: { limit, cursor: null, tiebreaker: null }, - sortField, - defaultIndex, - inspect: isInspected, - }; - - return ( - - query={timelineQuery} - fetchPolicy="network-only" - notifyOnNetworkStatusChange - variables={variables} - > - {({ data, loading, fetchMore, refetch }) => { - this.setRefetch(refetch); - this.setExecuteBeforeRefetch(clearSignalsState); - this.setExecuteBeforeFetchMore(clearSignalsState); - - const timelineEdges = getOr([], 'source.Timeline.edges', data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newCursor: string, tiebreaker?: string) => ({ - variables: { - pagination: { - cursor: newCursor, - tiebreaker, - limit, - }, - }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; - } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - Timeline: { - ...fetchMoreResult.source.Timeline, - edges: uniqBy('node._id', [ - ...prev.source.Timeline.edges, - ...fetchMoreResult.source.Timeline.edges, - ]), - }, - }, - }; - }, - })); - this.updatedDate = Date.now(); - return children!({ - id, - inspect: getOr(null, 'source.Timeline.inspect', data), - refetch: this.wrappedRefetch, - loading, - totalCount: getOr(0, 'source.Timeline.totalCount', data), - pageInfo: getOr({}, 'source.Timeline.pageInfo', data), - events: this.memoizedTimelineEvents(JSON.stringify(variables), timelineEdges), - loadMore: this.wrappedLoadMore, - getUpdatedAt: this.getUpdatedAt, - }); - }} - - ); - } - - private getUpdatedAt = () => this.updatedDate; - - private getTimelineEvents = (variables: string, timelineEdges: TimelineEdges[]): TimelineItem[] => - timelineEdges.map((e: TimelineEdges) => e.node); -} - -const makeMapStateToProps = () => { - const getQuery = inputsSelectors.timelineQueryByIdSelector(); - const mapStateToProps = (state: State, { id }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - isInspected, - }; - }; - return mapStateToProps; -}; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - clearSignalsState: ({ id }: { id?: string }) => { - if (id != null && id === SIGNALS_PAGE_TIMELINE_ID) { - dispatch(timelineActions.clearEventsLoading({ id })); - dispatch(timelineActions.clearEventsDeleted({ id })); - } - }, -}); - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const TimelineQuery = compose>( - connector, - withKibana -)(TimelineQueryComponent); diff --git a/x-pack/plugins/siem/public/containers/tls/index.tsx b/x-pack/plugins/siem/public/containers/tls/index.tsx deleted file mode 100644 index 3738355c8846eb..00000000000000 --- a/x-pack/plugins/siem/public/containers/tls/index.tsx +++ /dev/null @@ -1,159 +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 { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect } from 'react-redux'; -import { compose } from 'redux'; - -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; -import { - PageInfoPaginated, - TlsEdges, - TlsSortField, - GetTlsQuery, - FlowTargetSourceDest, -} from '../../graphql/types'; -import { inputsModel, networkModel, networkSelectors, State, inputsSelectors } from '../../store'; -import { withKibana, WithKibanaProps } from '../../lib/kibana'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; -import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; -import { tlsQuery } from './index.gql_query'; - -const ID = 'tlsQuery'; - -export interface TlsArgs { - id: string; - inspect: inputsModel.InspectQuery; - isInspected: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - pageInfo: PageInfoPaginated; - refetch: inputsModel.Refetch; - tls: TlsEdges[]; - totalCount: number; -} - -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: TlsArgs) => React.ReactNode; - flowTarget: FlowTargetSourceDest; - ip: string; - type: networkModel.NetworkType; -} - -export interface TlsComponentReduxProps { - activePage: number; - isInspected: boolean; - limit: number; - sort: TlsSortField; -} - -type TlsProps = OwnProps & TlsComponentReduxProps & WithKibanaProps; - -class TlsComponentQuery extends QueryTemplatePaginated< - TlsProps, - GetTlsQuery.Query, - GetTlsQuery.Variables -> { - public render() { - const { - activePage, - children, - endDate, - filterQuery, - flowTarget, - id = ID, - ip, - isInspected, - kibana, - limit, - skip, - sourceId, - startDate, - sort, - } = this.props; - const variables: GetTlsQuery.Variables = { - defaultIndex: kibana.services.uiSettings.get(DEFAULT_INDEX_KEY), - filterQuery: createFilter(filterQuery), - flowTarget, - inspect: isInspected, - ip, - pagination: generateTablePaginationOptions(activePage, limit), - sort, - sourceId, - timerange: { - interval: '12h', - from: startDate ? startDate : 0, - to: endDate ? endDate : Date.now(), - }, - }; - return ( - - query={tlsQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const tls = getOr([], 'source.Tls.edges', data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), - }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; - } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - Tls: { - ...fetchMoreResult.source.Tls, - edges: [...fetchMoreResult.source.Tls.edges], - }, - }, - }; - }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - id, - inspect: getOr(null, 'source.Tls.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - pageInfo: getOr({}, 'source.Tls.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - tls, - totalCount: getOr(-1, 'source.Tls.totalCount', data), - }); - }} - - ); - } -} - -const makeMapStateToProps = () => { - const getTlsSelector = networkSelectors.tlsSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - return (state: State, { flowTarget, id = ID, type }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getTlsSelector(state, type, flowTarget), - isInspected, - }; - }; -}; - -export const TlsQuery = compose>( - connect(makeMapStateToProps), - withKibana -)(TlsComponentQuery); diff --git a/x-pack/plugins/siem/public/containers/uncommon_processes/index.tsx b/x-pack/plugins/siem/public/containers/uncommon_processes/index.tsx deleted file mode 100644 index 0a2ce67d9be805..00000000000000 --- a/x-pack/plugins/siem/public/containers/uncommon_processes/index.tsx +++ /dev/null @@ -1,148 +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 { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect, ConnectedProps } from 'react-redux'; -import { compose } from 'redux'; - -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; -import { - GetUncommonProcessesQuery, - PageInfoPaginated, - UncommonProcessesEdges, -} from '../../graphql/types'; -import { hostsModel, hostsSelectors, inputsModel, State, inputsSelectors } from '../../store'; -import { withKibana, WithKibanaProps } from '../../lib/kibana'; -import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; - -import { uncommonProcessesQuery } from './index.gql_query'; - -const ID = 'uncommonProcessesQuery'; - -export interface UncommonProcessesArgs { - id: string; - inspect: inputsModel.InspectQuery; - isInspected: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - pageInfo: PageInfoPaginated; - refetch: inputsModel.Refetch; - totalCount: number; - uncommonProcesses: UncommonProcessesEdges[]; -} - -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: UncommonProcessesArgs) => React.ReactNode; - type: hostsModel.HostsType; -} - -type UncommonProcessesProps = OwnProps & PropsFromRedux & WithKibanaProps; - -class UncommonProcessesComponentQuery extends QueryTemplatePaginated< - UncommonProcessesProps, - GetUncommonProcessesQuery.Query, - GetUncommonProcessesQuery.Variables -> { - public render() { - const { - activePage, - children, - endDate, - filterQuery, - id = ID, - isInspected, - kibana, - limit, - skip, - sourceId, - startDate, - } = this.props; - const variables: GetUncommonProcessesQuery.Variables = { - defaultIndex: kibana.services.uiSettings.get(DEFAULT_INDEX_KEY), - filterQuery: createFilter(filterQuery), - inspect: isInspected, - pagination: generateTablePaginationOptions(activePage, limit), - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - }; - return ( - - query={uncommonProcessesQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const uncommonProcesses = getOr([], 'source.UncommonProcesses.edges', data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), - }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; - } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - UncommonProcesses: { - ...fetchMoreResult.source.UncommonProcesses, - edges: [...fetchMoreResult.source.UncommonProcesses.edges], - }, - }, - }; - }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - id, - inspect: getOr(null, 'source.UncommonProcesses.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - pageInfo: getOr({}, 'source.UncommonProcesses.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - totalCount: getOr(-1, 'source.UncommonProcesses.totalCount', data), - uncommonProcesses, - }); - }} - - ); - } -} - -const makeMapStateToProps = () => { - const getUncommonProcessesSelector = hostsSelectors.uncommonProcessesSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { type, id = ID }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getUncommonProcessesSelector(state, type), - isInspected, - }; - }; - return mapStateToProps; -}; - -const connector = connect(makeMapStateToProps); - -type PropsFromRedux = ConnectedProps; - -export const UncommonProcessesQuery = compose>( - connector, - withKibana -)(UncommonProcessesComponentQuery); diff --git a/x-pack/plugins/siem/public/containers/users/index.tsx b/x-pack/plugins/siem/public/containers/users/index.tsx deleted file mode 100644 index 5f71449c524606..00000000000000 --- a/x-pack/plugins/siem/public/containers/users/index.tsx +++ /dev/null @@ -1,153 +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 { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect, ConnectedProps } from 'react-redux'; -import { compose } from 'redux'; - -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; -import { GetUsersQuery, FlowTarget, PageInfoPaginated, UsersEdges } from '../../graphql/types'; -import { inputsModel, networkModel, networkSelectors, State, inputsSelectors } from '../../store'; -import { withKibana, WithKibanaProps } from '../../lib/kibana'; -import { createFilter, getDefaultFetchPolicy } from '../helpers'; -import { generateTablePaginationOptions } from '../../components/paginated_table/helpers'; -import { QueryTemplatePaginated, QueryTemplatePaginatedProps } from '../query_template_paginated'; - -import { usersQuery } from './index.gql_query'; - -const ID = 'usersQuery'; - -export interface UsersArgs { - id: string; - inspect: inputsModel.InspectQuery; - isInspected: boolean; - loading: boolean; - loadPage: (newActivePage: number) => void; - pageInfo: PageInfoPaginated; - refetch: inputsModel.Refetch; - totalCount: number; - users: UsersEdges[]; -} - -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: UsersArgs) => React.ReactNode; - flowTarget: FlowTarget; - ip: string; - type: networkModel.NetworkType; -} - -type UsersProps = OwnProps & PropsFromRedux & WithKibanaProps; - -class UsersComponentQuery extends QueryTemplatePaginated< - UsersProps, - GetUsersQuery.Query, - GetUsersQuery.Variables -> { - public render() { - const { - activePage, - children, - endDate, - filterQuery, - flowTarget, - id = ID, - ip, - isInspected, - kibana, - limit, - skip, - sourceId, - startDate, - sort, - } = this.props; - const variables: GetUsersQuery.Variables = { - defaultIndex: kibana.services.uiSettings.get(DEFAULT_INDEX_KEY), - filterQuery: createFilter(filterQuery), - flowTarget, - inspect: isInspected, - ip, - pagination: generateTablePaginationOptions(activePage, limit), - sort, - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - }; - return ( - - query={usersQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const users = getOr([], `source.Users.edges`, data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), - }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; - } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - Users: { - ...fetchMoreResult.source.Users, - edges: [...fetchMoreResult.source.Users.edges], - }, - }, - }; - }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - id, - inspect: getOr(null, 'source.Users.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - pageInfo: getOr({}, 'source.Users.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - totalCount: getOr(-1, 'source.Users.totalCount', data), - users, - }); - }} - - ); - } -} - -const makeMapStateToProps = () => { - const getUsersSelector = networkSelectors.usersSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = ID }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getUsersSelector(state), - isInspected, - }; - }; - - return mapStateToProps; -}; - -const connector = connect(makeMapStateToProps); - -type PropsFromRedux = ConnectedProps; - -export const UsersQuery = compose>( - connector, - withKibana -)(UsersComponentQuery); diff --git a/x-pack/plugins/siem/public/hooks/types.ts b/x-pack/plugins/siem/public/hooks/types.ts deleted file mode 100644 index 6527904964d000..00000000000000 --- a/x-pack/plugins/siem/public/hooks/types.ts +++ /dev/null @@ -1,15 +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 { SimpleSavedObject } from '../../../../../src/core/public'; - -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions -export type IndexPatternSavedObjectAttributes = { title: string }; - -export type IndexPatternSavedObject = Pick< - SimpleSavedObject, - 'type' | 'id' | 'attributes' | '_version' ->; diff --git a/x-pack/plugins/siem/public/components/page/hosts/authentications_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/hosts/components/authentications_table/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/page/hosts/authentications_table/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/hosts/components/authentications_table/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/hosts/components/authentications_table/index.test.tsx b/x-pack/plugins/siem/public/hosts/components/authentications_table/index.test.tsx new file mode 100644 index 00000000000000..2c39db2ab7340e --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/components/authentications_table/index.test.tsx @@ -0,0 +1,85 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React from 'react'; +import { Provider as ReduxStoreProvider } from 'react-redux'; + +import { apolloClientObservable, mockGlobalState, SUB_PLUGINS_REDUCER } from '../../../common/mock'; +import { createStore, State } from '../../../common/store'; +import { hostsModel } from '../../store'; +import { mockData } from './mock'; +import * as i18n from './translations'; +import { AuthenticationTable, getAuthenticationColumnsCurated } from '.'; + +describe('Authentication Table Component', () => { + const loadPage = jest.fn(); + const state: State = mockGlobalState; + + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + + beforeEach(() => { + store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + }); + + describe('rendering', () => { + test('it renders the authentication table', () => { + const wrapper = shallow( + + + + ); + + expect(wrapper.find('Connect(AuthenticationTableComponent)')).toMatchSnapshot(); + }); + }); + + describe('columns', () => { + test('on hosts page, we expect to get all columns', () => { + expect(getAuthenticationColumnsCurated(hostsModel.HostsType.page).length).toEqual(9); + }); + + test('on host details page, we expect to remove two columns', () => { + const columns = getAuthenticationColumnsCurated(hostsModel.HostsType.details); + expect(columns.length).toEqual(7); + }); + + test('on host details page, we should have Last Failed Destination column', () => { + const columns = getAuthenticationColumnsCurated(hostsModel.HostsType.page); + expect(columns.some(col => col.name === i18n.LAST_FAILED_DESTINATION)).toEqual(true); + }); + + test('on host details page, we should not have Last Failed Destination column', () => { + const columns = getAuthenticationColumnsCurated(hostsModel.HostsType.details); + expect(columns.some(col => col.name === i18n.LAST_FAILED_DESTINATION)).toEqual(false); + }); + + test('on host page, we should have Last Successful Destination column', () => { + const columns = getAuthenticationColumnsCurated(hostsModel.HostsType.page); + expect(columns.some(col => col.name === i18n.LAST_SUCCESSFUL_DESTINATION)).toEqual(true); + }); + + test('on host details page, we should not have Last Successful Destination column', () => { + const columns = getAuthenticationColumnsCurated(hostsModel.HostsType.details); + expect(columns.some(col => col.name === i18n.LAST_SUCCESSFUL_DESTINATION)).toEqual(false); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/hosts/components/authentications_table/index.tsx b/x-pack/plugins/siem/public/hosts/components/authentications_table/index.tsx new file mode 100644 index 00000000000000..ef28f268bb73a5 --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/components/authentications_table/index.tsx @@ -0,0 +1,349 @@ +/* + * 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. + */ + +/* eslint-disable react/display-name */ + +import { has } from 'lodash/fp'; +import React, { useCallback, useMemo } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; + +import { hostsActions, hostsModel, hostsSelectors } from '../../store'; +import { AuthenticationsEdges } from '../../../graphql/types'; +import { State } from '../../../common/store'; +import { + DragEffects, + DraggableWrapper, +} from '../../../common/components/drag_and_drop/draggable_wrapper'; +import { escapeDataProviderId } from '../../../common/components/drag_and_drop/helpers'; +import { getEmptyTagValue } from '../../../common/components/empty_value'; +import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date'; +import { HostDetailsLink, IPDetailsLink } from '../../../common/components/links'; +import { Columns, ItemsPerRow, PaginatedTable } from '../../../common/components/paginated_table'; +import { IS_OPERATOR } from '../../../timelines/components/timeline/data_providers/data_provider'; +import { Provider } from '../../../timelines/components/timeline/data_providers/provider'; + +import * as i18n from './translations'; +import { getRowItemDraggables } from '../../../common/components/tables/helpers'; + +const tableType = hostsModel.HostsTableType.authentications; + +interface OwnProps { + data: AuthenticationsEdges[]; + fakeTotalCount: number; + loading: boolean; + loadPage: (newActivePage: number) => void; + id: string; + isInspect: boolean; + showMorePagesIndicator: boolean; + totalCount: number; + type: hostsModel.HostsType; +} + +export type AuthTableColumns = [ + Columns, + Columns, + Columns, + Columns, + Columns, + Columns, + Columns, + Columns, + Columns +]; + +type AuthenticationTableProps = OwnProps & PropsFromRedux; + +const rowItems: ItemsPerRow[] = [ + { + text: i18n.ROWS_5, + numberOfRow: 5, + }, + { + text: i18n.ROWS_10, + numberOfRow: 10, + }, +]; + +const AuthenticationTableComponent = React.memo( + ({ + activePage, + data, + fakeTotalCount, + id, + isInspect, + limit, + loading, + loadPage, + showMorePagesIndicator, + totalCount, + type, + updateTableActivePage, + updateTableLimit, + }) => { + const updateLimitPagination = useCallback( + newLimit => + updateTableLimit({ + hostsType: type, + limit: newLimit, + tableType, + }), + [type, updateTableLimit] + ); + + const updateActivePage = useCallback( + newPage => + updateTableActivePage({ + activePage: newPage, + hostsType: type, + tableType, + }), + [type, updateTableActivePage] + ); + + const columns = useMemo(() => getAuthenticationColumnsCurated(type), [type]); + + return ( + + ); + } +); + +AuthenticationTableComponent.displayName = 'AuthenticationTableComponent'; + +const makeMapStateToProps = () => { + const getAuthenticationsSelector = hostsSelectors.authenticationsSelector(); + return (state: State, { type }: OwnProps) => { + return getAuthenticationsSelector(state, type); + }; +}; + +const mapDispatchToProps = { + updateTableActivePage: hostsActions.updateTableActivePage, + updateTableLimit: hostsActions.updateTableLimit, +}; + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const AuthenticationTable = connector(AuthenticationTableComponent); + +const getAuthenticationColumns = (): AuthTableColumns => [ + { + name: i18n.USER, + truncateText: false, + hideForMobile: false, + render: ({ node }) => + getRowItemDraggables({ + rowItems: node.user.name, + attrName: 'user.name', + idPrefix: `authentications-table-${node._id}-userName`, + }), + }, + { + name: i18n.SUCCESSES, + truncateText: false, + hideForMobile: false, + render: ({ node }) => { + const id = escapeDataProviderId( + `authentications-table-${node._id}-node-successes-${node.successes}` + ); + return ( + + snapshot.isDragging ? ( + + + + ) : ( + node.successes + ) + } + /> + ); + }, + width: '8%', + }, + { + name: i18n.FAILURES, + truncateText: false, + hideForMobile: false, + render: ({ node }) => { + const id = escapeDataProviderId( + `authentications-table-${node._id}-failures-${node.failures}` + ); + return ( + + snapshot.isDragging ? ( + + + + ) : ( + node.failures + ) + } + /> + ); + }, + width: '8%', + }, + { + name: i18n.LAST_SUCCESSFUL_TIME, + truncateText: false, + hideForMobile: false, + render: ({ node }) => + has('lastSuccess.timestamp', node) && node.lastSuccess!.timestamp != null ? ( + + ) : ( + getEmptyTagValue() + ), + }, + { + name: i18n.LAST_SUCCESSFUL_SOURCE, + truncateText: false, + hideForMobile: false, + render: ({ node }) => + getRowItemDraggables({ + rowItems: + node.lastSuccess != null && + node.lastSuccess.source != null && + node.lastSuccess.source.ip != null + ? node.lastSuccess.source.ip + : null, + attrName: 'source.ip', + idPrefix: `authentications-table-${node._id}-lastSuccessSource`, + render: item => , + }), + }, + { + name: i18n.LAST_SUCCESSFUL_DESTINATION, + truncateText: false, + hideForMobile: false, + render: ({ node }) => + getRowItemDraggables({ + rowItems: + node.lastSuccess != null && + node.lastSuccess.host != null && + node.lastSuccess.host.name != null + ? node.lastSuccess.host.name + : null, + attrName: 'host.name', + idPrefix: `authentications-table-${node._id}-lastSuccessfulDestination`, + render: item => , + }), + }, + { + name: i18n.LAST_FAILED_TIME, + truncateText: false, + hideForMobile: false, + render: ({ node }) => + has('lastFailure.timestamp', node) && node.lastFailure!.timestamp != null ? ( + + ) : ( + getEmptyTagValue() + ), + }, + { + name: i18n.LAST_FAILED_SOURCE, + truncateText: false, + hideForMobile: false, + render: ({ node }) => + getRowItemDraggables({ + rowItems: + node.lastFailure != null && + node.lastFailure.source != null && + node.lastFailure.source.ip != null + ? node.lastFailure.source.ip + : null, + attrName: 'source.ip', + idPrefix: `authentications-table-${node._id}-lastFailureSource`, + render: item => , + }), + }, + { + name: i18n.LAST_FAILED_DESTINATION, + truncateText: false, + hideForMobile: false, + render: ({ node }) => + getRowItemDraggables({ + rowItems: + node.lastFailure != null && + node.lastFailure.host != null && + node.lastFailure.host.name != null + ? node.lastFailure.host.name + : null, + attrName: 'host.name', + idPrefix: `authentications-table-${node._id}-lastFailureDestination`, + render: item => , + }), + }, +]; + +export const getAuthenticationColumnsCurated = ( + pageType: hostsModel.HostsType +): AuthTableColumns => { + const columns = getAuthenticationColumns(); + + // Columns to exclude from host details pages + if (pageType === hostsModel.HostsType.details) { + return [i18n.LAST_FAILED_DESTINATION, i18n.LAST_SUCCESSFUL_DESTINATION].reduce((acc, name) => { + acc.splice( + acc.findIndex(column => column.name === name), + 1 + ); + return acc; + }, columns); + } + + return columns; +}; diff --git a/x-pack/plugins/siem/public/hosts/components/authentications_table/mock.ts b/x-pack/plugins/siem/public/hosts/components/authentications_table/mock.ts new file mode 100644 index 00000000000000..84682fd14ac6ba --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/components/authentications_table/mock.ts @@ -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 { AuthenticationsData } from '../../../graphql/types'; + +export const mockData: { Authentications: AuthenticationsData } = { + Authentications: { + totalCount: 54, + edges: [ + { + node: { + _id: 'cPsuhGcB0WOhS6qyTKC0', + failures: 10, + successes: 0, + user: { name: ['Evan Hassanabad'] }, + lastSuccess: { + timestamp: '2019-01-23T22:35:32.222Z', + source: { + ip: ['127.0.0.1'], + }, + host: { + id: ['host-id-1'], + name: ['host-1'], + }, + }, + lastFailure: { + timestamp: '2019-01-23T22:35:32.222Z', + source: { + ip: ['8.8.8.8'], + }, + host: { + id: ['host-id-1'], + name: ['host-2'], + }, + }, + }, + cursor: { + value: '98966fa2013c396155c460d35c0902be', + }, + }, + { + node: { + _id: 'KwQDiWcB0WOhS6qyXmrW', + failures: 10, + successes: 0, + user: { name: ['Braden Hassanabad'] }, + lastSuccess: { + timestamp: '2019-01-23T22:35:32.222Z', + source: { + ip: ['127.0.0.1'], + }, + host: { + id: ['host-id-1'], + name: ['host-1'], + }, + }, + lastFailure: { + timestamp: '2019-01-23T22:35:32.222Z', + source: { + ip: ['8.8.8.8'], + }, + host: { + id: ['host-id-1'], + name: ['host-2'], + }, + }, + }, + cursor: { + value: 'aa7ca589f1b8220002f2fc61c64cfbf1', + }, + }, + ], + pageInfo: { + activePage: 1, + fakeTotalCount: 50, + showMorePagesIndicator: true, + }, + }, +}; diff --git a/x-pack/plugins/siem/public/components/page/hosts/authentications_table/translations.ts b/x-pack/plugins/siem/public/hosts/components/authentications_table/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/page/hosts/authentications_table/translations.ts rename to x-pack/plugins/siem/public/hosts/components/authentications_table/translations.ts diff --git a/x-pack/plugins/siem/public/hosts/components/first_last_seen_host/index.test.tsx b/x-pack/plugins/siem/public/hosts/components/first_last_seen_host/index.test.tsx new file mode 100644 index 00000000000000..9715c1cb5c8b45 --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/components/first_last_seen_host/index.test.tsx @@ -0,0 +1,138 @@ +/* + * 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 { cloneDeep } from 'lodash/fp'; +import React from 'react'; +import { MockedProvider } from 'react-apollo/test-utils'; +import { render, act } from '@testing-library/react'; + +import { mockFirstLastSeenHostQuery } from '../../containers/hosts/first_last_seen/mock'; +import { wait } from '../../../common/lib/helpers'; +import { TestProviders } from '../../../common/mock'; + +import { FirstLastSeenHost, FirstLastSeenHostType } from '.'; + +describe('FirstLastSeen Component', () => { + const firstSeen = 'Apr 8, 2019 @ 16:09:40.692'; + const lastSeen = 'Apr 8, 2019 @ 18:35:45.064'; + + // Suppress warnings about "react-apollo" until we migrate to apollo@3 + /* eslint-disable no-console */ + const originalError = console.error; + beforeAll(() => { + console.error = jest.fn(); + }); + afterAll(() => { + console.error = originalError; + }); + + test('Loading', async () => { + const { container } = render( + + + + + + ); + expect(container.innerHTML).toBe( + '' + ); + }); + + test('First Seen', async () => { + const { container } = render( + + + + + + ); + + await act(() => wait()); + + expect(container.innerHTML).toBe( + `
${firstSeen}
` + ); + }); + + test('Last Seen', async () => { + const { container } = render( + + + + + + ); + await act(() => wait()); + expect(container.innerHTML).toBe( + `
${lastSeen}
` + ); + }); + + test('First Seen is empty but not Last Seen', async () => { + const badDateTime = cloneDeep(mockFirstLastSeenHostQuery); + badDateTime[0].result.data!.source.HostFirstLastSeen.firstSeen = null; + const { container } = render( + + + + + + ); + + await act(() => wait()); + + expect(container.innerHTML).toBe( + `
${lastSeen}
` + ); + }); + + test('Last Seen is empty but not First Seen', async () => { + const badDateTime = cloneDeep(mockFirstLastSeenHostQuery); + badDateTime[0].result.data!.source.HostFirstLastSeen.lastSeen = null; + const { container } = render( + + + + + + ); + + await act(() => wait()); + + expect(container.innerHTML).toBe( + `
${firstSeen}
` + ); + }); + + test('First Seen With a bad date time string', async () => { + const badDateTime = cloneDeep(mockFirstLastSeenHostQuery); + badDateTime[0].result.data!.source.HostFirstLastSeen.firstSeen = 'something-invalid'; + const { container } = render( + + + + + + ); + await act(() => wait()); + expect(container.textContent).toBe('something-invalid'); + }); + + test('Last Seen With a bad date time string', async () => { + const badDateTime = cloneDeep(mockFirstLastSeenHostQuery); + badDateTime[0].result.data!.source.HostFirstLastSeen.lastSeen = 'something-invalid'; + const { container } = render( + + + + + + ); + await act(() => wait()); + expect(container.textContent).toBe('something-invalid'); + }); +}); diff --git a/x-pack/plugins/siem/public/hosts/components/first_last_seen_host/index.tsx b/x-pack/plugins/siem/public/hosts/components/first_last_seen_host/index.tsx new file mode 100644 index 00000000000000..05e65b496fae04 --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/components/first_last_seen_host/index.tsx @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiIcon, EuiLoadingSpinner, EuiText, EuiToolTip } from '@elastic/eui'; +import React from 'react'; +import { ApolloConsumer } from 'react-apollo'; + +import { useFirstLastSeenHostQuery } from '../../containers/hosts/first_last_seen'; +import { getEmptyTagValue } from '../../../common/components/empty_value'; +import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date'; + +export enum FirstLastSeenHostType { + FIRST_SEEN = 'first-seen', + LAST_SEEN = 'last-seen', +} + +export const FirstLastSeenHost = React.memo<{ hostname: string; type: FirstLastSeenHostType }>( + ({ hostname, type }) => { + return ( + + {client => { + const { loading, firstSeen, lastSeen, errorMessage } = useFirstLastSeenHostQuery( + hostname, + 'default', + client + ); + if (errorMessage != null) { + return ( + + + + ); + } + const valueSeen = type === FirstLastSeenHostType.FIRST_SEEN ? firstSeen : lastSeen; + return ( + <> + {loading && } + {!loading && valueSeen != null && new Date(valueSeen).toString() === 'Invalid Date' + ? valueSeen + : !loading && + valueSeen != null && ( + + + + )} + {!loading && valueSeen == null && getEmptyTagValue()} + + ); + }} + + ); + } +); + +FirstLastSeenHost.displayName = 'FirstLastSeenHost'; diff --git a/x-pack/plugins/siem/public/components/page/hosts/hosts_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/hosts/components/hosts_table/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/page/hosts/hosts_table/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/hosts/components/hosts_table/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/hosts/components/hosts_table/columns.tsx b/x-pack/plugins/siem/public/hosts/components/hosts_table/columns.tsx new file mode 100644 index 00000000000000..6b3097e1feabb6 --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/components/hosts_table/columns.tsx @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiIcon, EuiToolTip } from '@elastic/eui'; +import React from 'react'; +import { + DragEffects, + DraggableWrapper, +} from '../../../common/components/drag_and_drop/draggable_wrapper'; +import { escapeDataProviderId } from '../../../common/components/drag_and_drop/helpers'; +import { getEmptyTagValue } from '../../../common/components/empty_value'; +import { HostDetailsLink } from '../../../common/components/links'; +import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date'; +import { IS_OPERATOR } from '../../../timelines/components/timeline/data_providers/data_provider'; +import { Provider } from '../../../timelines/components/timeline/data_providers/provider'; +import { + AddFilterToGlobalSearchBar, + createFilter, +} from '../../../common/components/add_filter_to_global_search_bar'; +import { HostsTableColumns } from './'; + +import * as i18n from './translations'; + +export const getHostsColumns = (): HostsTableColumns => [ + { + field: 'node.host.name', + name: i18n.NAME, + truncateText: false, + hideForMobile: false, + sortable: true, + render: hostName => { + if (hostName != null && hostName.length > 0) { + const id = escapeDataProviderId(`hosts-table-hostName-${hostName[0]}`); + return ( + + snapshot.isDragging ? ( + + + + ) : ( + + ) + } + /> + ); + } + return getEmptyTagValue(); + }, + width: '35%', + }, + { + field: 'node.lastSeen', + name: ( + + <> + {i18n.LAST_SEEN}{' '} + + + + ), + truncateText: false, + hideForMobile: false, + sortable: true, + render: lastSeen => { + if (lastSeen != null) { + return ; + } + return getEmptyTagValue(); + }, + }, + { + field: 'node.host.os.name', + name: i18n.OS, + truncateText: false, + hideForMobile: false, + sortable: false, + render: hostOsName => { + if (hostOsName != null) { + return ( + + <>{hostOsName} + + ); + } + return getEmptyTagValue(); + }, + }, + { + field: 'node.host.os.version', + name: i18n.VERSION, + truncateText: false, + hideForMobile: false, + sortable: false, + render: hostOsVersion => { + if (hostOsVersion != null) { + return ( + + <>{hostOsVersion} + + ); + } + return getEmptyTagValue(); + }, + }, +]; diff --git a/x-pack/plugins/siem/public/hosts/components/hosts_table/index.test.tsx b/x-pack/plugins/siem/public/hosts/components/hosts_table/index.test.tsx new file mode 100644 index 00000000000000..8c1429174bd78e --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/components/hosts_table/index.test.tsx @@ -0,0 +1,139 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React from 'react'; +import { MockedProvider } from 'react-apollo/test-utils'; + +import { + apolloClientObservable, + mockIndexPattern, + mockGlobalState, + TestProviders, + SUB_PLUGINS_REDUCER, +} from '../../../common/mock'; +import { useMountAppended } from '../../../common/utils/use_mount_appended'; +import { createStore, State } from '../../../common/store'; +import { hostsModel } from '../../../hosts/store'; +import { HostsTableType } from '../../../hosts/store/model'; +import { HostsTable } from './index'; +import { mockData } from './mock'; + +// Test will fail because we will to need to mock some core services to make the test work +// For now let's forget about SiemSearchBar and QueryBar +jest.mock('../../../common/components/search_bar', () => ({ + SiemSearchBar: () => null, +})); +jest.mock('../../../common/components/query_bar', () => ({ + QueryBar: () => null, +})); + +describe('Hosts Table', () => { + const loadPage = jest.fn(); + const state: State = mockGlobalState; + + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + const mount = useMountAppended(); + + beforeEach(() => { + store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + }); + + describe('rendering', () => { + test('it renders the default Hosts table', () => { + const wrapper = shallow( + + + + ); + + expect(wrapper.find('HostsTable')).toMatchSnapshot(); + }); + + describe('Sorting on Table', () => { + let wrapper: ReturnType; + + beforeEach(() => { + wrapper = mount( + + + + + + ); + }); + test('Initial value of the store', () => { + expect(store.getState().hosts.page.queries[HostsTableType.hosts]).toEqual({ + activePage: 0, + direction: 'desc', + sortField: 'lastSeen', + limit: 10, + }); + expect( + wrapper + .find('.euiTable thead tr th button') + .at(1) + .text() + ).toEqual('Last seen Click to sort in ascending order'); + expect( + wrapper + .find('.euiTable thead tr th button') + .at(1) + .find('svg') + ).toBeTruthy(); + }); + + test('when you click on the column header, you should show the sorting icon', () => { + wrapper + .find('.euiTable thead tr th button') + .first() + .simulate('click'); + + wrapper.update(); + + expect(store.getState().hosts.page.queries[HostsTableType.hosts]).toEqual({ + activePage: 0, + direction: 'asc', + sortField: 'hostName', + limit: 10, + }); + expect( + wrapper + .find('.euiTable thead tr th button') + .first() + .text() + ).toEqual('Host nameClick to sort in descending order'); + }); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/hosts/components/hosts_table/index.tsx b/x-pack/plugins/siem/public/hosts/components/hosts_table/index.tsx new file mode 100644 index 00000000000000..550ee24f609226 --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/components/hosts_table/index.tsx @@ -0,0 +1,207 @@ +/* + * 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, { useMemo, useCallback } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import { IIndexPattern } from 'src/plugins/data/public'; + +import { + Direction, + HostFields, + HostItem, + HostsEdges, + HostsFields, + HostsSortField, + OsFields, +} from '../../../graphql/types'; +import { assertUnreachable } from '../../../common/lib/helpers'; +import { State } from '../../../common/store'; +import { + Columns, + Criteria, + ItemsPerRow, + PaginatedTable, + SortingBasicTable, +} from '../../../common/components/paginated_table'; +import { hostsActions, hostsModel, hostsSelectors } from '../../store'; +import { getHostsColumns } from './columns'; +import * as i18n from './translations'; + +const tableType = hostsModel.HostsTableType.hosts; + +interface OwnProps { + data: HostsEdges[]; + fakeTotalCount: number; + id: string; + indexPattern: IIndexPattern; + isInspect: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + showMorePagesIndicator: boolean; + totalCount: number; + type: hostsModel.HostsType; +} + +export type HostsTableColumns = [ + Columns, + Columns, + Columns, + Columns +]; + +type HostsTableProps = OwnProps & PropsFromRedux; + +const rowItems: ItemsPerRow[] = [ + { + text: i18n.ROWS_5, + numberOfRow: 5, + }, + { + text: i18n.ROWS_10, + numberOfRow: 10, + }, +]; +const getSorting = ( + trigger: string, + sortField: HostsFields, + direction: Direction +): SortingBasicTable => ({ field: getNodeField(sortField), direction }); + +const HostsTableComponent = React.memo( + ({ + activePage, + data, + direction, + fakeTotalCount, + id, + indexPattern, + isInspect, + limit, + loading, + loadPage, + showMorePagesIndicator, + sortField, + totalCount, + type, + updateHostsSort, + updateTableActivePage, + updateTableLimit, + }) => { + const updateLimitPagination = useCallback( + newLimit => + updateTableLimit({ + hostsType: type, + limit: newLimit, + tableType, + }), + [type, updateTableLimit] + ); + + const updateActivePage = useCallback( + newPage => + updateTableActivePage({ + activePage: newPage, + hostsType: type, + tableType, + }), + [type, updateTableActivePage] + ); + + const onChange = useCallback( + (criteria: Criteria) => { + if (criteria.sort != null) { + const sort: HostsSortField = { + field: getSortField(criteria.sort.field), + direction: criteria.sort.direction as Direction, + }; + if (sort.direction !== direction || sort.field !== sortField) { + updateHostsSort({ + sort, + hostsType: type, + }); + } + } + }, + [direction, sortField, type, updateHostsSort] + ); + + const hostsColumns = useMemo(() => getHostsColumns(), []); + + const sorting = useMemo(() => getSorting(`${sortField}-${direction}`, sortField, direction), [ + sortField, + direction, + ]); + + return ( + + ); + } +); + +HostsTableComponent.displayName = 'HostsTableComponent'; + +const getSortField = (field: string): HostsFields => { + switch (field) { + case 'node.host.name': + return HostsFields.hostName; + case 'node.lastSeen': + return HostsFields.lastSeen; + default: + return HostsFields.lastSeen; + } +}; + +const getNodeField = (field: HostsFields): string => { + switch (field) { + case HostsFields.hostName: + return 'node.host.name'; + case HostsFields.lastSeen: + return 'node.lastSeen'; + } + assertUnreachable(field); +}; + +const makeMapStateToProps = () => { + const getHostsSelector = hostsSelectors.hostsSelector(); + const mapStateToProps = (state: State, { type }: OwnProps) => { + return getHostsSelector(state, type); + }; + return mapStateToProps; +}; + +const mapDispatchToProps = { + updateHostsSort: hostsActions.updateHostsSort, + updateTableActivePage: hostsActions.updateTableActivePage, + updateTableLimit: hostsActions.updateTableLimit, +}; + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const HostsTable = connector(HostsTableComponent); + +HostsTable.displayName = 'HostsTable'; diff --git a/x-pack/plugins/siem/public/hosts/components/hosts_table/mock.ts b/x-pack/plugins/siem/public/hosts/components/hosts_table/mock.ts new file mode 100644 index 00000000000000..a3dd69be75cc67 --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/components/hosts_table/mock.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 { HostsData } from '../../../graphql/types'; + +export const mockData: { Hosts: HostsData } = { + Hosts: { + totalCount: 4, + edges: [ + { + node: { + _id: 'cPsuhGcB0WOhS6qyTKC0', + host: { + name: ['elrond.elstc.co'], + os: { + name: ['Ubuntu'], + version: ['18.04.1 LTS (Bionic Beaver)'], + }, + }, + }, + cursor: { + value: '98966fa2013c396155c460d35c0902be', + }, + }, + { + node: { + _id: 'KwQDiWcB0WOhS6qyXmrW', + host: { + name: ['siem-kibana'], + os: { + name: ['Debian GNU/Linux'], + version: ['9 (stretch)'], + }, + }, + cloud: { + instance: { + id: ['423232333829362673777'], + }, + machine: { + type: ['custom-4-16384'], + }, + provider: ['gce'], + region: ['us-east-1'], + }, + }, + cursor: { + value: 'aa7ca589f1b8220002f2fc61c64cfbf1', + }, + }, + ], + pageInfo: { + activePage: 1, + fakeTotalCount: 50, + showMorePagesIndicator: true, + }, + }, +}; diff --git a/x-pack/plugins/siem/public/components/page/hosts/hosts_table/translations.ts b/x-pack/plugins/siem/public/hosts/components/hosts_table/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/page/hosts/hosts_table/translations.ts rename to x-pack/plugins/siem/public/hosts/components/hosts_table/translations.ts diff --git a/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/hosts/components/kpi_hosts/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/hosts/components/kpi_hosts/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/hosts/components/kpi_hosts/index.test.tsx b/x-pack/plugins/siem/public/hosts/components/kpi_hosts/index.test.tsx new file mode 100644 index 00000000000000..09e253ae56747d --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/components/kpi_hosts/index.test.tsx @@ -0,0 +1,106 @@ +/* + * 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 { mockKpiHostsData, mockKpiHostDetailsData } from './mock'; +import React from 'react'; +import { shallow, ShallowWrapper } from 'enzyme'; +import { KpiHostsComponentBase } from '.'; +import * as statItems from '../../../common/components/stat_items'; +import { kpiHostsMapping } from './kpi_hosts_mapping'; +import { kpiHostDetailsMapping } from './kpi_host_details_mapping'; + +describe('kpiHostsComponent', () => { + const ID = 'kpiHost'; + const from = new Date('2019-06-15T06:00:00.000Z').valueOf(); + const to = new Date('2019-06-18T06:00:00.000Z').valueOf(); + const narrowDateRange = () => {}; + describe('render', () => { + test('it should render spinner if it is loading', () => { + const wrapper: ShallowWrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); + + test('it should render KpiHostsData', () => { + const wrapper: ShallowWrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); + + test('it should render KpiHostDetailsData', () => { + const wrapper: ShallowWrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); + }); + + const table = [ + [mockKpiHostsData, kpiHostsMapping] as [typeof mockKpiHostsData, typeof kpiHostsMapping], + [mockKpiHostDetailsData, kpiHostDetailsMapping] as [ + typeof mockKpiHostDetailsData, + typeof kpiHostDetailsMapping + ], + ]; + + describe.each(table)( + 'it should handle KpiHostsProps and KpiHostDetailsProps', + (data, mapping) => { + let mockUseKpiMatrixStatus: jest.SpyInstance; + beforeAll(() => { + mockUseKpiMatrixStatus = jest.spyOn(statItems, 'useKpiMatrixStatus'); + }); + + beforeEach(() => { + shallow( + + ); + }); + + afterEach(() => { + mockUseKpiMatrixStatus.mockClear(); + }); + + afterAll(() => { + mockUseKpiMatrixStatus.mockRestore(); + }); + + test(`it should apply correct mapping by given data type`, () => { + expect(mockUseKpiMatrixStatus).toBeCalledWith(mapping, data, ID, from, to, narrowDateRange); + }); + } + ); +}); diff --git a/x-pack/plugins/siem/public/hosts/components/kpi_hosts/index.tsx b/x-pack/plugins/siem/public/hosts/components/kpi_hosts/index.tsx new file mode 100644 index 00000000000000..ba70df7d361d45 --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/components/kpi_hosts/index.tsx @@ -0,0 +1,84 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +import { KpiHostsData, KpiHostDetailsData } from '../../../graphql/types'; +import { + StatItemsComponent, + StatItemsProps, + useKpiMatrixStatus, +} from '../../../common/components/stat_items'; +import { kpiHostsMapping } from './kpi_hosts_mapping'; +import { kpiHostDetailsMapping } from './kpi_host_details_mapping'; +import { UpdateDateRange } from '../../../common/components/charts/common'; + +const kpiWidgetHeight = 247; + +interface GenericKpiHostProps { + from: number; + id: string; + loading: boolean; + to: number; + narrowDateRange: UpdateDateRange; +} + +interface KpiHostsProps extends GenericKpiHostProps { + data: KpiHostsData; +} + +interface KpiHostDetailsProps extends GenericKpiHostProps { + data: KpiHostDetailsData; +} + +const FlexGroupSpinner = styled(EuiFlexGroup)` + { + min-height: ${kpiWidgetHeight}px; + } +`; + +FlexGroupSpinner.displayName = 'FlexGroupSpinner'; + +export const KpiHostsComponentBase = ({ + data, + from, + loading, + id, + to, + narrowDateRange, +}: KpiHostsProps | KpiHostDetailsProps) => { + const mappings = + (data as KpiHostsData).hosts !== undefined ? kpiHostsMapping : kpiHostDetailsMapping; + const statItemsProps: StatItemsProps[] = useKpiMatrixStatus( + mappings, + data, + id, + from, + to, + narrowDateRange + ); + return loading ? ( + + + + + + ) : ( + + {statItemsProps.map((mappedStatItemProps, idx) => { + return ; + })} + + ); +}; + +KpiHostsComponentBase.displayName = 'KpiHostsComponentBase'; + +export const KpiHostsComponent = React.memo(KpiHostsComponentBase); + +KpiHostsComponent.displayName = 'KpiHostsComponent'; diff --git a/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/kpi_host_details_mapping.ts b/x-pack/plugins/siem/public/hosts/components/kpi_hosts/kpi_host_details_mapping.ts similarity index 96% rename from x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/kpi_host_details_mapping.ts rename to x-pack/plugins/siem/public/hosts/components/kpi_hosts/kpi_host_details_mapping.ts index 59f8e55c461060..b3e98b70c4cb0e 100644 --- a/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/kpi_host_details_mapping.ts +++ b/x-pack/plugins/siem/public/hosts/components/kpi_hosts/kpi_host_details_mapping.ts @@ -5,7 +5,7 @@ */ import * as i18n from './translations'; -import { StatItems } from '../../../stat_items'; +import { StatItems } from '../../../common/components/stat_items'; import { KpiHostsChartColors } from './types'; export const kpiHostDetailsMapping: Readonly = [ diff --git a/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/kpi_hosts_mapping.ts b/x-pack/plugins/siem/public/hosts/components/kpi_hosts/kpi_hosts_mapping.ts similarity index 96% rename from x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/kpi_hosts_mapping.ts rename to x-pack/plugins/siem/public/hosts/components/kpi_hosts/kpi_hosts_mapping.ts index e2d6348d058409..78a9fd5b84d1fa 100644 --- a/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/kpi_hosts_mapping.ts +++ b/x-pack/plugins/siem/public/hosts/components/kpi_hosts/kpi_hosts_mapping.ts @@ -5,8 +5,8 @@ */ import * as i18n from './translations'; -import { StatItems } from '../../../stat_items'; import { KpiHostsChartColors } from './types'; +import { StatItems } from '../../../common/components/stat_items'; export const kpiHostsMapping: Readonly = [ { diff --git a/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/mock.tsx b/x-pack/plugins/siem/public/hosts/components/kpi_hosts/mock.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/mock.tsx rename to x-pack/plugins/siem/public/hosts/components/kpi_hosts/mock.tsx diff --git a/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/translations.ts b/x-pack/plugins/siem/public/hosts/components/kpi_hosts/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/translations.ts rename to x-pack/plugins/siem/public/hosts/components/kpi_hosts/translations.ts diff --git a/x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/types.ts b/x-pack/plugins/siem/public/hosts/components/kpi_hosts/types.ts similarity index 100% rename from x-pack/plugins/siem/public/components/page/hosts/kpi_hosts/types.ts rename to x-pack/plugins/siem/public/hosts/components/kpi_hosts/types.ts diff --git a/x-pack/plugins/siem/public/components/page/hosts/uncommon_process_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/hosts/components/uncommon_process_table/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/page/hosts/uncommon_process_table/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/hosts/components/uncommon_process_table/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/hosts/components/uncommon_process_table/index.test.tsx b/x-pack/plugins/siem/public/hosts/components/uncommon_process_table/index.test.tsx new file mode 100644 index 00000000000000..1fcb9b5ef621f5 --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/components/uncommon_process_table/index.test.tsx @@ -0,0 +1,345 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React from 'react'; + +import { TestProviders } from '../../../common/mock'; +import { hostsModel } from '../../store'; +import { getEmptyValue } from '../../../common/components/empty_value'; +import { useMountAppended } from '../../../common/utils/use_mount_appended'; + +import { getArgs, UncommonProcessTable, getUncommonColumnsCurated } from '.'; +import { mockData } from './mock'; +import { HostsType } from '../../store/model'; +import * as i18n from './translations'; + +describe('Uncommon Process Table Component', () => { + const loadPage = jest.fn(); + const mount = useMountAppended(); + + describe('rendering', () => { + test('it renders the default Uncommon process table', () => { + const wrapper = shallow( + + + + ); + + expect(wrapper.find('UncommonProcessTable')).toMatchSnapshot(); + }); + + test('it has a double dash (empty value) without any hosts at all', () => { + const wrapper = mount( + + + + ); + expect( + wrapper + .find('.euiTableRow') + .at(0) + .find('.euiTableRowCell') + .at(3) + .text() + ).toBe(`Host names${getEmptyValue()}`); + }); + + test('it has a single host without any extra comma when the number of hosts is exactly 1', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('.euiTableRow') + .at(1) + .find('.euiTableRowCell') + .at(3) + .text() + ).toBe('Host nameshello-world '); + }); + + test('it has a single link when the number of hosts is exactly 1', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('.euiTableRow') + .at(1) + .find('.euiTableRowCell') + .at(3) + .find('a').length + ).toBe(1); + }); + + test('it has a comma separated list of hosts when the number of hosts is greater than 1', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('.euiTableRow') + .at(2) + .find('.euiTableRowCell') + .at(3) + .text() + ).toBe('Host nameshello-world,hello-world-2 '); + }); + + test('it has 2 links when the number of hosts is equal to 2', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('.euiTableRow') + .at(2) + .find('.euiTableRowCell') + .at(3) + .find('a').length + ).toBe(2); + }); + + test('it is empty when all hosts are invalid because they do not contain an id and a name', () => { + const wrapper = mount( + + + + ); + expect( + wrapper + .find('.euiTableRow') + .at(3) + .find('.euiTableRowCell') + .at(3) + .text() + ).toBe(`Host names${getEmptyValue()}`); + }); + + test('it has no link when all hosts are invalid because they do not contain an id and a name', () => { + const wrapper = mount( + + + + ); + expect( + wrapper + .find('.euiTableRow') + .at(3) + .find('.euiTableRowCell') + .at(3) + .find('a').length + ).toBe(0); + }); + + test('it is returns two hosts when others are invalid because they do not contain an id and a name', () => { + const wrapper = mount( + + + + ); + expect( + wrapper + .find('.euiTableRow') + .at(4) + .find('.euiTableRowCell') + .at(3) + .text() + ).toBe('Host nameshello-world,hello-world-2 '); + }); + }); + + describe('#getArgs', () => { + test('it works with string array', () => { + const args = ['1', '2', '3']; + expect(getArgs(args)).toEqual('1 2 3'); + }); + + test('it returns null if empty array', () => { + const args: string[] = []; + expect(getArgs(args)).toEqual(null); + }); + + test('it returns null if given null', () => { + expect(getArgs(null)).toEqual(null); + }); + + test('it returns null if given undefined', () => { + expect(getArgs(undefined)).toEqual(null); + }); + }); + + describe('#getUncommonColumnsCurated', () => { + test('on hosts page, we expect to get all columns', () => { + expect(getUncommonColumnsCurated(HostsType.page).length).toEqual(6); + }); + + test('on host details page, we expect to remove two columns', () => { + const columns = getUncommonColumnsCurated(HostsType.details); + expect(columns.length).toEqual(4); + }); + + test('on host page, we should have hosts', () => { + const columns = getUncommonColumnsCurated(HostsType.page); + expect(columns.some(col => col.name === i18n.HOSTS)).toEqual(true); + }); + + test('on host page, we should have number of hosts', () => { + const columns = getUncommonColumnsCurated(HostsType.page); + expect(columns.some(col => col.name === i18n.NUMBER_OF_HOSTS)).toEqual(true); + }); + + test('on host details page, we should not have hosts', () => { + const columns = getUncommonColumnsCurated(HostsType.details); + expect(columns.some(col => col.name === i18n.HOSTS)).toEqual(false); + }); + + test('on host details page, we should not have number of hosts', () => { + const columns = getUncommonColumnsCurated(HostsType.details); + expect(columns.some(col => col.name === i18n.NUMBER_OF_HOSTS)).toEqual(false); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/hosts/components/uncommon_process_table/index.tsx b/x-pack/plugins/siem/public/hosts/components/uncommon_process_table/index.tsx new file mode 100644 index 00000000000000..a34cfe3327a9d1 --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/components/uncommon_process_table/index.tsx @@ -0,0 +1,238 @@ +/* + * 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. + */ + +/* eslint-disable react/display-name */ + +import React, { useCallback, useMemo } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; + +import { UncommonProcessesEdges, UncommonProcessItem } from '../../../graphql/types'; +import { State } from '../../../common/store'; +import { hostsActions, hostsModel, hostsSelectors } from '../../store'; +import { defaultToEmptyTag, getEmptyValue } from '../../../common/components/empty_value'; +import { HostDetailsLink } from '../../../common/components/links'; +import { Columns, ItemsPerRow, PaginatedTable } from '../../../common/components/paginated_table'; + +import * as i18n from './translations'; +import { getRowItemDraggables } from '../../../common/components/tables/helpers'; +import { HostsType } from '../../store/model'; +const tableType = hostsModel.HostsTableType.uncommonProcesses; +interface OwnProps { + data: UncommonProcessesEdges[]; + fakeTotalCount: number; + id: string; + isInspect: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + showMorePagesIndicator: boolean; + totalCount: number; + type: hostsModel.HostsType; +} + +export type UncommonProcessTableColumns = [ + Columns, + Columns, + Columns, + Columns, + Columns, + Columns +]; + +type UncommonProcessTableProps = OwnProps & PropsFromRedux; + +const rowItems: ItemsPerRow[] = [ + { + text: i18n.ROWS_5, + numberOfRow: 5, + }, + { + text: i18n.ROWS_10, + numberOfRow: 10, + }, +]; + +export const getArgs = (args: string[] | null | undefined): string | null => { + if (args != null && args.length !== 0) { + return args.join(' '); + } else { + return null; + } +}; + +const UncommonProcessTableComponent = React.memo( + ({ + activePage, + data, + fakeTotalCount, + id, + isInspect, + limit, + loading, + loadPage, + totalCount, + showMorePagesIndicator, + updateTableActivePage, + updateTableLimit, + type, + }) => { + const updateLimitPagination = useCallback( + newLimit => + updateTableLimit({ + hostsType: type, + limit: newLimit, + tableType, + }), + [type, updateTableLimit] + ); + + const updateActivePage = useCallback( + newPage => + updateTableActivePage({ + activePage: newPage, + hostsType: type, + tableType, + }), + [type, updateTableActivePage] + ); + + const columns = useMemo(() => getUncommonColumnsCurated(type), [type]); + + return ( + + ); + } +); + +UncommonProcessTableComponent.displayName = 'UncommonProcessTableComponent'; + +const makeMapStateToProps = () => { + const getUncommonProcessesSelector = hostsSelectors.uncommonProcessesSelector(); + return (state: State, { type }: OwnProps) => getUncommonProcessesSelector(state, type); +}; + +const mapDispatchToProps = { + updateTableActivePage: hostsActions.updateTableActivePage, + updateTableLimit: hostsActions.updateTableLimit, +}; + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const UncommonProcessTable = connector(UncommonProcessTableComponent); + +UncommonProcessTable.displayName = 'UncommonProcessTable'; + +const getUncommonColumns = (): UncommonProcessTableColumns => [ + { + name: i18n.NAME, + truncateText: false, + hideForMobile: false, + render: ({ node }) => + getRowItemDraggables({ + rowItems: node.process.name, + attrName: 'process.name', + idPrefix: `uncommon-process-table-${node._id}-processName`, + }), + width: '20%', + }, + { + align: 'right', + name: i18n.NUMBER_OF_HOSTS, + truncateText: false, + hideForMobile: false, + render: ({ node }) => <>{node.hosts != null ? node.hosts.length : getEmptyValue()}, + width: '8%', + }, + { + align: 'right', + name: i18n.NUMBER_OF_INSTANCES, + truncateText: false, + hideForMobile: false, + render: ({ node }) => defaultToEmptyTag(node.instances), + width: '8%', + }, + { + name: i18n.HOSTS, + truncateText: false, + hideForMobile: false, + render: ({ node }) => + getRowItemDraggables({ + rowItems: getHostNames(node), + attrName: 'host.name', + idPrefix: `uncommon-process-table-${node._id}-processHost`, + render: item => , + }), + width: '25%', + }, + { + name: i18n.LAST_COMMAND, + truncateText: false, + hideForMobile: false, + render: ({ node }) => + getRowItemDraggables({ + rowItems: node.process != null ? node.process.args : null, + attrName: 'process.args', + idPrefix: `uncommon-process-table-${node._id}-processArgs`, + displayCount: 1, // TODO: Change this back once we have improved the UI + }), + width: '25%', + }, + { + name: i18n.LAST_USER, + truncateText: false, + hideForMobile: false, + render: ({ node }) => + getRowItemDraggables({ + rowItems: node.user != null ? node.user.name : null, + attrName: 'user.name', + idPrefix: `uncommon-process-table-${node._id}-processUser`, + }), + }, +]; + +export const getHostNames = (node: UncommonProcessItem): string[] => { + if (node.hosts != null) { + return node.hosts + .filter(host => host.name != null && host.name[0] != null) + .map(host => (host.name != null && host.name[0] != null ? host.name[0] : '')); + } else { + return []; + } +}; + +export const getUncommonColumnsCurated = (pageType: HostsType): UncommonProcessTableColumns => { + const columns: UncommonProcessTableColumns = getUncommonColumns(); + if (pageType === HostsType.details) { + return [i18n.HOSTS, i18n.NUMBER_OF_HOSTS].reduce((acc, name) => { + acc.splice( + acc.findIndex(column => column.name === name), + 1 + ); + return acc; + }, columns); + } else { + return columns; + } +}; diff --git a/x-pack/plugins/siem/public/hosts/components/uncommon_process_table/mock.ts b/x-pack/plugins/siem/public/hosts/components/uncommon_process_table/mock.ts new file mode 100644 index 00000000000000..52b835278634b0 --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/components/uncommon_process_table/mock.ts @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UncommonProcessesData } from '../../../graphql/types'; + +export const mockData: { UncommonProcess: UncommonProcessesData } = { + UncommonProcess: { + totalCount: 5, + edges: [ + { + node: { + _id: 'cPsuhGcB0WOhS6qyTKC0', + process: { + title: ['Hello World'], + name: ['elrond.elstc.co'], + }, + hosts: [], + instances: 93, + user: { + id: ['0'], + name: ['root'], + }, + }, + cursor: { + value: '98966fa2013c396155c460d35c0902be', + }, + }, + { + node: { + _id: 'cPsuhGcB0WOhS6qyTKC0', + process: { + title: ['Hello World'], + name: ['elrond.elstc.co'], + }, + hosts: [{ id: ['host-id-1'], name: ['hello-world'] }], + instances: 93, + user: { + id: ['0'], + name: ['root'], + }, + }, + cursor: { + value: '98966fa2013c396155c460d35c0902be', + }, + }, + { + node: { + _id: 'KwQDiWcB0WOhS6qyXmrW', + process: { + title: ['Hello World'], + name: ['siem-kibana'], + }, + hosts: [ + { id: ['host-id-1'], name: ['hello-world'] }, + { id: ['host-id-2'], name: ['hello-world-2'] }, + ], + instances: 97, + user: { + id: ['1'], + name: ['Evan'], + }, + }, + cursor: { + value: 'aa7ca589f1b8220002f2fc61c64cfbf1', + }, + }, + { + node: { + _id: 'KwQDiWcB0WOhS6qyXmrW', + process: { + title: ['Hello World'], + name: ['siem-kibana'], + }, + hosts: [{ ip: ['127.0.0.1'] }], + instances: 97, + user: { + id: ['1'], + name: ['Evan'], + }, + }, + cursor: { + value: 'aa7ca589f1b8220002f2fc61c64cfbf1', + }, + }, + { + node: { + _id: 'KwQDiWcB0WOhS6qyXmrW', + process: { + title: ['Hello World'], + name: ['siem-kibana'], + }, + hosts: [ + { ip: ['127.0.0.1'] }, + { id: ['host-id-1'], name: ['hello-world'] }, + { ip: ['127.0.0.1'] }, + { id: ['host-id-2'], name: ['hello-world-2'] }, + { ip: ['127.0.0.1'] }, + ], + instances: 97, + user: { + id: ['1'], + name: ['Evan'], + }, + }, + cursor: { + value: 'aa7ca589f1b8220002f2fc61c64cfbf1', + }, + }, + ], + pageInfo: { + activePage: 1, + fakeTotalCount: 50, + showMorePagesIndicator: true, + }, + }, +}; diff --git a/x-pack/plugins/siem/public/components/page/hosts/uncommon_process_table/translations.ts b/x-pack/plugins/siem/public/hosts/components/uncommon_process_table/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/page/hosts/uncommon_process_table/translations.ts rename to x-pack/plugins/siem/public/hosts/components/uncommon_process_table/translations.ts diff --git a/x-pack/plugins/siem/public/containers/authentications/index.gql_query.ts b/x-pack/plugins/siem/public/hosts/containers/authentications/index.gql_query.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/authentications/index.gql_query.ts rename to x-pack/plugins/siem/public/hosts/containers/authentications/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/hosts/containers/authentications/index.tsx b/x-pack/plugins/siem/public/hosts/containers/authentications/index.tsx new file mode 100644 index 00000000000000..bfada0583f8e95 --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/containers/authentications/index.tsx @@ -0,0 +1,153 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; + +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { + AuthenticationsEdges, + GetAuthenticationsQuery, + PageInfoPaginated, +} from '../../../graphql/types'; +import { inputsModel, State, inputsSelectors } from '../../../common/store'; +import { createFilter, getDefaultFetchPolicy } from '../../../common/containers/helpers'; +import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; +import { withKibana, WithKibanaProps } from '../../../common/lib/kibana'; +import { + QueryTemplatePaginated, + QueryTemplatePaginatedProps, +} from '../../../common/containers/query_template_paginated'; +import { hostsModel, hostsSelectors } from '../../store'; +import { authenticationsQuery } from './index.gql_query'; + +const ID = 'authenticationQuery'; + +export interface AuthenticationArgs { + authentications: AuthenticationsEdges[]; + id: string; + inspect: inputsModel.InspectQuery; + isInspected: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + pageInfo: PageInfoPaginated; + refetch: inputsModel.Refetch; + totalCount: number; +} + +export interface OwnProps extends QueryTemplatePaginatedProps { + children: (args: AuthenticationArgs) => React.ReactNode; + type: hostsModel.HostsType; +} + +export interface AuthenticationsComponentReduxProps { + activePage: number; + isInspected: boolean; + limit: number; +} + +type AuthenticationsProps = OwnProps & AuthenticationsComponentReduxProps & WithKibanaProps; + +class AuthenticationsComponentQuery extends QueryTemplatePaginated< + AuthenticationsProps, + GetAuthenticationsQuery.Query, + GetAuthenticationsQuery.Variables +> { + public render() { + const { + activePage, + children, + endDate, + filterQuery, + id = ID, + isInspected, + kibana, + limit, + skip, + sourceId, + startDate, + } = this.props; + const variables: GetAuthenticationsQuery.Variables = { + sourceId, + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + pagination: generateTablePaginationOptions(activePage, limit), + filterQuery: createFilter(filterQuery), + defaultIndex: kibana.services.uiSettings.get(DEFAULT_INDEX_KEY), + inspect: isInspected, + }; + return ( + + query={authenticationsQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + skip={skip} + variables={variables} + > + {({ data, loading, fetchMore, networkStatus, refetch }) => { + const authentications = getOr([], 'source.Authentications.edges', data); + this.setFetchMore(fetchMore); + this.setFetchMoreOptions((newActivePage: number) => ({ + variables: { + pagination: generateTablePaginationOptions(newActivePage, limit), + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) { + return prev; + } + return { + ...fetchMoreResult, + source: { + ...fetchMoreResult.source, + Authentications: { + ...fetchMoreResult.source.Authentications, + edges: [...fetchMoreResult.source.Authentications.edges], + }, + }, + }; + }, + })); + const isLoading = this.isItAValidLoading(loading, variables, networkStatus); + return children({ + authentications, + id, + inspect: getOr(null, 'source.Authentications.inspect', data), + isInspected, + loading: isLoading, + loadPage: this.wrappedLoadMore, + pageInfo: getOr({}, 'source.Authentications.pageInfo', data), + refetch: this.memoizedRefetchQuery(variables, limit, refetch), + totalCount: getOr(-1, 'source.Authentications.totalCount', data), + }); + }} + + ); + } +} + +const makeMapStateToProps = () => { + const getAuthenticationsSelector = hostsSelectors.authenticationsSelector(); + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { type, id = ID }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + ...getAuthenticationsSelector(state, type), + isInspected, + }; + }; + return mapStateToProps; +}; + +export const AuthenticationsQuery = compose>( + connect(makeMapStateToProps), + withKibana +)(AuthenticationsComponentQuery); diff --git a/x-pack/plugins/siem/public/containers/hosts/first_last_seen/first_last_seen.gql_query.ts b/x-pack/plugins/siem/public/hosts/containers/hosts/first_last_seen/first_last_seen.gql_query.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/hosts/first_last_seen/first_last_seen.gql_query.ts rename to x-pack/plugins/siem/public/hosts/containers/hosts/first_last_seen/first_last_seen.gql_query.ts diff --git a/x-pack/plugins/siem/public/hosts/containers/hosts/first_last_seen/index.ts b/x-pack/plugins/siem/public/hosts/containers/hosts/first_last_seen/index.ts new file mode 100644 index 00000000000000..54e9147be17c06 --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/containers/hosts/first_last_seen/index.ts @@ -0,0 +1,85 @@ +/* + * 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 ApolloClient from 'apollo-client'; +import { get } from 'lodash/fp'; +import React, { useEffect, useState } from 'react'; + +import { DEFAULT_INDEX_KEY } from '../../../../../common/constants'; +import { useUiSetting$ } from '../../../../common/lib/kibana'; +import { GetHostFirstLastSeenQuery } from '../../../../graphql/types'; +import { inputsModel } from '../../../../common/store'; +import { QueryTemplateProps } from '../../../../common/containers/query_template'; + +import { HostFirstLastSeenGqlQuery } from './first_last_seen.gql_query'; + +export interface FirstLastSeenHostArgs { + id: string; + errorMessage: string; + firstSeen: Date; + lastSeen: Date; + loading: boolean; + refetch: inputsModel.Refetch; +} + +export interface OwnProps extends QueryTemplateProps { + children: (args: FirstLastSeenHostArgs) => React.ReactNode; + hostName: string; +} + +export function useFirstLastSeenHostQuery( + hostName: string, + sourceId: string, + apolloClient: ApolloClient +) { + const [loading, updateLoading] = useState(false); + const [firstSeen, updateFirstSeen] = useState(null); + const [lastSeen, updateLastSeen] = useState(null); + const [errorMessage, updateErrorMessage] = useState(null); + const [defaultIndex] = useUiSetting$(DEFAULT_INDEX_KEY); + + async function fetchFirstLastSeenHost(signal: AbortSignal) { + updateLoading(true); + return apolloClient + .query({ + query: HostFirstLastSeenGqlQuery, + fetchPolicy: 'cache-first', + variables: { + sourceId, + hostName, + defaultIndex, + }, + context: { + fetchOptions: { + signal, + }, + }, + }) + .then( + result => { + updateLoading(false); + updateFirstSeen(get('data.source.HostFirstLastSeen.firstSeen', result)); + updateLastSeen(get('data.source.HostFirstLastSeen.lastSeen', result)); + updateErrorMessage(null); + }, + error => { + updateLoading(false); + updateFirstSeen(null); + updateLastSeen(null); + updateErrorMessage(error.message); + } + ); + } + + useEffect(() => { + const abortCtrl = new AbortController(); + const signal = abortCtrl.signal; + fetchFirstLastSeenHost(signal); + return () => abortCtrl.abort(); + }, []); + + return { firstSeen, lastSeen, loading, errorMessage }; +} diff --git a/x-pack/plugins/siem/public/hosts/containers/hosts/first_last_seen/mock.ts b/x-pack/plugins/siem/public/hosts/containers/hosts/first_last_seen/mock.ts new file mode 100644 index 00000000000000..51e484ffbd8599 --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/containers/hosts/first_last_seen/mock.ts @@ -0,0 +1,52 @@ +/* + * 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_INDEX_PATTERN } from '../../../../../common/constants'; +import { GetHostFirstLastSeenQuery } from '../../../../graphql/types'; + +import { HostFirstLastSeenGqlQuery } from './first_last_seen.gql_query'; + +interface MockedProvidedQuery { + request: { + query: GetHostFirstLastSeenQuery.Query; + variables: GetHostFirstLastSeenQuery.Variables; + }; + result: { + data?: { + source: { + id: string; + HostFirstLastSeen: { + firstSeen: string | null; + lastSeen: string | null; + }; + }; + }; + errors?: [{ message: string }]; + }; +} +export const mockFirstLastSeenHostQuery: MockedProvidedQuery[] = [ + { + request: { + query: HostFirstLastSeenGqlQuery, + variables: { + sourceId: 'default', + hostName: 'kibana-siem', + defaultIndex: DEFAULT_INDEX_PATTERN, + }, + }, + result: { + data: { + source: { + id: 'default', + HostFirstLastSeen: { + firstSeen: '2019-04-08T16:09:40.692Z', + lastSeen: '2019-04-08T18:35:45.064Z', + }, + }, + }, + }, + }, +]; diff --git a/x-pack/plugins/siem/public/containers/hosts/hosts_table.gql_query.ts b/x-pack/plugins/siem/public/hosts/containers/hosts/hosts_table.gql_query.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/hosts/hosts_table.gql_query.ts rename to x-pack/plugins/siem/public/hosts/containers/hosts/hosts_table.gql_query.ts diff --git a/x-pack/plugins/siem/public/hosts/containers/hosts/index.tsx b/x-pack/plugins/siem/public/hosts/containers/hosts/index.tsx new file mode 100644 index 00000000000000..70f21b6f23cc0d --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/containers/hosts/index.tsx @@ -0,0 +1,186 @@ +/* + * 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 { get, getOr } from 'lodash/fp'; +import memoizeOne from 'memoize-one'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; + +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { + Direction, + GetHostsTableQuery, + HostsEdges, + HostsFields, + PageInfoPaginated, +} from '../../../graphql/types'; +import { inputsModel, State, inputsSelectors } from '../../../common/store'; +import { createFilter, getDefaultFetchPolicy } from '../../../common/containers/helpers'; +import { + QueryTemplatePaginated, + QueryTemplatePaginatedProps, +} from '../../../common/containers/query_template_paginated'; +import { withKibana, WithKibanaProps } from '../../../common/lib/kibana'; +import { hostsModel, hostsSelectors } from '../../store'; +import { HostsTableQuery } from './hosts_table.gql_query'; +import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; + +const ID = 'hostsQuery'; + +export interface HostsArgs { + endDate: number; + hosts: HostsEdges[]; + id: string; + inspect: inputsModel.InspectQuery; + isInspected: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + pageInfo: PageInfoPaginated; + refetch: inputsModel.Refetch; + startDate: number; + totalCount: number; +} + +export interface OwnProps extends QueryTemplatePaginatedProps { + children: (args: HostsArgs) => React.ReactNode; + type: hostsModel.HostsType; + startDate: number; + endDate: number; +} + +export interface HostsComponentReduxProps { + activePage: number; + isInspected: boolean; + limit: number; + sortField: HostsFields; + direction: Direction; +} + +type HostsProps = OwnProps & HostsComponentReduxProps & WithKibanaProps; + +class HostsComponentQuery extends QueryTemplatePaginated< + HostsProps, + GetHostsTableQuery.Query, + GetHostsTableQuery.Variables +> { + private memoizedHosts: ( + variables: string, + data: GetHostsTableQuery.Source | undefined + ) => HostsEdges[]; + + constructor(props: HostsProps) { + super(props); + this.memoizedHosts = memoizeOne(this.getHosts); + } + + public render() { + const { + activePage, + id = ID, + isInspected, + children, + direction, + filterQuery, + endDate, + kibana, + limit, + startDate, + skip, + sourceId, + sortField, + } = this.props; + const defaultIndex = kibana.services.uiSettings.get(DEFAULT_INDEX_KEY); + + const variables: GetHostsTableQuery.Variables = { + sourceId, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + sort: { + direction, + field: sortField, + }, + pagination: generateTablePaginationOptions(activePage, limit), + filterQuery: createFilter(filterQuery), + defaultIndex, + inspect: isInspected, + }; + return ( + + query={HostsTableQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + variables={variables} + skip={skip} + > + {({ data, loading, fetchMore, networkStatus, refetch }) => { + this.setFetchMore(fetchMore); + this.setFetchMoreOptions((newActivePage: number) => ({ + variables: { + pagination: generateTablePaginationOptions(newActivePage, limit), + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) { + return prev; + } + return { + ...fetchMoreResult, + source: { + ...fetchMoreResult.source, + Hosts: { + ...fetchMoreResult.source.Hosts, + edges: [...fetchMoreResult.source.Hosts.edges], + }, + }, + }; + }, + })); + const isLoading = this.isItAValidLoading(loading, variables, networkStatus); + return children({ + endDate, + hosts: this.memoizedHosts(JSON.stringify(variables), get('source', data)), + id, + inspect: getOr(null, 'source.Hosts.inspect', data), + isInspected, + loading: isLoading, + loadPage: this.wrappedLoadMore, + pageInfo: getOr({}, 'source.Hosts.pageInfo', data), + refetch: this.memoizedRefetchQuery(variables, limit, refetch), + startDate, + totalCount: getOr(-1, 'source.Hosts.totalCount', data), + }); + }} + + ); + } + + private getHosts = ( + variables: string, + source: GetHostsTableQuery.Source | undefined + ): HostsEdges[] => getOr([], 'Hosts.edges', source); +} + +const makeMapStateToProps = () => { + const getHostsSelector = hostsSelectors.hostsSelector(); + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { type, id = ID }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + ...getHostsSelector(state, type), + isInspected, + }; + }; + return mapStateToProps; +}; + +export const HostsQuery = compose>( + connect(makeMapStateToProps), + withKibana +)(HostsComponentQuery); diff --git a/x-pack/plugins/siem/public/containers/hosts/overview/host_overview.gql_query.ts b/x-pack/plugins/siem/public/hosts/containers/hosts/overview/host_overview.gql_query.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/hosts/overview/host_overview.gql_query.ts rename to x-pack/plugins/siem/public/hosts/containers/hosts/overview/host_overview.gql_query.ts diff --git a/x-pack/plugins/siem/public/hosts/containers/hosts/overview/index.tsx b/x-pack/plugins/siem/public/hosts/containers/hosts/overview/index.tsx new file mode 100644 index 00000000000000..5267fff3a26d67 --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/containers/hosts/overview/index.tsx @@ -0,0 +1,113 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; + +import { DEFAULT_INDEX_KEY } from '../../../../../common/constants'; +import { inputsModel, inputsSelectors, State } from '../../../../common/store'; +import { getDefaultFetchPolicy } from '../../../../common/containers/helpers'; +import { QueryTemplate, QueryTemplateProps } from '../../../../common/containers/query_template'; +import { withKibana, WithKibanaProps } from '../../../../common/lib/kibana'; + +import { HostOverviewQuery } from './host_overview.gql_query'; +import { GetHostOverviewQuery, HostItem } from '../../../../graphql/types'; + +const ID = 'hostOverviewQuery'; + +export interface HostOverviewArgs { + id: string; + inspect: inputsModel.InspectQuery; + hostOverview: HostItem; + loading: boolean; + refetch: inputsModel.Refetch; + startDate: number; + endDate: number; +} + +export interface HostOverviewReduxProps { + isInspected: boolean; +} + +export interface OwnProps extends QueryTemplateProps { + children: (args: HostOverviewArgs) => React.ReactNode; + hostName: string; + startDate: number; + endDate: number; +} + +type HostsOverViewProps = OwnProps & HostOverviewReduxProps & WithKibanaProps; + +class HostOverviewByNameComponentQuery extends QueryTemplate< + HostsOverViewProps, + GetHostOverviewQuery.Query, + GetHostOverviewQuery.Variables +> { + public render() { + const { + id = ID, + isInspected, + children, + hostName, + kibana, + skip, + sourceId, + startDate, + endDate, + } = this.props; + return ( + + query={HostOverviewQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + skip={skip} + variables={{ + sourceId, + hostName, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + defaultIndex: kibana.services.uiSettings.get(DEFAULT_INDEX_KEY), + inspect: isInspected, + }} + > + {({ data, loading, refetch }) => { + const hostOverview = getOr([], 'source.HostOverview', data); + return children({ + id, + inspect: getOr(null, 'source.HostOverview.inspect', data), + refetch, + loading, + hostOverview, + startDate, + endDate, + }); + }} + + ); + } +} + +const makeMapStateToProps = () => { + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = ID }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + isInspected, + }; + }; + return mapStateToProps; +}; + +export const HostOverviewByNameQuery = compose>( + connect(makeMapStateToProps), + withKibana +)(HostOverviewByNameComponentQuery); diff --git a/x-pack/plugins/siem/public/containers/kpi_host_details/index.gql_query.tsx b/x-pack/plugins/siem/public/hosts/containers/kpi_host_details/index.gql_query.tsx similarity index 100% rename from x-pack/plugins/siem/public/containers/kpi_host_details/index.gql_query.tsx rename to x-pack/plugins/siem/public/hosts/containers/kpi_host_details/index.gql_query.tsx diff --git a/x-pack/plugins/siem/public/hosts/containers/kpi_host_details/index.tsx b/x-pack/plugins/siem/public/hosts/containers/kpi_host_details/index.tsx new file mode 100644 index 00000000000000..1551e7d7067146 --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/containers/kpi_host_details/index.tsx @@ -0,0 +1,85 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect, ConnectedProps } from 'react-redux'; + +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { KpiHostDetailsData, GetKpiHostDetailsQuery } from '../../../graphql/types'; +import { inputsModel, inputsSelectors, State } from '../../../common/store'; +import { useUiSetting } from '../../../common/lib/kibana'; +import { createFilter, getDefaultFetchPolicy } from '../../../common/containers/helpers'; +import { QueryTemplateProps } from '../../../common/containers/query_template'; + +import { kpiHostDetailsQuery } from './index.gql_query'; + +const ID = 'kpiHostDetailsQuery'; + +export interface KpiHostDetailsArgs { + id: string; + inspect: inputsModel.InspectQuery; + kpiHostDetails: KpiHostDetailsData; + loading: boolean; + refetch: inputsModel.Refetch; +} + +export interface QueryKpiHostDetailsProps extends QueryTemplateProps { + children: (args: KpiHostDetailsArgs) => React.ReactNode; +} + +const KpiHostDetailsComponentQuery = React.memo( + ({ id = ID, children, endDate, filterQuery, isInspected, skip, sourceId, startDate }) => ( + + query={kpiHostDetailsQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + skip={skip} + variables={{ + sourceId, + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + filterQuery: createFilter(filterQuery), + defaultIndex: useUiSetting(DEFAULT_INDEX_KEY), + inspect: isInspected, + }} + > + {({ data, loading, refetch }) => { + const kpiHostDetails = getOr({}, `source.KpiHostDetails`, data); + return children({ + id, + inspect: getOr(null, 'source.KpiHostDetails.inspect', data), + kpiHostDetails, + loading, + refetch, + }); + }} + + ) +); + +KpiHostDetailsComponentQuery.displayName = 'KpiHostDetailsComponentQuery'; + +const makeMapStateToProps = () => { + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = ID }: QueryKpiHostDetailsProps) => { + const { isInspected } = getQuery(state, id); + return { + isInspected, + }; + }; + return mapStateToProps; +}; + +const connector = connect(makeMapStateToProps); + +type PropsFromRedux = ConnectedProps; + +export const KpiHostDetailsQuery = connector(KpiHostDetailsComponentQuery); diff --git a/x-pack/plugins/siem/public/containers/kpi_hosts/index.gql_query.ts b/x-pack/plugins/siem/public/hosts/containers/kpi_hosts/index.gql_query.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/kpi_hosts/index.gql_query.ts rename to x-pack/plugins/siem/public/hosts/containers/kpi_hosts/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/hosts/containers/kpi_hosts/index.tsx b/x-pack/plugins/siem/public/hosts/containers/kpi_hosts/index.tsx new file mode 100644 index 00000000000000..1a6df58f045976 --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/containers/kpi_hosts/index.tsx @@ -0,0 +1,85 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect, ConnectedProps } from 'react-redux'; + +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { GetKpiHostsQuery, KpiHostsData } from '../../../graphql/types'; +import { inputsModel, inputsSelectors, State } from '../../../common/store'; +import { useUiSetting } from '../../../common/lib/kibana'; +import { createFilter, getDefaultFetchPolicy } from '../../../common/containers/helpers'; +import { QueryTemplateProps } from '../../../common/containers/query_template'; + +import { kpiHostsQuery } from './index.gql_query'; + +const ID = 'kpiHostsQuery'; + +export interface KpiHostsArgs { + id: string; + inspect: inputsModel.InspectQuery; + kpiHosts: KpiHostsData; + loading: boolean; + refetch: inputsModel.Refetch; +} + +export interface KpiHostsProps extends QueryTemplateProps { + children: (args: KpiHostsArgs) => React.ReactNode; +} + +const KpiHostsComponentQuery = React.memo( + ({ id = ID, children, endDate, filterQuery, isInspected, skip, sourceId, startDate }) => ( + + query={kpiHostsQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + skip={skip} + variables={{ + sourceId, + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + filterQuery: createFilter(filterQuery), + defaultIndex: useUiSetting(DEFAULT_INDEX_KEY), + inspect: isInspected, + }} + > + {({ data, loading, refetch }) => { + const kpiHosts = getOr({}, `source.KpiHosts`, data); + return children({ + id, + inspect: getOr(null, 'source.KpiHosts.inspect', data), + kpiHosts, + loading, + refetch, + }); + }} + + ) +); + +KpiHostsComponentQuery.displayName = 'KpiHostsComponentQuery'; + +const makeMapStateToProps = () => { + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = ID }: KpiHostsProps) => { + const { isInspected } = getQuery(state, id); + return { + isInspected, + }; + }; + return mapStateToProps; +}; + +const connector = connect(makeMapStateToProps); + +type PropsFromRedux = ConnectedProps; + +export const KpiHostsQuery = connector(KpiHostsComponentQuery); diff --git a/x-pack/plugins/siem/public/containers/uncommon_processes/index.gql_query.ts b/x-pack/plugins/siem/public/hosts/containers/uncommon_processes/index.gql_query.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/uncommon_processes/index.gql_query.ts rename to x-pack/plugins/siem/public/hosts/containers/uncommon_processes/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/hosts/containers/uncommon_processes/index.tsx b/x-pack/plugins/siem/public/hosts/containers/uncommon_processes/index.tsx new file mode 100644 index 00000000000000..f8e5b1bed73cd2 --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/containers/uncommon_processes/index.tsx @@ -0,0 +1,151 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect, ConnectedProps } from 'react-redux'; +import { compose } from 'redux'; + +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { + GetUncommonProcessesQuery, + PageInfoPaginated, + UncommonProcessesEdges, +} from '../../../graphql/types'; +import { inputsModel, State, inputsSelectors } from '../../../common/store'; +import { withKibana, WithKibanaProps } from '../../../common/lib/kibana'; +import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; +import { createFilter, getDefaultFetchPolicy } from '../../../common/containers/helpers'; +import { + QueryTemplatePaginated, + QueryTemplatePaginatedProps, +} from '../../../common/containers/query_template_paginated'; +import { hostsModel, hostsSelectors } from '../../store'; +import { uncommonProcessesQuery } from './index.gql_query'; + +const ID = 'uncommonProcessesQuery'; + +export interface UncommonProcessesArgs { + id: string; + inspect: inputsModel.InspectQuery; + isInspected: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + pageInfo: PageInfoPaginated; + refetch: inputsModel.Refetch; + totalCount: number; + uncommonProcesses: UncommonProcessesEdges[]; +} + +export interface OwnProps extends QueryTemplatePaginatedProps { + children: (args: UncommonProcessesArgs) => React.ReactNode; + type: hostsModel.HostsType; +} + +type UncommonProcessesProps = OwnProps & PropsFromRedux & WithKibanaProps; + +class UncommonProcessesComponentQuery extends QueryTemplatePaginated< + UncommonProcessesProps, + GetUncommonProcessesQuery.Query, + GetUncommonProcessesQuery.Variables +> { + public render() { + const { + activePage, + children, + endDate, + filterQuery, + id = ID, + isInspected, + kibana, + limit, + skip, + sourceId, + startDate, + } = this.props; + const variables: GetUncommonProcessesQuery.Variables = { + defaultIndex: kibana.services.uiSettings.get(DEFAULT_INDEX_KEY), + filterQuery: createFilter(filterQuery), + inspect: isInspected, + pagination: generateTablePaginationOptions(activePage, limit), + sourceId, + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + }; + return ( + + query={uncommonProcessesQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + skip={skip} + variables={variables} + > + {({ data, loading, fetchMore, networkStatus, refetch }) => { + const uncommonProcesses = getOr([], 'source.UncommonProcesses.edges', data); + this.setFetchMore(fetchMore); + this.setFetchMoreOptions((newActivePage: number) => ({ + variables: { + pagination: generateTablePaginationOptions(newActivePage, limit), + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) { + return prev; + } + return { + ...fetchMoreResult, + source: { + ...fetchMoreResult.source, + UncommonProcesses: { + ...fetchMoreResult.source.UncommonProcesses, + edges: [...fetchMoreResult.source.UncommonProcesses.edges], + }, + }, + }; + }, + })); + const isLoading = this.isItAValidLoading(loading, variables, networkStatus); + return children({ + id, + inspect: getOr(null, 'source.UncommonProcesses.inspect', data), + isInspected, + loading: isLoading, + loadPage: this.wrappedLoadMore, + pageInfo: getOr({}, 'source.UncommonProcesses.pageInfo', data), + refetch: this.memoizedRefetchQuery(variables, limit, refetch), + totalCount: getOr(-1, 'source.UncommonProcesses.totalCount', data), + uncommonProcesses, + }); + }} + + ); + } +} + +const makeMapStateToProps = () => { + const getUncommonProcessesSelector = hostsSelectors.uncommonProcessesSelector(); + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { type, id = ID }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + ...getUncommonProcessesSelector(state, type), + isInspected, + }; + }; + return mapStateToProps; +}; + +const connector = connect(makeMapStateToProps); + +type PropsFromRedux = ConnectedProps; + +export const UncommonProcessesQuery = compose>( + connector, + withKibana +)(UncommonProcessesComponentQuery); diff --git a/x-pack/plugins/siem/public/hosts/index.ts b/x-pack/plugins/siem/public/hosts/index.ts new file mode 100644 index 00000000000000..6f27428e71c27c --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/index.ts @@ -0,0 +1,23 @@ +/* + * 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 { SecuritySubPluginWithStore } from '../app/types'; +import { getHostsRoutes } from './routes'; +import { initialHostsState, hostsReducer, HostsState } from './store'; + +export class Hosts { + public setup() {} + + public start(): SecuritySubPluginWithStore<'hosts', HostsState> { + return { + routes: getHostsRoutes(), + store: { + initialState: { hosts: initialHostsState }, + reducer: { hosts: hostsReducer }, + }, + }; + } +} diff --git a/x-pack/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx b/x-pack/plugins/siem/public/hosts/pages/details/details_tabs.test.tsx similarity index 92% rename from x-pack/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx rename to x-pack/plugins/siem/public/hosts/pages/details/details_tabs.test.tsx index 81c1b317d45965..fa76dc93375e01 100644 --- a/x-pack/plugins/siem/public/pages/hosts/details/details_tabs.test.tsx +++ b/x-pack/plugins/siem/public/hosts/pages/details/details_tabs.test.tsx @@ -9,16 +9,16 @@ import { IIndexPattern } from 'src/plugins/data/public'; import { MemoryRouter } from 'react-router-dom'; import useResizeObserver from 'use-resize-observer/polyfilled'; -import { mockIndexPattern } from '../../../mock/index_pattern'; -import { TestProviders } from '../../../mock/test_providers'; +import { mockIndexPattern } from '../../../common/mock/index_pattern'; +import { TestProviders } from '../../../common/mock/test_providers'; import { HostDetailsTabs } from './details_tabs'; import { HostDetailsTabsProps, SetAbsoluteRangeDatePicker } from './types'; import { hostDetailsPagePath } from '../types'; import { type } from './utils'; -import { useMountAppended } from '../../../utils/use_mount_appended'; +import { useMountAppended } from '../../../common/utils/use_mount_appended'; import { getHostDetailsPageFilters } from './helpers'; -jest.mock('../../../containers/source', () => ({ +jest.mock('../../../common/containers/source', () => ({ indicesExistOrDataTemporarilyUnavailable: () => true, WithSource: ({ children, @@ -29,10 +29,10 @@ jest.mock('../../../containers/source', () => ({ // Test will fail because we will to need to mock some core services to make the test work // For now let's forget about SiemSearchBar and QueryBar -jest.mock('../../../components/search_bar', () => ({ +jest.mock('../../../common/components/search_bar', () => ({ SiemSearchBar: () => null, })); -jest.mock('../../../components/query_bar', () => ({ +jest.mock('../../../common/components/query_bar', () => ({ QueryBar: () => null, })); diff --git a/x-pack/plugins/siem/public/pages/hosts/details/details_tabs.tsx b/x-pack/plugins/siem/public/hosts/pages/details/details_tabs.tsx similarity index 85% rename from x-pack/plugins/siem/public/pages/hosts/details/details_tabs.tsx rename to x-pack/plugins/siem/public/hosts/pages/details/details_tabs.tsx index d6c0211901ff05..505d0f37ca0392 100644 --- a/x-pack/plugins/siem/public/pages/hosts/details/details_tabs.tsx +++ b/x-pack/plugins/siem/public/hosts/pages/details/details_tabs.tsx @@ -7,12 +7,12 @@ import React, { useCallback } from 'react'; import { Route, Switch } from 'react-router-dom'; -import { UpdateDateRange } from '../../../components/charts/common'; -import { scoreIntervalToDateTime } from '../../../components/ml/score/score_interval_to_datetime'; -import { Anomaly } from '../../../components/ml/types'; -import { HostsTableType } from '../../../store/hosts/model'; -import { AnomaliesQueryTabBody } from '../../../containers/anomalies/anomalies_query_tab_body'; -import { AnomaliesHostTable } from '../../../components/ml/tables/anomalies_host_table'; +import { UpdateDateRange } from '../../../common/components/charts/common'; +import { scoreIntervalToDateTime } from '../../../common/components/ml/score/score_interval_to_datetime'; +import { Anomaly } from '../../../common/components/ml/types'; +import { HostsTableType } from '../../store/model'; +import { AnomaliesQueryTabBody } from '../../../common/containers/anomalies/anomalies_query_tab_body'; +import { AnomaliesHostTable } from '../../../common/components/ml/tables/anomalies_host_table'; import { HostDetailsTabsProps } from './types'; import { type } from './utils'; diff --git a/x-pack/plugins/siem/public/pages/hosts/details/helpers.test.ts b/x-pack/plugins/siem/public/hosts/pages/details/helpers.test.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/hosts/details/helpers.test.ts rename to x-pack/plugins/siem/public/hosts/pages/details/helpers.test.ts diff --git a/x-pack/plugins/siem/public/hosts/pages/details/helpers.ts b/x-pack/plugins/siem/public/hosts/pages/details/helpers.ts new file mode 100644 index 00000000000000..9ec0084d708a02 --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/pages/details/helpers.ts @@ -0,0 +1,49 @@ +/* + * 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 { escapeQueryValue } from '../../../common/lib/keury'; +import { Filter } from '../../../../../../../src/plugins/data/public'; + +/** Returns the kqlQueryExpression for the `Events` widget on the `Host Details` page */ +export const getHostDetailsEventsKqlQueryExpression = ({ + filterQueryExpression, + hostName, +}: { + filterQueryExpression: string; + hostName: string; +}): string => { + if (filterQueryExpression.length) { + return `${filterQueryExpression}${ + hostName.length ? ` and host.name: ${escapeQueryValue(hostName)}` : '' + }`; + } else { + return hostName.length ? `host.name: ${escapeQueryValue(hostName)}` : ''; + } +}; + +export const getHostDetailsPageFilters = (hostName: string): Filter[] => [ + { + meta: { + alias: null, + negate: false, + disabled: false, + type: 'phrase', + key: 'host.name', + value: hostName, + params: { + query: hostName, + }, + }, + query: { + match: { + 'host.name': { + query: hostName, + type: 'phrase', + }, + }, + }, + }, +]; diff --git a/x-pack/plugins/siem/public/hosts/pages/details/index.tsx b/x-pack/plugins/siem/public/hosts/pages/details/index.tsx new file mode 100644 index 00000000000000..a5fabf4d515f86 --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/pages/details/index.tsx @@ -0,0 +1,237 @@ +/* + * 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 { EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; +import React, { useEffect, useCallback, useMemo } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import { StickyContainer } from 'react-sticky'; + +import { UpdateDateRange } from '../../../common/components/charts/common'; +import { FiltersGlobal } from '../../../common/components/filters_global'; +import { HeaderPage } from '../../../common/components/header_page'; +import { LastEventTime } from '../../../common/components/last_event_time'; +import { AnomalyTableProvider } from '../../../common/components/ml/anomaly/anomaly_table_provider'; +import { hostToCriteria } from '../../../common/components/ml/criteria/host_to_criteria'; +import { hasMlUserPermissions } from '../../../../common/machine_learning/has_ml_user_permissions'; +import { useMlCapabilities } from '../../../common/components/ml_popover/hooks/use_ml_capabilities'; +import { scoreIntervalToDateTime } from '../../../common/components/ml/score/score_interval_to_datetime'; +import { SiemNavigation } from '../../../common/components/navigation'; +import { KpiHostsComponent } from '../../components/kpi_hosts'; +import { HostOverview } from '../../../overview/components/host_overview'; +import { manageQuery } from '../../../common/components/page/manage_query'; +import { SiemSearchBar } from '../../../common/components/search_bar'; +import { WrapperPage } from '../../../common/components/wrapper_page'; +import { HostOverviewByNameQuery } from '../../containers/hosts/overview'; +import { KpiHostDetailsQuery } from '../../containers/kpi_host_details'; +import { + indicesExistOrDataTemporarilyUnavailable, + WithSource, +} from '../../../common/containers/source'; +import { LastEventIndexKey } from '../../../graphql/types'; +import { useKibana } from '../../../common/lib/kibana'; +import { convertToBuildEsQuery } from '../../../common/lib/keury'; +import { inputsSelectors, State } from '../../../common/store'; +import { setHostDetailsTablesActivePageToZero as dispatchHostDetailsTablesActivePageToZero } from '../../store/actions'; +import { setAbsoluteRangeDatePicker as dispatchAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions'; +import { SpyRoute } from '../../../common/utils/route/spy_routes'; +import { esQuery, Filter } from '../../../../../../../src/plugins/data/public'; + +import { HostsEmptyPage } from '../hosts_empty_page'; +import { HostDetailsTabs } from './details_tabs'; +import { navTabsHostDetails } from './nav_tabs'; +import { HostDetailsProps } from './types'; +import { type } from './utils'; +import { getHostDetailsPageFilters } from './helpers'; + +const HostOverviewManage = manageQuery(HostOverview); +const KpiHostDetailsManage = manageQuery(KpiHostsComponent); + +const HostDetailsComponent = React.memo( + ({ + filters, + from, + isInitializing, + query, + setAbsoluteRangeDatePicker, + setHostDetailsTablesActivePageToZero, + setQuery, + to, + detailName, + deleteQuery, + hostDetailsPagePath, + }) => { + useEffect(() => { + setHostDetailsTablesActivePageToZero(); + }, [setHostDetailsTablesActivePageToZero, detailName]); + const capabilities = useMlCapabilities(); + const kibana = useKibana(); + const hostDetailsPageFilters: Filter[] = useMemo(() => getHostDetailsPageFilters(detailName), [ + detailName, + ]); + const getFilters = () => [...hostDetailsPageFilters, ...filters]; + const narrowDateRange = useCallback( + ({ x }) => { + if (!x) { + return; + } + const [min, max] = x; + setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + }, + [setAbsoluteRangeDatePicker] + ); + + return ( + <> + + {({ indicesExist, indexPattern }) => { + const filterQuery = convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + indexPattern, + queries: [query], + filters: getFilters(), + }); + return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( + + + + + + + + } + title={detailName} + /> + + + {({ hostOverview, loading, id, inspect, refetch }) => ( + + {({ isLoadingAnomaliesData, anomaliesData }) => ( + { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }} + /> + )} + + )} + + + + + + {({ kpiHostDetails, id, inspect, loading, refetch }) => ( + + )} + + + + + + + + + + + + ) : ( + + + + + + ); + }} + + + + + ); + } +); +HostDetailsComponent.displayName = 'HostDetailsComponent'; + +export const makeMapStateToProps = () => { + const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); + const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); + return (state: State) => ({ + query: getGlobalQuerySelector(state), + filters: getGlobalFiltersQuerySelector(state), + }); +}; + +const mapDispatchToProps = { + setAbsoluteRangeDatePicker: dispatchAbsoluteRangeDatePicker, + setHostDetailsTablesActivePageToZero: dispatchHostDetailsTablesActivePageToZero, +}; + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const HostDetails = connector(HostDetailsComponent); diff --git a/x-pack/plugins/siem/public/hosts/pages/details/nav_tabs.test.tsx b/x-pack/plugins/siem/public/hosts/pages/details/nav_tabs.test.tsx new file mode 100644 index 00000000000000..0dd31dc9abce7f --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/pages/details/nav_tabs.test.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HostsTableType } from '../../store/model'; +import { navTabsHostDetails } from './nav_tabs'; + +describe('navTabsHostDetails', () => { + const mockHostName = 'mockHostName'; + test('it should skip anomalies tab if without mlUserPermission', () => { + const tabs = navTabsHostDetails(mockHostName, false); + expect(tabs).toHaveProperty(HostsTableType.authentications); + expect(tabs).toHaveProperty(HostsTableType.uncommonProcesses); + expect(tabs).not.toHaveProperty(HostsTableType.anomalies); + expect(tabs).toHaveProperty(HostsTableType.events); + }); + + test('it should display anomalies tab if with mlUserPermission', () => { + const tabs = navTabsHostDetails(mockHostName, true); + expect(tabs).toHaveProperty(HostsTableType.authentications); + expect(tabs).toHaveProperty(HostsTableType.uncommonProcesses); + expect(tabs).toHaveProperty(HostsTableType.anomalies); + expect(tabs).toHaveProperty(HostsTableType.events); + }); +}); diff --git a/x-pack/plugins/siem/public/hosts/pages/details/nav_tabs.tsx b/x-pack/plugins/siem/public/hosts/pages/details/nav_tabs.tsx new file mode 100644 index 00000000000000..4d04d16580a638 --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/pages/details/nav_tabs.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { omit } from 'lodash/fp'; +import * as i18n from '../translations'; +import { HostDetailsNavTab } from './types'; +import { HostsTableType } from '../../store/model'; +import { SiemPageName } from '../../../app/types'; + +const getTabsOnHostDetailsUrl = (hostName: string, tabName: HostsTableType) => + `#/${SiemPageName.hosts}/${hostName}/${tabName}`; + +export const navTabsHostDetails = ( + hostName: string, + hasMlUserPermissions: boolean +): HostDetailsNavTab => { + const hostDetailsNavTabs = { + [HostsTableType.authentications]: { + id: HostsTableType.authentications, + name: i18n.NAVIGATION_AUTHENTICATIONS_TITLE, + href: getTabsOnHostDetailsUrl(hostName, HostsTableType.authentications), + disabled: false, + urlKey: 'host', + isDetailPage: true, + }, + [HostsTableType.uncommonProcesses]: { + id: HostsTableType.uncommonProcesses, + name: i18n.NAVIGATION_UNCOMMON_PROCESSES_TITLE, + href: getTabsOnHostDetailsUrl(hostName, HostsTableType.uncommonProcesses), + disabled: false, + urlKey: 'host', + isDetailPage: true, + }, + [HostsTableType.anomalies]: { + id: HostsTableType.anomalies, + name: i18n.NAVIGATION_ANOMALIES_TITLE, + href: getTabsOnHostDetailsUrl(hostName, HostsTableType.anomalies), + disabled: false, + urlKey: 'host', + isDetailPage: true, + }, + [HostsTableType.events]: { + id: HostsTableType.events, + name: i18n.NAVIGATION_EVENTS_TITLE, + href: getTabsOnHostDetailsUrl(hostName, HostsTableType.events), + disabled: false, + urlKey: 'host', + isDetailPage: true, + }, + [HostsTableType.alerts]: { + id: HostsTableType.alerts, + name: i18n.NAVIGATION_ALERTS_TITLE, + href: getTabsOnHostDetailsUrl(hostName, HostsTableType.alerts), + disabled: false, + urlKey: 'host', + }, + }; + + return hasMlUserPermissions + ? hostDetailsNavTabs + : omit(HostsTableType.anomalies, hostDetailsNavTabs); +}; diff --git a/x-pack/plugins/siem/public/hosts/pages/details/types.ts b/x-pack/plugins/siem/public/hosts/pages/details/types.ts new file mode 100644 index 00000000000000..f145abed2d8ffc --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/pages/details/types.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ActionCreator } from 'typescript-fsa'; +import { Query, IIndexPattern, Filter } from 'src/plugins/data/public'; +import { InputsModelId } from '../../../common/store/inputs/constants'; +import { HostComponentProps } from '../../../common/components/link_to/redirect_to_hosts'; +import { HostsTableType } from '../../store/model'; +import { HostsQueryProps } from '../types'; +import { NavTab } from '../../../common/components/navigation/types'; +import { KeyHostsNavTabWithoutMlPermission } from '../navigation/types'; +import { hostsModel } from '../../store'; + +interface HostDetailsComponentReduxProps { + query: Query; + filters: Filter[]; +} + +interface HostBodyComponentDispatchProps { + setAbsoluteRangeDatePicker: ActionCreator<{ + id: InputsModelId; + from: number; + to: number; + }>; + detailName: string; + hostDetailsPagePath: string; +} + +interface HostDetailsComponentDispatchProps extends HostBodyComponentDispatchProps { + setHostDetailsTablesActivePageToZero: ActionCreator; +} + +export interface HostDetailsProps extends HostsQueryProps { + detailName: string; + hostDetailsPagePath: string; +} + +export type HostDetailsComponentProps = HostDetailsComponentReduxProps & + HostDetailsComponentDispatchProps & + HostComponentProps & + HostsQueryProps; + +type KeyHostDetailsNavTabWithoutMlPermission = HostsTableType.authentications & + HostsTableType.uncommonProcesses & + HostsTableType.events; + +type KeyHostDetailsNavTabWithMlPermission = KeyHostsNavTabWithoutMlPermission & + HostsTableType.anomalies; + +type KeyHostDetailsNavTab = + | KeyHostDetailsNavTabWithoutMlPermission + | KeyHostDetailsNavTabWithMlPermission; + +export type HostDetailsNavTab = Record; + +export type HostDetailsTabsProps = HostBodyComponentDispatchProps & + HostsQueryProps & { + pageFilters?: Filter[]; + filterQuery: string; + indexPattern: IIndexPattern; + type: hostsModel.HostsType; + }; + +export type SetAbsoluteRangeDatePicker = ActionCreator<{ + id: InputsModelId; + from: number; + to: number; +}>; diff --git a/x-pack/plugins/siem/public/hosts/pages/details/utils.ts b/x-pack/plugins/siem/public/hosts/pages/details/utils.ts new file mode 100644 index 00000000000000..d45cb3368b4e19 --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/pages/details/utils.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get, isEmpty } from 'lodash/fp'; + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ChromeBreadcrumb } from '../../../../../../../src/core/public'; +import { hostsModel } from '../../store'; +import { HostsTableType } from '../../store/model'; +import { + getHostsUrl, + getHostDetailsUrl, +} from '../../../common/components/link_to/redirect_to_hosts'; + +import * as i18n from '../translations'; +import { HostRouteSpyState } from '../../../common/utils/route/types'; + +export const type = hostsModel.HostsType.details; + +const TabNameMappedToI18nKey: Record = { + [HostsTableType.hosts]: i18n.NAVIGATION_ALL_HOSTS_TITLE, + [HostsTableType.authentications]: i18n.NAVIGATION_AUTHENTICATIONS_TITLE, + [HostsTableType.uncommonProcesses]: i18n.NAVIGATION_UNCOMMON_PROCESSES_TITLE, + [HostsTableType.anomalies]: i18n.NAVIGATION_ANOMALIES_TITLE, + [HostsTableType.events]: i18n.NAVIGATION_EVENTS_TITLE, + [HostsTableType.alerts]: i18n.NAVIGATION_ALERTS_TITLE, +}; + +export const getBreadcrumbs = (params: HostRouteSpyState, search: string[]): ChromeBreadcrumb[] => { + let breadcrumb = [ + { + text: i18n.PAGE_TITLE, + href: `${getHostsUrl()}${!isEmpty(search[0]) ? search[0] : ''}`, + }, + ]; + + if (params.detailName != null) { + breadcrumb = [ + ...breadcrumb, + { + text: params.detailName, + href: `${getHostDetailsUrl(params.detailName)}${!isEmpty(search[1]) ? search[1] : ''}`, + }, + ]; + } + if (params.tabName != null) { + const tabName = get('tabName', params); + if (!tabName) return breadcrumb; + + breadcrumb = [ + ...breadcrumb, + { + text: TabNameMappedToI18nKey[tabName], + href: '', + }, + ]; + } + return breadcrumb; +}; diff --git a/x-pack/plugins/siem/public/pages/hosts/hosts.test.tsx b/x-pack/plugins/siem/public/hosts/pages/hosts.test.tsx similarity index 88% rename from x-pack/plugins/siem/public/pages/hosts/hosts.test.tsx rename to x-pack/plugins/siem/public/hosts/pages/hosts.test.tsx index 6134c1dd6911a2..5cb35eaa775b6f 100644 --- a/x-pack/plugins/siem/public/pages/hosts/hosts.test.tsx +++ b/x-pack/plugins/siem/public/hosts/pages/hosts.test.tsx @@ -11,23 +11,28 @@ import { Router } from 'react-router-dom'; import { MockedProvider } from 'react-apollo/test-utils'; import { Filter } from '../../../../../../src/plugins/data/common/es_query'; -import '../../mock/match_media'; -import { mocksSource } from '../../containers/source/mock'; -import { wait } from '../../lib/helpers'; -import { apolloClientObservable, TestProviders, mockGlobalState } from '../../mock'; -import { SiemNavigation } from '../../components/navigation'; -import { inputsActions } from '../../store/inputs'; -import { State, createStore } from '../../store'; +import '../../common/mock/match_media'; +import { mocksSource } from '../../common/containers/source/mock'; +import { wait } from '../../common/lib/helpers'; +import { + apolloClientObservable, + TestProviders, + mockGlobalState, + SUB_PLUGINS_REDUCER, +} from '../../common/mock'; +import { SiemNavigation } from '../../common/components/navigation'; +import { inputsActions } from '../../common/store/inputs'; +import { State, createStore } from '../../common/store'; import { HostsComponentProps } from './types'; import { Hosts } from './hosts'; import { HostsTabs } from './hosts_tabs'; // Test will fail because we will to need to mock some core services to make the test work // For now let's forget about SiemSearchBar and QueryBar -jest.mock('../../components/search_bar', () => ({ +jest.mock('../../common/components/search_bar', () => ({ SiemSearchBar: () => null, })); -jest.mock('../../components/query_bar', () => ({ +jest.mock('../../common/components/query_bar', () => ({ QueryBar: () => null, })); @@ -166,7 +171,7 @@ describe('Hosts - rendering', () => { ]; localSource[0].result.data.source.status.indicesExist = true; const myState: State = mockGlobalState; - const myStore = createStore(myState, apolloClientObservable); + const myStore = createStore(myState, SUB_PLUGINS_REDUCER, apolloClientObservable); const wrapper = mount( diff --git a/x-pack/plugins/siem/public/pages/hosts/hosts.tsx b/x-pack/plugins/siem/public/hosts/pages/hosts.tsx similarity index 80% rename from x-pack/plugins/siem/public/pages/hosts/hosts.tsx rename to x-pack/plugins/siem/public/hosts/pages/hosts.tsx index 2fbbc0d96a1e35..f7583f65a4fcd4 100644 --- a/x-pack/plugins/siem/public/pages/hosts/hosts.tsx +++ b/x-pack/plugins/siem/public/hosts/pages/hosts.tsx @@ -10,34 +10,38 @@ import { connect, ConnectedProps } from 'react-redux'; import { StickyContainer } from 'react-sticky'; import { useParams } from 'react-router-dom'; -import { UpdateDateRange } from '../../components/charts/common'; -import { FiltersGlobal } from '../../components/filters_global'; -import { HeaderPage } from '../../components/header_page'; -import { LastEventTime } from '../../components/last_event_time'; -import { hasMlUserPermissions } from '../../components/ml/permissions/has_ml_user_permissions'; -import { SiemNavigation } from '../../components/navigation'; -import { KpiHostsComponent } from '../../components/page/hosts'; -import { manageQuery } from '../../components/page/manage_query'; -import { SiemSearchBar } from '../../components/search_bar'; -import { WrapperPage } from '../../components/wrapper_page'; -import { KpiHostsQuery } from '../../containers/kpi_hosts'; -import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source'; +import { UpdateDateRange } from '../../common/components/charts/common'; +import { FiltersGlobal } from '../../common/components/filters_global'; +import { HeaderPage } from '../../common/components/header_page'; +import { LastEventTime } from '../../common/components/last_event_time'; +import { hasMlUserPermissions } from '../../../common/machine_learning/has_ml_user_permissions'; +import { SiemNavigation } from '../../common/components/navigation'; +import { KpiHostsComponent } from '../components/kpi_hosts'; +import { manageQuery } from '../../common/components/page/manage_query'; +import { SiemSearchBar } from '../../common/components/search_bar'; +import { WrapperPage } from '../../common/components/wrapper_page'; +import { KpiHostsQuery } from '../containers/kpi_hosts'; +import { + indicesExistOrDataTemporarilyUnavailable, + WithSource, +} from '../../common/containers/source'; import { LastEventIndexKey } from '../../graphql/types'; -import { useKibana } from '../../lib/kibana'; -import { convertToBuildEsQuery } from '../../lib/keury'; -import { inputsSelectors, State, hostsModel } from '../../store'; -import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions'; +import { useKibana } from '../../common/lib/kibana'; +import { convertToBuildEsQuery } from '../../common/lib/keury'; +import { inputsSelectors, State } from '../../common/store'; +import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../common/store/inputs/actions'; -import { SpyRoute } from '../../utils/route/spy_routes'; +import { SpyRoute } from '../../common/utils/route/spy_routes'; import { esQuery } from '../../../../../../src/plugins/data/public'; -import { useMlCapabilities } from '../../components/ml_popover/hooks/use_ml_capabilities'; +import { useMlCapabilities } from '../../common/components/ml_popover/hooks/use_ml_capabilities'; import { HostsEmptyPage } from './hosts_empty_page'; import { HostsTabs } from './hosts_tabs'; import { navTabsHosts } from './nav_tabs'; import * as i18n from './translations'; import { HostsComponentProps } from './types'; import { filterHostData } from './navigation'; -import { HostsTableType } from '../../store/hosts/model'; +import { hostsModel } from '../store'; +import { HostsTableType } from '../store/model'; const KpiHostsComponentManage = manageQuery(KpiHostsComponent); diff --git a/x-pack/plugins/siem/public/pages/hosts/hosts_empty_page.tsx b/x-pack/plugins/siem/public/hosts/pages/hosts_empty_page.tsx similarity index 85% rename from x-pack/plugins/siem/public/pages/hosts/hosts_empty_page.tsx rename to x-pack/plugins/siem/public/hosts/pages/hosts_empty_page.tsx index bded0b90e187be..e52fc89678038f 100644 --- a/x-pack/plugins/siem/public/pages/hosts/hosts_empty_page.tsx +++ b/x-pack/plugins/siem/public/hosts/pages/hosts_empty_page.tsx @@ -6,10 +6,9 @@ import React from 'react'; -import { EmptyPage } from '../../components/empty_page'; -import { useKibana } from '../../lib/kibana'; - -import * as i18n from '../common/translations'; +import { EmptyPage } from '../../common/components/empty_page'; +import { useKibana } from '../../common/lib/kibana'; +import * as i18n from '../../common/translations'; export const HostsEmptyPage = React.memo(() => { const { http, docLinks } = useKibana().services; diff --git a/x-pack/plugins/siem/public/pages/hosts/hosts_tabs.tsx b/x-pack/plugins/siem/public/hosts/pages/hosts_tabs.tsx similarity index 84% rename from x-pack/plugins/siem/public/pages/hosts/hosts_tabs.tsx rename to x-pack/plugins/siem/public/hosts/pages/hosts_tabs.tsx index de25deeb5b477c..549c198a435263 100644 --- a/x-pack/plugins/siem/public/pages/hosts/hosts_tabs.tsx +++ b/x-pack/plugins/siem/public/hosts/pages/hosts_tabs.tsx @@ -8,12 +8,12 @@ import React, { memo, useCallback } from 'react'; import { Route, Switch } from 'react-router-dom'; import { HostsTabsProps } from './types'; -import { scoreIntervalToDateTime } from '../../components/ml/score/score_interval_to_datetime'; -import { Anomaly } from '../../components/ml/types'; -import { HostsTableType } from '../../store/hosts/model'; -import { AnomaliesQueryTabBody } from '../../containers/anomalies/anomalies_query_tab_body'; -import { AnomaliesHostTable } from '../../components/ml/tables/anomalies_host_table'; -import { UpdateDateRange } from '../../components/charts/common'; +import { scoreIntervalToDateTime } from '../../common/components/ml/score/score_interval_to_datetime'; +import { Anomaly } from '../../common/components/ml/types'; +import { HostsTableType } from '../store/model'; +import { AnomaliesQueryTabBody } from '../../common/containers/anomalies/anomalies_query_tab_body'; +import { AnomaliesHostTable } from '../../common/components/ml/tables/anomalies_host_table'; +import { UpdateDateRange } from '../../common/components/charts/common'; import { HostsQueryTabBody, diff --git a/x-pack/plugins/siem/public/hosts/pages/index.tsx b/x-pack/plugins/siem/public/hosts/pages/index.tsx new file mode 100644 index 00000000000000..336abc60e5ba16 --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/pages/index.tsx @@ -0,0 +1,95 @@ +/* + * 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 { Redirect, Route, Switch, RouteComponentProps } from 'react-router-dom'; + +import { HostDetails } from './details'; +import { HostsTableType } from '../store/model'; + +import { GlobalTime } from '../../common/containers/global_time'; +import { SiemPageName } from '../../app/types'; +import { Hosts } from './hosts'; +import { hostsPagePath, hostDetailsPagePath } from './types'; + +const getHostsTabPath = (pagePath: string) => + `${pagePath}/:tabName(` + + `${HostsTableType.hosts}|` + + `${HostsTableType.authentications}|` + + `${HostsTableType.uncommonProcesses}|` + + `${HostsTableType.anomalies}|` + + `${HostsTableType.events}|` + + `${HostsTableType.alerts})`; + +const getHostDetailsTabPath = (pagePath: string) => + `${hostDetailsPagePath}/:tabName(` + + `${HostsTableType.authentications}|` + + `${HostsTableType.uncommonProcesses}|` + + `${HostsTableType.anomalies}|` + + `${HostsTableType.events}|` + + `${HostsTableType.alerts})`; + +type Props = Partial> & { url: string }; + +export const HostsContainer = React.memo(({ url }) => ( + + {({ to, from, setQuery, deleteQuery, isInitializing }) => ( + + ( + + )} + /> + ( + + )} + /> + } + /> + ( + + )} + /> + + )} + +)); + +HostsContainer.displayName = 'HostsContainer'; diff --git a/x-pack/plugins/siem/public/hosts/pages/nav_tabs.test.tsx b/x-pack/plugins/siem/public/hosts/pages/nav_tabs.test.tsx new file mode 100644 index 00000000000000..745c454e13f5aa --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/pages/nav_tabs.test.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HostsTableType } from '../store/model'; +import { navTabsHosts } from './nav_tabs'; + +describe('navTabsHosts', () => { + test('it should skip anomalies tab if without mlUserPermission', () => { + const tabs = navTabsHosts(false); + expect(tabs).toHaveProperty(HostsTableType.hosts); + expect(tabs).toHaveProperty(HostsTableType.authentications); + expect(tabs).toHaveProperty(HostsTableType.uncommonProcesses); + expect(tabs).not.toHaveProperty(HostsTableType.anomalies); + expect(tabs).toHaveProperty(HostsTableType.events); + }); + + test('it should display anomalies tab if with mlUserPermission', () => { + const tabs = navTabsHosts(true); + expect(tabs).toHaveProperty(HostsTableType.hosts); + expect(tabs).toHaveProperty(HostsTableType.authentications); + expect(tabs).toHaveProperty(HostsTableType.uncommonProcesses); + expect(tabs).toHaveProperty(HostsTableType.anomalies); + expect(tabs).toHaveProperty(HostsTableType.events); + }); +}); diff --git a/x-pack/plugins/siem/public/hosts/pages/nav_tabs.tsx b/x-pack/plugins/siem/public/hosts/pages/nav_tabs.tsx new file mode 100644 index 00000000000000..9bab3f7efe74af --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/pages/nav_tabs.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { omit } from 'lodash/fp'; +import * as i18n from './translations'; +import { HostsTableType } from '../store/model'; +import { HostsNavTab } from './navigation/types'; +import { SiemPageName } from '../../app/types'; + +const getTabsOnHostsUrl = (tabName: HostsTableType) => `#/${SiemPageName.hosts}/${tabName}`; + +export const navTabsHosts = (hasMlUserPermissions: boolean): HostsNavTab => { + const hostsNavTabs = { + [HostsTableType.hosts]: { + id: HostsTableType.hosts, + name: i18n.NAVIGATION_ALL_HOSTS_TITLE, + href: getTabsOnHostsUrl(HostsTableType.hosts), + disabled: false, + urlKey: 'host', + }, + [HostsTableType.authentications]: { + id: HostsTableType.authentications, + name: i18n.NAVIGATION_AUTHENTICATIONS_TITLE, + href: getTabsOnHostsUrl(HostsTableType.authentications), + disabled: false, + urlKey: 'host', + }, + [HostsTableType.uncommonProcesses]: { + id: HostsTableType.uncommonProcesses, + name: i18n.NAVIGATION_UNCOMMON_PROCESSES_TITLE, + href: getTabsOnHostsUrl(HostsTableType.uncommonProcesses), + disabled: false, + urlKey: 'host', + }, + [HostsTableType.anomalies]: { + id: HostsTableType.anomalies, + name: i18n.NAVIGATION_ANOMALIES_TITLE, + href: getTabsOnHostsUrl(HostsTableType.anomalies), + disabled: false, + urlKey: 'host', + }, + [HostsTableType.events]: { + id: HostsTableType.events, + name: i18n.NAVIGATION_EVENTS_TITLE, + href: getTabsOnHostsUrl(HostsTableType.events), + disabled: false, + urlKey: 'host', + }, + [HostsTableType.alerts]: { + id: HostsTableType.alerts, + name: i18n.NAVIGATION_ALERTS_TITLE, + href: getTabsOnHostsUrl(HostsTableType.alerts), + disabled: false, + urlKey: 'host', + }, + }; + + return hasMlUserPermissions ? hostsNavTabs : omit([HostsTableType.anomalies], hostsNavTabs); +}; diff --git a/x-pack/plugins/siem/public/hosts/pages/navigation/alerts_query_tab_body.tsx b/x-pack/plugins/siem/public/hosts/pages/navigation/alerts_query_tab_body.tsx new file mode 100644 index 00000000000000..a0d8df6b87514b --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/pages/navigation/alerts_query_tab_body.tsx @@ -0,0 +1,54 @@ +/* + * 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, { useMemo } from 'react'; + +import { Filter } from '../../../../../../../src/plugins/data/public'; +import { AlertsView } from '../../../common/components/alerts_viewer'; +import { AlertsComponentQueryProps } from './types'; + +export const filterHostData: Filter[] = [ + { + query: { + bool: { + filter: [ + { + bool: { + should: [ + { + exists: { + field: 'host.name', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + meta: { + alias: '', + disabled: false, + key: 'bool', + negate: false, + type: 'custom', + value: + '{"query": {"bool": {"filter": [{"bool": {"should": [{"exists": {"field": "host.name"}}],"minimum_should_match": 1}}]}}}', + }, + }, +]; +export const HostAlertsQueryTabBody = React.memo((alertsProps: AlertsComponentQueryProps) => { + const { pageFilters, ...rest } = alertsProps; + const hostPageFilters = useMemo( + () => (pageFilters != null ? [...filterHostData, ...pageFilters] : filterHostData), + [pageFilters] + ); + + return ; +}); + +HostAlertsQueryTabBody.displayName = 'HostAlertsQueryTabBody'; diff --git a/x-pack/plugins/siem/public/pages/hosts/navigation/authentications_query_tab_body.tsx b/x-pack/plugins/siem/public/hosts/pages/navigation/authentications_query_tab_body.tsx similarity index 86% rename from x-pack/plugins/siem/public/pages/hosts/navigation/authentications_query_tab_body.tsx rename to x-pack/plugins/siem/public/hosts/pages/navigation/authentications_query_tab_body.tsx index 5a6759fd072217..ae7c0205acf56a 100644 --- a/x-pack/plugins/siem/public/pages/hosts/navigation/authentications_query_tab_body.tsx +++ b/x-pack/plugins/siem/public/hosts/pages/navigation/authentications_query_tab_body.tsx @@ -6,18 +6,18 @@ import { getOr } from 'lodash/fp'; import React, { useEffect } from 'react'; -import { AuthenticationTable } from '../../../components/page/hosts/authentications_table'; -import { manageQuery } from '../../../components/page/manage_query'; -import { AuthenticationsQuery } from '../../../containers/authentications'; +import { AuthenticationTable } from '../../components/authentications_table'; +import { manageQuery } from '../../../common/components/page/manage_query'; +import { AuthenticationsQuery } from '../../containers/authentications'; import { HostsComponentsQueryProps } from './types'; -import { hostsModel } from '../../../store/hosts'; +import { hostsModel } from '../../store'; import { MatrixHistogramOption, MatrixHistogramMappingTypes, MatrixHisrogramConfigs, -} from '../../../components/matrix_histogram/types'; -import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; -import { KpiHostsChartColors } from '../../../components/page/hosts/kpi_hosts/types'; +} from '../../../common/components/matrix_histogram/types'; +import { MatrixHistogramContainer } from '../../../common/components/matrix_histogram'; +import { KpiHostsChartColors } from '../../components/kpi_hosts/types'; import * as i18n from '../translations'; import { HistogramType } from '../../../graphql/types'; diff --git a/x-pack/plugins/siem/public/pages/hosts/navigation/events_query_tab_body.tsx b/x-pack/plugins/siem/public/hosts/pages/navigation/events_query_tab_body.tsx similarity index 85% rename from x-pack/plugins/siem/public/pages/hosts/navigation/events_query_tab_body.tsx rename to x-pack/plugins/siem/public/hosts/pages/navigation/events_query_tab_body.tsx index cb2c19c642bc45..6d2183a3a38d96 100644 --- a/x-pack/plugins/siem/public/pages/hosts/navigation/events_query_tab_body.tsx +++ b/x-pack/plugins/siem/public/hosts/pages/navigation/events_query_tab_body.tsx @@ -5,15 +5,15 @@ */ import React, { useEffect } from 'react'; -import { StatefulEventsViewer } from '../../../components/events_viewer'; +import { StatefulEventsViewer } from '../../../common/components/events_viewer'; import { HostsComponentsQueryProps } from './types'; -import { hostsModel } from '../../../store/hosts'; -import { eventsDefaultModel } from '../../../components/events_viewer/default_model'; +import { hostsModel } from '../../store'; +import { eventsDefaultModel } from '../../../common/components/events_viewer/default_model'; import { MatrixHistogramOption, MatrixHisrogramConfigs, -} from '../../../components/matrix_histogram/types'; -import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; +} from '../../../common/components/matrix_histogram/types'; +import { MatrixHistogramContainer } from '../../../common/components/matrix_histogram'; import * as i18n from '../translations'; import { HistogramType } from '../../../graphql/types'; diff --git a/x-pack/plugins/siem/public/pages/hosts/navigation/hosts_query_tab_body.tsx b/x-pack/plugins/siem/public/hosts/pages/navigation/hosts_query_tab_body.tsx similarity index 88% rename from x-pack/plugins/siem/public/pages/hosts/navigation/hosts_query_tab_body.tsx rename to x-pack/plugins/siem/public/hosts/pages/navigation/hosts_query_tab_body.tsx index 6c301d692d0e17..95be25a6c4fecf 100644 --- a/x-pack/plugins/siem/public/pages/hosts/navigation/hosts_query_tab_body.tsx +++ b/x-pack/plugins/siem/public/hosts/pages/navigation/hosts_query_tab_body.tsx @@ -6,10 +6,10 @@ import { getOr } from 'lodash/fp'; import React from 'react'; -import { HostsQuery } from '../../../containers/hosts'; +import { HostsQuery } from '../../containers/hosts'; import { HostsComponentsQueryProps } from './types'; -import { HostsTable } from '../../../components/page/hosts'; -import { manageQuery } from '../../../components/page/manage_query'; +import { HostsTable } from '../../components/hosts_table'; +import { manageQuery } from '../../../common/components/page/manage_query'; const HostsTableManage = manageQuery(HostsTable); diff --git a/x-pack/plugins/siem/public/pages/hosts/navigation/index.ts b/x-pack/plugins/siem/public/hosts/pages/navigation/index.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/hosts/navigation/index.ts rename to x-pack/plugins/siem/public/hosts/pages/navigation/index.ts diff --git a/x-pack/plugins/siem/public/hosts/pages/navigation/types.ts b/x-pack/plugins/siem/public/hosts/pages/navigation/types.ts new file mode 100644 index 00000000000000..76f56fe1718aa7 --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/pages/navigation/types.ts @@ -0,0 +1,61 @@ +/* + * 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 { ESTermQuery } from '../../../../common/typed_json'; +import { Filter, IIndexPattern } from '../../../../../../../src/plugins/data/public'; +import { NarrowDateRange } from '../../../common/components/ml/types'; +import { InspectQuery, Refetch } from '../../../common/store/inputs/model'; + +import { HostsTableType, HostsType } from '../../store/model'; +import { NavTab } from '../../../common/components/navigation/types'; +import { UpdateDateRange } from '../../../common/components/charts/common'; + +export type KeyHostsNavTabWithoutMlPermission = HostsTableType.hosts & + HostsTableType.authentications & + HostsTableType.uncommonProcesses & + HostsTableType.events; + +type KeyHostsNavTabWithMlPermission = KeyHostsNavTabWithoutMlPermission & HostsTableType.anomalies; + +type KeyHostsNavTab = KeyHostsNavTabWithoutMlPermission | KeyHostsNavTabWithMlPermission; + +export type HostsNavTab = Record; + +export type SetQuery = ({ + id, + inspect, + loading, + refetch, +}: { + id: string; + inspect: InspectQuery | null; + loading: boolean; + refetch: Refetch; +}) => void; + +export interface QueryTabBodyProps { + type: HostsType; + startDate: number; + endDate: number; + filterQuery?: string | ESTermQuery; +} + +export type HostsComponentsQueryProps = QueryTabBodyProps & { + deleteQuery?: ({ id }: { id: string }) => void; + indexPattern: IIndexPattern; + pageFilters?: Filter[]; + skip: boolean; + setQuery: SetQuery; + updateDateRange?: UpdateDateRange; + narrowDateRange?: NarrowDateRange; +}; + +export type AlertsComponentQueryProps = HostsComponentsQueryProps & { + filterQuery: string; + pageFilters?: Filter[]; +}; + +export type CommonChildren = (args: HostsComponentsQueryProps) => JSX.Element; diff --git a/x-pack/plugins/siem/public/pages/hosts/navigation/uncommon_process_query_tab_body.tsx b/x-pack/plugins/siem/public/hosts/pages/navigation/uncommon_process_query_tab_body.tsx similarity index 86% rename from x-pack/plugins/siem/public/pages/hosts/navigation/uncommon_process_query_tab_body.tsx rename to x-pack/plugins/siem/public/hosts/pages/navigation/uncommon_process_query_tab_body.tsx index 141e2e5a63684e..f1691dbaa04b45 100644 --- a/x-pack/plugins/siem/public/pages/hosts/navigation/uncommon_process_query_tab_body.tsx +++ b/x-pack/plugins/siem/public/hosts/pages/navigation/uncommon_process_query_tab_body.tsx @@ -6,10 +6,10 @@ import { getOr } from 'lodash/fp'; import React from 'react'; -import { UncommonProcessesQuery } from '../../../containers/uncommon_processes'; +import { UncommonProcessesQuery } from '../../containers/uncommon_processes'; import { HostsComponentsQueryProps } from './types'; -import { UncommonProcessTable } from '../../../components/page/hosts'; -import { manageQuery } from '../../../components/page/manage_query'; +import { UncommonProcessTable } from '../../components/uncommon_process_table'; +import { manageQuery } from '../../../common/components/page/manage_query'; const UncommonProcessTableManage = manageQuery(UncommonProcessTable); diff --git a/x-pack/plugins/siem/public/pages/hosts/translations.ts b/x-pack/plugins/siem/public/hosts/pages/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/hosts/translations.ts rename to x-pack/plugins/siem/public/hosts/pages/translations.ts diff --git a/x-pack/plugins/siem/public/hosts/pages/types.ts b/x-pack/plugins/siem/public/hosts/pages/types.ts new file mode 100644 index 00000000000000..229349f390ecdb --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/pages/types.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 { IIndexPattern } from 'src/plugins/data/public'; +import { ActionCreator } from 'typescript-fsa'; + +import { SiemPageName } from '../../app/types'; +import { hostsModel } from '../store'; +import { GlobalTimeArgs } from '../../common/containers/global_time'; +import { InputsModelId } from '../../common/store/inputs/constants'; + +export const hostsPagePath = `/:pageName(${SiemPageName.hosts})`; +export const hostDetailsPagePath = `${hostsPagePath}/:detailName`; + +export type HostsTabsProps = HostsComponentProps & { + filterQuery: string; + type: hostsModel.HostsType; + indexPattern: IIndexPattern; + setAbsoluteRangeDatePicker: ActionCreator<{ + id: InputsModelId; + from: number; + to: number; + }>; +}; + +export type HostsQueryProps = GlobalTimeArgs; + +export type HostsComponentProps = HostsQueryProps & { hostsPagePath: string }; diff --git a/x-pack/plugins/siem/public/hosts/routes.tsx b/x-pack/plugins/siem/public/hosts/routes.tsx new file mode 100644 index 00000000000000..93585fa0f83943 --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/routes.tsx @@ -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. + */ + +import React from 'react'; +import { Route } from 'react-router-dom'; + +import { HostsContainer } from './pages'; +import { SiemPageName } from '../app/types'; + +export const getHostsRoutes = () => [ + } + />, +]; diff --git a/x-pack/plugins/siem/public/store/hosts/actions.ts b/x-pack/plugins/siem/public/hosts/store/actions.ts similarity index 100% rename from x-pack/plugins/siem/public/store/hosts/actions.ts rename to x-pack/plugins/siem/public/hosts/store/actions.ts diff --git a/x-pack/plugins/siem/public/hosts/store/helpers.test.ts b/x-pack/plugins/siem/public/hosts/store/helpers.test.ts new file mode 100644 index 00000000000000..4894e6d4c8c573 --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/store/helpers.test.ts @@ -0,0 +1,127 @@ +/* + * 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 { Direction, HostsFields } from '../../graphql/types'; +import { DEFAULT_TABLE_LIMIT } from '../../common/store/constants'; +import { HostsModel, HostsTableType, HostsType } from './model'; +import { setHostsQueriesActivePageToZero } from './helpers'; + +export const mockHostsState: HostsModel = { + page: { + queries: { + [HostsTableType.authentications]: { + activePage: 5, + limit: DEFAULT_TABLE_LIMIT, + }, + [HostsTableType.hosts]: { + activePage: 9, + direction: Direction.desc, + limit: DEFAULT_TABLE_LIMIT, + sortField: HostsFields.lastSeen, + }, + [HostsTableType.events]: { + activePage: 4, + limit: DEFAULT_TABLE_LIMIT, + }, + [HostsTableType.uncommonProcesses]: { + activePage: 8, + limit: DEFAULT_TABLE_LIMIT, + }, + [HostsTableType.anomalies]: null, + [HostsTableType.alerts]: { + activePage: 4, + limit: DEFAULT_TABLE_LIMIT, + }, + }, + }, + details: { + queries: { + [HostsTableType.authentications]: { + activePage: 5, + limit: DEFAULT_TABLE_LIMIT, + }, + [HostsTableType.hosts]: { + activePage: 9, + direction: Direction.desc, + limit: DEFAULT_TABLE_LIMIT, + sortField: HostsFields.lastSeen, + }, + [HostsTableType.events]: { + activePage: 4, + limit: DEFAULT_TABLE_LIMIT, + }, + [HostsTableType.uncommonProcesses]: { + activePage: 8, + limit: DEFAULT_TABLE_LIMIT, + }, + [HostsTableType.anomalies]: null, + [HostsTableType.alerts]: { + activePage: 4, + limit: DEFAULT_TABLE_LIMIT, + }, + }, + }, +}; + +describe('Hosts redux store', () => { + describe('#setHostsQueriesActivePageToZero', () => { + test('set activePage to zero for all queries in hosts page ', () => { + expect(setHostsQueriesActivePageToZero(mockHostsState, HostsType.page)).toEqual({ + allHosts: { + activePage: 0, + direction: 'desc', + limit: 10, + sortField: 'lastSeen', + }, + anomalies: null, + authentications: { + activePage: 0, + limit: 10, + }, + events: { + activePage: 0, + limit: 10, + }, + uncommonProcesses: { + activePage: 0, + limit: 10, + }, + alerts: { + activePage: 0, + limit: 10, + }, + }); + }); + + test('set activePage to zero for all queries in host details ', () => { + expect(setHostsQueriesActivePageToZero(mockHostsState, HostsType.details)).toEqual({ + allHosts: { + activePage: 0, + direction: 'desc', + limit: 10, + sortField: 'lastSeen', + }, + anomalies: null, + authentications: { + activePage: 0, + limit: 10, + }, + events: { + activePage: 0, + limit: 10, + }, + uncommonProcesses: { + activePage: 0, + limit: 10, + }, + alerts: { + activePage: 0, + limit: 10, + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/hosts/store/helpers.ts b/x-pack/plugins/siem/public/hosts/store/helpers.ts new file mode 100644 index 00000000000000..771c3b1061b6ce --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/store/helpers.ts @@ -0,0 +1,66 @@ +/* + * 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_TABLE_ACTIVE_PAGE } from '../../common/store/constants'; + +import { HostsModel, HostsTableType, Queries, HostsType } from './model'; + +export const setHostPageQueriesActivePageToZero = (state: HostsModel): Queries => ({ + ...state.page.queries, + [HostsTableType.authentications]: { + ...state.page.queries[HostsTableType.authentications], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, + [HostsTableType.hosts]: { + ...state.page.queries[HostsTableType.hosts], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, + [HostsTableType.events]: { + ...state.page.queries[HostsTableType.events], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, + [HostsTableType.uncommonProcesses]: { + ...state.page.queries[HostsTableType.uncommonProcesses], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, + [HostsTableType.alerts]: { + ...state.page.queries[HostsTableType.alerts], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, +}); + +export const setHostDetailsQueriesActivePageToZero = (state: HostsModel): Queries => ({ + ...state.details.queries, + [HostsTableType.authentications]: { + ...state.details.queries[HostsTableType.authentications], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, + [HostsTableType.hosts]: { + ...state.details.queries[HostsTableType.hosts], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, + [HostsTableType.events]: { + ...state.details.queries[HostsTableType.events], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, + [HostsTableType.uncommonProcesses]: { + ...state.details.queries[HostsTableType.uncommonProcesses], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, + [HostsTableType.alerts]: { + ...state.page.queries[HostsTableType.alerts], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, +}); + +export const setHostsQueriesActivePageToZero = (state: HostsModel, type: HostsType): Queries => { + if (type === HostsType.page) { + return setHostPageQueriesActivePageToZero(state); + } else if (type === HostsType.details) { + return setHostDetailsQueriesActivePageToZero(state); + } + throw new Error(`HostsType ${type} is unknown`); +}; diff --git a/x-pack/plugins/siem/public/hosts/store/index.ts b/x-pack/plugins/siem/public/hosts/store/index.ts new file mode 100644 index 00000000000000..89ad4a7602fe1c --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/store/index.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 { Reducer, AnyAction } from 'redux'; +import * as hostsActions from './actions'; +import * as hostsModel from './model'; +import * as hostsSelectors from './selectors'; + +export { hostsActions, hostsModel, hostsSelectors }; +export * from './reducer'; + +export interface HostsPluginState { + hosts: hostsModel.HostsModel; +} + +export interface HostsPluginReducer { + hosts: Reducer; +} diff --git a/x-pack/plugins/siem/public/store/hosts/model.ts b/x-pack/plugins/siem/public/hosts/store/model.ts similarity index 100% rename from x-pack/plugins/siem/public/store/hosts/model.ts rename to x-pack/plugins/siem/public/hosts/store/model.ts diff --git a/x-pack/plugins/siem/public/hosts/store/reducer.ts b/x-pack/plugins/siem/public/hosts/store/reducer.ts new file mode 100644 index 00000000000000..59277f64650e68 --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/store/reducer.ts @@ -0,0 +1,143 @@ +/* + * 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 { reducerWithInitialState } from 'typescript-fsa-reducers'; + +import { Direction, HostsFields } from '../../graphql/types'; +import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from '../../common/store/constants'; + +import { + setHostDetailsTablesActivePageToZero, + setHostTablesActivePageToZero, + updateHostsSort, + updateTableActivePage, + updateTableLimit, +} from './actions'; +import { + setHostPageQueriesActivePageToZero, + setHostDetailsQueriesActivePageToZero, +} from './helpers'; +import { HostsModel, HostsTableType } from './model'; + +export type HostsState = HostsModel; + +export const initialHostsState: HostsState = { + page: { + queries: { + [HostsTableType.authentications]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + }, + [HostsTableType.hosts]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + direction: Direction.desc, + limit: DEFAULT_TABLE_LIMIT, + sortField: HostsFields.lastSeen, + }, + [HostsTableType.events]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + }, + [HostsTableType.uncommonProcesses]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + }, + [HostsTableType.anomalies]: null, + [HostsTableType.alerts]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + }, + }, + }, + details: { + queries: { + [HostsTableType.authentications]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + }, + [HostsTableType.hosts]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + direction: Direction.desc, + limit: DEFAULT_TABLE_LIMIT, + sortField: HostsFields.lastSeen, + }, + [HostsTableType.events]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + }, + [HostsTableType.uncommonProcesses]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + }, + [HostsTableType.anomalies]: null, + [HostsTableType.alerts]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + }, + }, + }, +}; + +export const hostsReducer = reducerWithInitialState(initialHostsState) + .case(setHostTablesActivePageToZero, state => ({ + ...state, + page: { + ...state.page, + queries: setHostPageQueriesActivePageToZero(state), + }, + details: { + ...state.details, + queries: setHostDetailsQueriesActivePageToZero(state), + }, + })) + .case(setHostDetailsTablesActivePageToZero, state => ({ + ...state, + details: { + ...state.details, + queries: setHostDetailsQueriesActivePageToZero(state), + }, + })) + .case(updateTableActivePage, (state, { activePage, hostsType, tableType }) => ({ + ...state, + [hostsType]: { + ...state[hostsType], + queries: { + ...state[hostsType].queries, + [tableType]: { + ...state[hostsType].queries[tableType], + activePage, + }, + }, + }, + })) + .case(updateTableLimit, (state, { limit, hostsType, tableType }) => ({ + ...state, + [hostsType]: { + ...state[hostsType], + queries: { + ...state[hostsType].queries, + [tableType]: { + ...state[hostsType].queries[tableType], + limit, + }, + }, + }, + })) + .case(updateHostsSort, (state, { sort, hostsType }) => ({ + ...state, + [hostsType]: { + ...state[hostsType], + queries: { + ...state[hostsType].queries, + [HostsTableType.hosts]: { + ...state[hostsType].queries[HostsTableType.hosts], + direction: sort.direction, + sortField: sort.field, + }, + }, + }, + })) + .build(); diff --git a/x-pack/plugins/siem/public/hosts/store/selectors.ts b/x-pack/plugins/siem/public/hosts/store/selectors.ts new file mode 100644 index 00000000000000..96cae534bb352d --- /dev/null +++ b/x-pack/plugins/siem/public/hosts/store/selectors.ts @@ -0,0 +1,29 @@ +/* + * 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 { get } from 'lodash/fp'; +import { createSelector } from 'reselect'; + +import { State } from '../../common/store/reducer'; + +import { GenericHostsModel, HostsType, HostsTableType } from './model'; + +const selectHosts = (state: State, hostsType: HostsType): GenericHostsModel => + get(hostsType, state.hosts); + +export const authenticationsSelector = () => + createSelector(selectHosts, hosts => hosts.queries.authentications); + +export const hostsSelector = () => + createSelector(selectHosts, hosts => hosts.queries[HostsTableType.hosts]); + +export const eventsSelector = () => createSelector(selectHosts, hosts => hosts.queries.events); + +export const uncommonProcessesSelector = () => + createSelector(selectHosts, hosts => hosts.queries.uncommonProcesses); + +export const alertsSelector = () => + createSelector(selectHosts, hosts => hosts.queries[HostsTableType.alerts]); diff --git a/x-pack/plugins/siem/public/lib/compose/helpers.test.ts b/x-pack/plugins/siem/public/lib/compose/helpers.test.ts deleted file mode 100644 index af4521b4f6e2c6..00000000000000 --- a/x-pack/plugins/siem/public/lib/compose/helpers.test.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. - */ - -import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'; -import { errorLink, reTryOneTimeOnErrorLink } from '../../containers/errors'; -import { getLinks } from './helpers'; -import { withClientState } from 'apollo-link-state'; -import * as apolloLinkHttp from 'apollo-link-http'; -import introspectionQueryResultData from '../../graphql/introspection.json'; - -jest.mock('apollo-cache-inmemory'); -jest.mock('apollo-link-http'); -jest.mock('apollo-link-state'); -jest.mock('../../containers/errors'); -const mockWithClientState = 'mockWithClientState'; -const mockHttpLink = { mockHttpLink: 'mockHttpLink' }; - -// @ts-ignore -withClientState.mockReturnValue(mockWithClientState); -// @ts-ignore -apolloLinkHttp.createHttpLink.mockImplementation(() => mockHttpLink); - -describe('getLinks helper', () => { - test('It should return links in correct order', () => { - const mockCache = new InMemoryCache({ - dataIdFromObject: () => null, - fragmentMatcher: new IntrospectionFragmentMatcher({ - introspectionQueryResultData, - }), - }); - const links = getLinks(mockCache, 'basePath'); - expect(links[0]).toEqual(errorLink); - expect(links[1]).toEqual(reTryOneTimeOnErrorLink); - expect(links[2]).toEqual(mockWithClientState); - expect(links[3]).toEqual(mockHttpLink); - }); -}); diff --git a/x-pack/plugins/siem/public/lib/connectors/components/connector_flyout/index.tsx b/x-pack/plugins/siem/public/lib/connectors/components/connector_flyout/index.tsx deleted file mode 100644 index 10b1e75c6ea845..00000000000000 --- a/x-pack/plugins/siem/public/lib/connectors/components/connector_flyout/index.tsx +++ /dev/null @@ -1,151 +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, { useCallback, useEffect } from 'react'; -import { EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSpacer } from '@elastic/eui'; - -import { isEmpty, get } from 'lodash/fp'; - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ActionConnectorFieldsProps } from '../../../../../../triggers_actions_ui/public/types'; -import { FieldMapping } from '../../../../pages/case/components/configure_cases/field_mapping'; - -import { CasesConfigurationMapping } from '../../../../containers/case/configure/types'; - -import * as i18n from '../../translations'; -import { ActionConnector, ConnectorFlyoutHOCProps } from '../../types'; -import { createDefaultMapping } from '../../utils'; -import { connectorsConfiguration } from '../../config'; - -export const withConnectorFlyout = ({ - ConnectorFormComponent, - connectorActionTypeId, - secretKeys = [], - configKeys = [], -}: ConnectorFlyoutHOCProps) => { - const ConnectorFlyout: React.FC> = ({ - action, - editActionConfig, - editActionSecrets, - errors, - }) => { - /* We do not provide defaults values to the fields (like empty string for apiUrl) intentionally. - * If we do, errors will be shown the first time the flyout is open even though the user did not - * interact with the form. Also, we would like to show errors for empty fields provided by the user. - /*/ - const { apiUrl, casesConfiguration: { mapping = [] } = {} } = action.config; - const configKeysWithDefault = [...configKeys, 'apiUrl']; - - const isApiUrlInvalid: boolean = errors.apiUrl.length > 0 && apiUrl != null; - - /** - * We need to distinguish between the add flyout and the edit flyout. - * useEffect will run only once on component mount. - * This guarantees that the function below will run only once. - * On the first render of the component the apiUrl can be either undefined or filled. - * If it is filled then we are on the edit flyout. Otherwise we are on the add flyout. - */ - - useEffect(() => { - if (!isEmpty(apiUrl)) { - secretKeys.forEach((key: string) => editActionSecrets(key, '')); - } - }, []); - - if (isEmpty(mapping)) { - editActionConfig('casesConfiguration', { - ...action.config.casesConfiguration, - mapping: createDefaultMapping(connectorsConfiguration[connectorActionTypeId].fields), - }); - } - - const handleOnChangeActionConfig = useCallback( - (key: string, value: string) => editActionConfig(key, value), - [] - ); - - const handleOnBlurActionConfig = useCallback( - (key: string) => { - if (configKeysWithDefault.includes(key) && get(key, action.config) == null) { - editActionConfig(key, ''); - } - }, - [action.config] - ); - - const handleOnChangeSecretConfig = useCallback( - (key: string, value: string) => editActionSecrets(key, value), - [] - ); - - const handleOnBlurSecretConfig = useCallback( - (key: string) => { - if (secretKeys.includes(key) && get(key, action.secrets) == null) { - editActionSecrets(key, ''); - } - }, - [action.secrets] - ); - - const handleOnChangeMappingConfig = useCallback( - (newMapping: CasesConfigurationMapping[]) => - editActionConfig('casesConfiguration', { - ...action.config.casesConfiguration, - mapping: newMapping, - }), - [action.config] - ); - - return ( - <> - - - - handleOnChangeActionConfig('apiUrl', evt.target.value)} - onBlur={handleOnBlurActionConfig.bind(null, 'apiUrl')} - /> - - - - - - - - - - - - - ); - }; - - return ConnectorFlyout; -}; diff --git a/x-pack/plugins/siem/public/lib/connectors/jira/index.tsx b/x-pack/plugins/siem/public/lib/connectors/jira/index.tsx deleted file mode 100644 index 049ccb7cf17b7e..00000000000000 --- a/x-pack/plugins/siem/public/lib/connectors/jira/index.tsx +++ /dev/null @@ -1,54 +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 { lazy } from 'react'; -import { - ValidationResult, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../triggers_actions_ui/public/types'; - -import { connector } from './config'; -import { createActionType } from '../utils'; -import logo from './logo.svg'; -import { JiraActionConnector } from './types'; -import * as i18n from './translations'; - -interface Errors { - projectKey: string[]; - email: string[]; - apiToken: string[]; -} - -const validateConnector = (action: JiraActionConnector): ValidationResult => { - const errors: Errors = { - projectKey: [], - email: [], - apiToken: [], - }; - - if (!action.config.projectKey) { - errors.projectKey = [...errors.projectKey, i18n.JIRA_PROJECT_KEY_REQUIRED]; - } - - if (!action.secrets.email) { - errors.email = [...errors.email, i18n.EMAIL_REQUIRED]; - } - - if (!action.secrets.apiToken) { - errors.apiToken = [...errors.apiToken, i18n.API_TOKEN_REQUIRED]; - } - - return { errors }; -}; - -export const getActionType = createActionType({ - id: connector.id, - iconClass: logo, - selectMessage: i18n.JIRA_DESC, - actionTypeTitle: connector.name, - validateConnector, - actionConnectorFields: lazy(() => import('./flyout')), -}); diff --git a/x-pack/plugins/siem/public/lib/connectors/jira/types.ts b/x-pack/plugins/siem/public/lib/connectors/jira/types.ts deleted file mode 100644 index d6b8a6cadcb902..00000000000000 --- a/x-pack/plugins/siem/public/lib/connectors/jira/types.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. - */ - -/* eslint-disable no-restricted-imports */ -/* eslint-disable @kbn/eslint/no-restricted-paths */ - -import { - JiraPublicConfigurationType, - JiraSecretConfigurationType, -} from '../../../../../actions/server/builtin_action_types/jira/types'; - -export { JiraFieldsType } from '../../../../../case/common/api/connectors'; - -export * from '../types'; - -export interface JiraActionConnector { - config: JiraPublicConfigurationType; - secrets: JiraSecretConfigurationType; -} diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow/index.tsx b/x-pack/plugins/siem/public/lib/connectors/servicenow/index.tsx deleted file mode 100644 index 0a239648271d1a..00000000000000 --- a/x-pack/plugins/siem/public/lib/connectors/servicenow/index.tsx +++ /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. - */ - -import { lazy } from 'react'; -import { - ValidationResult, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../triggers_actions_ui/public/types'; -import { connector } from './config'; -import { createActionType } from '../utils'; -import logo from './logo.svg'; -import { ServiceNowActionConnector } from './types'; -import * as i18n from './translations'; - -interface Errors { - username: string[]; - password: string[]; -} - -const validateConnector = (action: ServiceNowActionConnector): ValidationResult => { - const errors: Errors = { - username: [], - password: [], - }; - - if (!action.secrets.username) { - errors.username = [...errors.username, i18n.USERNAME_REQUIRED]; - } - - if (!action.secrets.password) { - errors.password = [...errors.password, i18n.PASSWORD_REQUIRED]; - } - - return { errors }; -}; - -export const getActionType = createActionType({ - id: connector.id, - iconClass: logo, - selectMessage: i18n.SERVICENOW_DESC, - actionTypeTitle: connector.name, - validateConnector, - actionConnectorFields: lazy(() => import('./flyout')), -}); diff --git a/x-pack/plugins/siem/public/lib/connectors/servicenow/types.ts b/x-pack/plugins/siem/public/lib/connectors/servicenow/types.ts deleted file mode 100644 index 43da5624a497b4..00000000000000 --- a/x-pack/plugins/siem/public/lib/connectors/servicenow/types.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. - */ - -/* eslint-disable no-restricted-imports */ -/* eslint-disable @kbn/eslint/no-restricted-paths */ - -import { - ServiceNowPublicConfigurationType, - ServiceNowSecretConfigurationType, -} from '../../../../../actions/server/builtin_action_types/servicenow/types'; - -export { ServiceNowFieldsType } from '../../../../../case/common/api/connectors'; - -export * from '../types'; - -export interface ServiceNowActionConnector { - config: ServiceNowPublicConfigurationType; - secrets: ServiceNowSecretConfigurationType; -} diff --git a/x-pack/plugins/siem/public/lib/connectors/types.ts b/x-pack/plugins/siem/public/lib/connectors/types.ts deleted file mode 100644 index 3d3692c9806e48..00000000000000 --- a/x-pack/plugins/siem/public/lib/connectors/types.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. - */ - -/* eslint-disable no-restricted-imports */ -/* eslint-disable @kbn/eslint/no-restricted-paths */ - -import { ActionType } from '../../../../triggers_actions_ui/public'; -import { IErrorObject } from '../../../../triggers_actions_ui/public/types'; -import { ExternalIncidentServiceConfiguration } from '../../../../actions/server/builtin_action_types/case/types'; - -import { ActionType as ThirdPartySupportedActions, CaseField } from '../../../../case/common/api'; - -export { ThirdPartyField as AllThirdPartyFields } from '../../../../case/common/api'; - -export interface ThirdPartyField { - label: string; - validSourceFields: CaseField[]; - defaultSourceField: CaseField; - defaultActionType: ThirdPartySupportedActions; -} - -export interface ConnectorConfiguration extends ActionType { - logo: string; - fields: Record; -} - -export interface ActionConnector { - config: ExternalIncidentServiceConfiguration; - secrets: {}; -} - -export interface ActionConnectorParams { - message: string; -} - -export interface ActionConnectorValidationErrors { - apiUrl: string[]; -} - -export type Optional = Omit & Partial; - -export interface ConnectorFlyoutFormProps { - errors: IErrorObject; - action: T; - onChangeSecret: (key: string, value: string) => void; - onBlurSecret: (key: string) => void; - onChangeConfig: (key: string, value: string) => void; - onBlurConfig: (key: string) => void; -} - -export interface ConnectorFlyoutHOCProps { - ConnectorFormComponent: React.FC>; - connectorActionTypeId: string; - configKeys?: string[]; - secretKeys?: string[]; -} diff --git a/x-pack/plugins/siem/public/lib/connectors/utils.ts b/x-pack/plugins/siem/public/lib/connectors/utils.ts deleted file mode 100644 index cc1608a05e2ce8..00000000000000 --- a/x-pack/plugins/siem/public/lib/connectors/utils.ts +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - ActionTypeModel, - ValidationResult, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../triggers_actions_ui/public/types'; - -import { - ActionConnector, - ActionConnectorParams, - ActionConnectorValidationErrors, - Optional, - ThirdPartyField, -} from './types'; -import { isUrlInvalid } from './validators'; - -import * as i18n from './translations'; -import { CasesConfigurationMapping } from '../../containers/case/configure/types'; - -export const createActionType = ({ - id, - actionTypeTitle, - selectMessage, - iconClass, - validateConnector, - validateParams = connectorParamsValidator, - actionConnectorFields, - actionParamsFields = null, -}: Optional) => (): ActionTypeModel => { - return { - id, - iconClass, - selectMessage, - actionTypeTitle, - validateConnector: (action: ActionConnector): ValidationResult => { - const errors: ActionConnectorValidationErrors = { - apiUrl: [], - }; - - if (!action.config.apiUrl) { - errors.apiUrl = [...errors.apiUrl, i18n.API_URL_REQUIRED]; - } - - if (isUrlInvalid(action.config.apiUrl)) { - errors.apiUrl = [...errors.apiUrl, i18n.API_URL_INVALID]; - } - - return { errors: { ...errors, ...validateConnector(action).errors } }; - }, - validateParams, - actionConnectorFields, - actionParamsFields, - }; -}; - -const connectorParamsValidator = (actionParams: ActionConnectorParams): ValidationResult => { - return { errors: {} }; -}; - -export const createDefaultMapping = ( - fields: Record -): CasesConfigurationMapping[] => - Object.keys(fields).map( - key => - ({ - source: fields[key].defaultSourceField, - target: key, - actionType: fields[key].defaultActionType, - } as CasesConfigurationMapping) - ); diff --git a/x-pack/plugins/siem/public/lib/keury/index.ts b/x-pack/plugins/siem/public/lib/keury/index.ts deleted file mode 100644 index 810baa89cd60dc..00000000000000 --- a/x-pack/plugins/siem/public/lib/keury/index.ts +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { isEmpty, isString, flow } from 'lodash/fp'; -import { - EsQueryConfig, - Query, - Filter, - esQuery, - esKuery, - IIndexPattern, -} from '../../../../../../src/plugins/data/public'; - -import { JsonObject } from '../../../../../../src/plugins/kibana_utils/public'; - -import { KueryFilterQuery } from '../../store'; - -export const convertKueryToElasticSearchQuery = ( - kueryExpression: string, - indexPattern?: IIndexPattern -) => { - try { - return kueryExpression - ? JSON.stringify( - esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kueryExpression), indexPattern) - ) - : ''; - } catch (err) { - return ''; - } -}; - -export const convertKueryToDslFilter = ( - kueryExpression: string, - indexPattern: IIndexPattern -): JsonObject => { - try { - return kueryExpression - ? esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(kueryExpression), indexPattern) - : {}; - } catch (err) { - return {}; - } -}; - -export const escapeQueryValue = (val: number | string = ''): string | number => { - if (isString(val)) { - if (isEmpty(val)) { - return '""'; - } - return `"${escapeKuery(val)}"`; - } - - return val; -}; - -export const isFromKueryExpressionValid = (kqlFilterQuery: KueryFilterQuery | null): boolean => { - if (kqlFilterQuery && kqlFilterQuery.kind === 'kuery') { - try { - esKuery.fromKueryExpression(kqlFilterQuery.expression); - } catch (err) { - return false; - } - } - return true; -}; - -const escapeWhitespace = (val: string) => - val - .replace(/\t/g, '\\t') - .replace(/\r/g, '\\r') - .replace(/\n/g, '\\n'); - -// See the SpecialCharacter rule in kuery.peg -const escapeSpecialCharacters = (val: string) => val.replace(/["]/g, '\\$&'); // $& means the whole matched string - -// See the Keyword rule in kuery.peg -const escapeAndOr = (val: string) => val.replace(/(\s+)(and|or)(\s+)/gi, '$1\\$2$3'); - -const escapeNot = (val: string) => val.replace(/not(\s+)/gi, '\\$&'); - -export const escapeKuery = flow(escapeSpecialCharacters, escapeAndOr, escapeNot, escapeWhitespace); - -export const convertToBuildEsQuery = ({ - config, - indexPattern, - queries, - filters, -}: { - config: EsQueryConfig; - indexPattern: IIndexPattern; - queries: Query[]; - filters: Filter[]; -}) => { - try { - return JSON.stringify( - esQuery.buildEsQuery( - indexPattern, - queries, - filters.filter(f => f.meta.disabled === false), - { - ...config, - dateFormatTZ: undefined, - } - ) - ); - } catch (exp) { - return ''; - } -}; diff --git a/x-pack/plugins/siem/public/lib/kibana/kibana_react.ts b/x-pack/plugins/siem/public/lib/kibana/kibana_react.ts deleted file mode 100644 index 88be8d25e5840a..00000000000000 --- a/x-pack/plugins/siem/public/lib/kibana/kibana_react.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 { - KibanaContextProvider, - KibanaReactContextValue, - useKibana, - useUiSetting, - useUiSetting$, - withKibana, -} from '../../../../../../src/plugins/kibana_react/public'; -import { StartServices } from '../../plugin'; - -export type KibanaContext = KibanaReactContextValue; -export interface WithKibanaProps { - kibana: KibanaContext; -} - -// eslint-disable-next-line react-hooks/rules-of-hooks -const typedUseKibana = () => useKibana(); - -export { - KibanaContextProvider, - typedUseKibana as useKibana, - useUiSetting, - useUiSetting$, - withKibana, -}; diff --git a/x-pack/plugins/siem/public/lib/telemetry/index.ts b/x-pack/plugins/siem/public/lib/telemetry/index.ts deleted file mode 100644 index 37d181e9b8ad73..00000000000000 --- a/x-pack/plugins/siem/public/lib/telemetry/index.ts +++ /dev/null @@ -1,54 +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 { METRIC_TYPE, UiStatsMetricType } from '@kbn/analytics'; - -import { SetupPlugins } from '../../plugin'; -export { telemetryMiddleware } from './middleware'; - -export { METRIC_TYPE }; - -type TrackFn = (type: UiStatsMetricType, event: string | string[], count?: number) => void; - -const noop = () => {}; - -let _track: TrackFn; - -export const track: TrackFn = (type, event, count) => { - try { - _track(type, event, count); - } catch (error) { - // ignore failed tracking call - } -}; - -export const initTelemetry = (usageCollection: SetupPlugins['usageCollection'], appId: string) => { - _track = usageCollection?.reportUiStats?.bind(null, appId) ?? noop; -}; - -export enum TELEMETRY_EVENT { - // Detections - SIEM_RULE_ENABLED = 'siem_rule_enabled', - SIEM_RULE_DISABLED = 'siem_rule_disabled', - CUSTOM_RULE_ENABLED = 'custom_rule_enabled', - CUSTOM_RULE_DISABLED = 'custom_rule_disabled', - - // ML - SIEM_JOB_ENABLED = 'siem_job_enabled', - SIEM_JOB_DISABLED = 'siem_job_disabled', - CUSTOM_JOB_ENABLED = 'custom_job_enabled', - CUSTOM_JOB_DISABLED = 'custom_job_disabled', - JOB_ENABLE_FAILURE = 'job_enable_failure', - JOB_DISABLE_FAILURE = 'job_disable_failure', - - // Timeline - TIMELINE_OPENED = 'open_timeline', - TIMELINE_SAVED = 'timeline_saved', - TIMELINE_NAMED = 'timeline_named', - - // UI Interactions - TAB_CLICKED = 'tab_', -} diff --git a/x-pack/plugins/siem/public/mock/kibana_core.ts b/x-pack/plugins/siem/public/mock/kibana_core.ts deleted file mode 100644 index b175ddbf5106db..00000000000000 --- a/x-pack/plugins/siem/public/mock/kibana_core.ts +++ /dev/null @@ -1,13 +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 { coreMock } from '../../../../../src/core/public/mocks'; -import { dataPluginMock } from '../../../../../src/plugins/data/public/mocks'; - -export const createKibanaCoreStartMock = () => coreMock.createStart(); -export const createKibanaPluginsStartMock = () => ({ - data: dataPluginMock.createStartContract(), -}); diff --git a/x-pack/plugins/siem/public/mock/kibana_react.ts b/x-pack/plugins/siem/public/mock/kibana_react.ts deleted file mode 100644 index cebba3e237f986..00000000000000 --- a/x-pack/plugins/siem/public/mock/kibana_react.ts +++ /dev/null @@ -1,108 +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. - */ - -/* eslint-disable react/display-name */ - -import React from 'react'; -import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; - -import { - DEFAULT_SIEM_TIME_RANGE, - DEFAULT_SIEM_REFRESH_INTERVAL, - DEFAULT_INDEX_KEY, - DEFAULT_DATE_FORMAT, - DEFAULT_DATE_FORMAT_TZ, - DEFAULT_DARK_MODE, - DEFAULT_TIME_RANGE, - DEFAULT_REFRESH_RATE_INTERVAL, - DEFAULT_FROM, - DEFAULT_TO, - DEFAULT_INTERVAL_PAUSE, - DEFAULT_INTERVAL_VALUE, - DEFAULT_BYTES_FORMAT, - DEFAULT_INDEX_PATTERN, -} from '../../common/constants'; -import { createKibanaCoreStartMock, createKibanaPluginsStartMock } from './kibana_core'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const mockUiSettings: Record = { - [DEFAULT_TIME_RANGE]: { from: 'now-15m', to: 'now', mode: 'quick' }, - [DEFAULT_REFRESH_RATE_INTERVAL]: { pause: false, value: 0 }, - [DEFAULT_SIEM_TIME_RANGE]: { - from: DEFAULT_FROM, - to: DEFAULT_TO, - }, - [DEFAULT_SIEM_REFRESH_INTERVAL]: { - pause: DEFAULT_INTERVAL_PAUSE, - value: DEFAULT_INTERVAL_VALUE, - }, - [DEFAULT_INDEX_KEY]: DEFAULT_INDEX_PATTERN, - [DEFAULT_BYTES_FORMAT]: '0,0.[0]b', - [DEFAULT_DATE_FORMAT_TZ]: 'UTC', - [DEFAULT_DATE_FORMAT]: 'MMM D, YYYY @ HH:mm:ss.SSS', - [DEFAULT_DARK_MODE]: false, -}; - -export const createUseUiSettingMock = () => ( - key: string, - defaultValue?: T -): T => { - const result = mockUiSettings[key]; - - if (typeof result != null) return result; - - if (defaultValue != null) { - return defaultValue; - } - - throw new Error(`Unexpected config key: ${key}`); -}; - -export const createUseUiSetting$Mock = () => { - const useUiSettingMock = createUseUiSettingMock(); - - return ( - key: string, - defaultValue?: T - ): [T, () => void] | undefined => [useUiSettingMock(key, defaultValue), jest.fn()]; -}; - -export const createUseKibanaMock = () => { - const core = createKibanaCoreStartMock(); - const plugins = createKibanaPluginsStartMock(); - const useUiSetting = createUseUiSettingMock(); - - const services = { - ...core, - ...plugins, - uiSettings: { - ...core.uiSettings, - get: useUiSetting, - }, - }; - - return () => ({ services }); -}; - -export const createWithKibanaMock = () => { - const kibana = createUseKibanaMock()(); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (Component: any) => (props: any) => { - return React.createElement(Component, { ...props, kibana }); - }; -}; - -export const createKibanaContextProviderMock = () => { - const kibana = createUseKibanaMock()(); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return ({ services, ...rest }: any) => - React.createElement(KibanaContextProvider, { - ...rest, - services: { ...kibana.services, ...services }, - }); -}; diff --git a/x-pack/plugins/siem/public/mock/utils.ts b/x-pack/plugins/siem/public/mock/utils.ts deleted file mode 100644 index 6a372f163a648f..00000000000000 --- a/x-pack/plugins/siem/public/mock/utils.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. - */ - -interface Global extends NodeJS.Global { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - window?: any; -} - -export const globalNode: Global = global; diff --git a/x-pack/plugins/siem/public/components/arrows/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/network/components/arrows/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/arrows/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/network/components/arrows/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/arrows/helpers.test.ts b/x-pack/plugins/siem/public/network/components/arrows/helpers.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/arrows/helpers.test.ts rename to x-pack/plugins/siem/public/network/components/arrows/helpers.test.ts diff --git a/x-pack/plugins/siem/public/components/arrows/helpers.ts b/x-pack/plugins/siem/public/network/components/arrows/helpers.ts similarity index 100% rename from x-pack/plugins/siem/public/components/arrows/helpers.ts rename to x-pack/plugins/siem/public/network/components/arrows/helpers.ts diff --git a/x-pack/plugins/siem/public/network/components/arrows/index.test.tsx b/x-pack/plugins/siem/public/network/components/arrows/index.test.tsx new file mode 100644 index 00000000000000..e5fa1131c7c47c --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/arrows/index.test.tsx @@ -0,0 +1,42 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; + +import { TestProviders } from '../../../common/mock'; + +import { ArrowBody, ArrowHead } from '.'; + +describe('arrows', () => { + describe('ArrowBody', () => { + test('renders correctly against snapshot', () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('ArrowBody')).toMatchSnapshot(); + }); + }); + + describe('ArrowHead', () => { + test('it renders an arrow head icon', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="arrow-icon"]') + .first() + .exists() + ).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/components/arrows/index.tsx b/x-pack/plugins/siem/public/network/components/arrows/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/arrows/index.tsx rename to x-pack/plugins/siem/public/network/components/arrows/index.tsx diff --git a/x-pack/plugins/siem/public/components/direction/direction.test.tsx b/x-pack/plugins/siem/public/network/components/direction/direction.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/direction/direction.test.tsx rename to x-pack/plugins/siem/public/network/components/direction/direction.test.tsx diff --git a/x-pack/plugins/siem/public/network/components/direction/index.tsx b/x-pack/plugins/siem/public/network/components/direction/index.tsx new file mode 100644 index 00000000000000..c8e8f009339c12 --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/direction/index.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { NetworkDirectionEcs } from '../../../graphql/types'; +import { DraggableBadge } from '../../../common/components/draggables'; +import { NETWORK_DIRECTION_FIELD_NAME } from '../source_destination/field_names'; + +export const INBOUND = 'inbound'; +export const OUTBOUND = 'outbound'; + +export const EXTERNAL = 'external'; +export const INTERNAL = 'internal'; + +export const INCOMING = 'incoming'; +export const OUTGOING = 'outgoing'; + +export const LISTENING = 'listening'; +export const UNKNOWN = 'unknown'; + +export const DEFAULT_ICON = 'questionInCircle'; + +/** Returns an icon representing the value of `network.direction` */ +export const getDirectionIcon = ( + networkDirection?: string | null +): 'arrowUp' | 'arrowDown' | 'globe' | 'bullseye' | 'questionInCircle' => { + if (networkDirection == null) { + return DEFAULT_ICON; + } + + const direction = `${networkDirection}`.toLowerCase(); + + switch (direction) { + case NetworkDirectionEcs.outbound: + case NetworkDirectionEcs.outgoing: + return 'arrowUp'; + case NetworkDirectionEcs.inbound: + case NetworkDirectionEcs.incoming: + case NetworkDirectionEcs.listening: + return 'arrowDown'; + case NetworkDirectionEcs.external: + return 'globe'; + case NetworkDirectionEcs.internal: + return 'bullseye'; + case NetworkDirectionEcs.unknown: + default: + return DEFAULT_ICON; + } +}; + +/** + * Renders a badge containing the value of `network.direction` + */ +export const DirectionBadge = React.memo<{ + contextId: string; + direction?: string | null; + eventId: string; +}>(({ contextId, eventId, direction }) => ( + +)); + +DirectionBadge.displayName = 'DirectionBadge'; diff --git a/x-pack/plugins/siem/public/network/components/embeddables/__mocks__/mock.ts b/x-pack/plugins/siem/public/network/components/embeddables/__mocks__/mock.ts new file mode 100644 index 00000000000000..bc1de567b60ae7 --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/embeddables/__mocks__/mock.ts @@ -0,0 +1,477 @@ +/* + * 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 { IndexPatternMapping } from '../types'; +import { IndexPatternSavedObject } from '../../../../common/hooks/types'; + +export const mockIndexPatternIds: IndexPatternMapping[] = [ + { title: 'filebeat-*', id: '8c7323ac-97ad-4b53-ac0a-40f8f691a918' }, +]; + +export const mockAPMIndexPatternIds: IndexPatternMapping[] = [ + { title: 'apm-*', id: '8c7323ac-97ad-4b53-ac0a-40f8f691a918' }, +]; + +export const mockSourceLayer = { + sourceDescriptor: { + id: 'uuid.v4()', + type: 'ES_SEARCH', + applyGlobalQuery: true, + geoField: 'source.geo.location', + filterByMapBounds: false, + tooltipProperties: [ + 'host.name', + 'source.ip', + 'source.domain', + 'source.geo.country_iso_code', + 'source.as.organization.name', + ], + useTopHits: false, + topHitsTimeField: '@timestamp', + topHitsSize: 1, + indexPatternId: '8c7323ac-97ad-4b53-ac0a-40f8f691a918', + }, + style: { + type: 'VECTOR', + properties: { + fillColor: { + type: 'STATIC', + options: { color: '#6092C0' }, + }, + lineColor: { + type: 'STATIC', + options: { color: '#FFFFFF' }, + }, + lineWidth: { type: 'STATIC', options: { size: 2 } }, + iconSize: { type: 'STATIC', options: { size: 8 } }, + iconOrientation: { + type: 'STATIC', + options: { orientation: 0 }, + }, + symbolizeAs: { + options: { value: 'icon' }, + }, + icon: { + type: 'STATIC', + options: { value: 'home' }, + }, + }, + }, + id: 'uuid.v4()', + label: `filebeat-* | Source Point`, + minZoom: 0, + maxZoom: 24, + alpha: 1, + visible: true, + type: 'VECTOR', + query: { query: '', language: 'kuery' }, + joins: [], +}; + +export const mockDestinationLayer = { + sourceDescriptor: { + id: 'uuid.v4()', + type: 'ES_SEARCH', + applyGlobalQuery: true, + geoField: 'destination.geo.location', + filterByMapBounds: true, + tooltipProperties: [ + 'host.name', + 'destination.ip', + 'destination.domain', + 'destination.geo.country_iso_code', + 'destination.as.organization.name', + ], + useTopHits: false, + topHitsTimeField: '@timestamp', + topHitsSize: 1, + indexPatternId: '8c7323ac-97ad-4b53-ac0a-40f8f691a918', + }, + style: { + type: 'VECTOR', + properties: { + fillColor: { + type: 'STATIC', + options: { color: '#D36086' }, + }, + lineColor: { + type: 'STATIC', + options: { color: '#FFFFFF' }, + }, + lineWidth: { type: 'STATIC', options: { size: 2 } }, + iconSize: { type: 'STATIC', options: { size: 8 } }, + iconOrientation: { + type: 'STATIC', + options: { orientation: 0 }, + }, + symbolizeAs: { + options: { value: 'icon' }, + }, + icon: { + type: 'STATIC', + options: { value: 'marker' }, + }, + }, + }, + id: 'uuid.v4()', + label: `filebeat-* | Destination Point`, + minZoom: 0, + maxZoom: 24, + alpha: 1, + visible: true, + type: 'VECTOR', + query: { query: '', language: 'kuery' }, +}; + +export const mockClientLayer = { + sourceDescriptor: { + id: 'uuid.v4()', + type: 'ES_SEARCH', + applyGlobalQuery: true, + geoField: 'client.geo.location', + filterByMapBounds: false, + tooltipProperties: [ + 'host.name', + 'client.ip', + 'client.domain', + 'client.geo.country_iso_code', + 'client.as.organization.name', + ], + useTopHits: false, + topHitsTimeField: '@timestamp', + topHitsSize: 1, + indexPatternId: '8c7323ac-97ad-4b53-ac0a-40f8f691a918', + }, + style: { + type: 'VECTOR', + properties: { + fillColor: { + type: 'STATIC', + options: { color: '#6092C0' }, + }, + lineColor: { + type: 'STATIC', + options: { color: '#FFFFFF' }, + }, + lineWidth: { type: 'STATIC', options: { size: 2 } }, + iconSize: { type: 'STATIC', options: { size: 8 } }, + iconOrientation: { + type: 'STATIC', + options: { orientation: 0 }, + }, + symbolizeAs: { + options: { value: 'icon' }, + }, + icon: { + type: 'STATIC', + options: { value: 'home' }, + }, + }, + }, + id: 'uuid.v4()', + label: `apm-* | Client Point`, + minZoom: 0, + maxZoom: 24, + alpha: 1, + visible: true, + type: 'VECTOR', + query: { query: '', language: 'kuery' }, + joins: [], +}; + +export const mockServerLayer = { + sourceDescriptor: { + id: 'uuid.v4()', + type: 'ES_SEARCH', + applyGlobalQuery: true, + geoField: 'server.geo.location', + filterByMapBounds: true, + tooltipProperties: [ + 'host.name', + 'server.ip', + 'server.domain', + 'server.geo.country_iso_code', + 'server.as.organization.name', + ], + useTopHits: false, + topHitsTimeField: '@timestamp', + topHitsSize: 1, + indexPatternId: '8c7323ac-97ad-4b53-ac0a-40f8f691a918', + }, + style: { + type: 'VECTOR', + properties: { + fillColor: { + type: 'STATIC', + options: { color: '#D36086' }, + }, + lineColor: { + type: 'STATIC', + options: { color: '#FFFFFF' }, + }, + lineWidth: { type: 'STATIC', options: { size: 2 } }, + iconSize: { type: 'STATIC', options: { size: 8 } }, + iconOrientation: { + type: 'STATIC', + options: { orientation: 0 }, + }, + symbolizeAs: { + options: { value: 'icon' }, + }, + icon: { + type: 'STATIC', + options: { value: 'marker' }, + }, + }, + }, + id: 'uuid.v4()', + label: `apm-* | Server Point`, + minZoom: 0, + maxZoom: 24, + alpha: 1, + visible: true, + type: 'VECTOR', + query: { query: '', language: 'kuery' }, +}; + +export const mockLineLayer = { + sourceDescriptor: { + type: 'ES_PEW_PEW', + applyGlobalQuery: true, + id: 'uuid.v4()', + indexPatternId: '8c7323ac-97ad-4b53-ac0a-40f8f691a918', + sourceGeoField: 'source.geo.location', + destGeoField: 'destination.geo.location', + metrics: [ + { type: 'sum', field: 'source.bytes', label: 'source.bytes' }, + { type: 'sum', field: 'destination.bytes', label: 'destination.bytes' }, + ], + }, + style: { + type: 'VECTOR', + properties: { + fillColor: { + type: 'STATIC', + options: { color: '#1EA593' }, + }, + lineColor: { + type: 'STATIC', + options: { color: '#6092C0' }, + }, + lineWidth: { + type: 'DYNAMIC', + options: { + field: { + label: 'count', + name: 'doc_count', + origin: 'source', + }, + minSize: 1, + maxSize: 8, + fieldMetaOptions: { + isEnabled: true, + sigma: 3, + }, + }, + }, + iconSize: { type: 'STATIC', options: { size: 10 } }, + iconOrientation: { + type: 'STATIC', + options: { orientation: 0 }, + }, + symbolizeAs: { + options: { value: 'icon' }, + }, + icon: { + type: 'STATIC', + options: { value: 'airfield' }, + }, + }, + }, + id: 'uuid.v4()', + label: `filebeat-* | Line`, + minZoom: 0, + maxZoom: 24, + alpha: 0.5, + visible: true, + type: 'VECTOR', + query: { query: '', language: 'kuery' }, +}; + +export const mockClientServerLineLayer = { + sourceDescriptor: { + type: 'ES_PEW_PEW', + applyGlobalQuery: true, + id: 'uuid.v4()', + indexPatternId: '8c7323ac-97ad-4b53-ac0a-40f8f691a918', + sourceGeoField: 'client.geo.location', + destGeoField: 'server.geo.location', + metrics: [ + { type: 'sum', field: 'client.bytes', label: 'client.bytes' }, + { type: 'sum', field: 'server.bytes', label: 'server.bytes' }, + ], + }, + style: { + type: 'VECTOR', + properties: { + fillColor: { + type: 'STATIC', + options: { color: '#1EA593' }, + }, + lineColor: { + type: 'STATIC', + options: { color: '#6092C0' }, + }, + lineWidth: { + type: 'DYNAMIC', + options: { + field: { + label: 'count', + name: 'doc_count', + origin: 'source', + }, + minSize: 1, + maxSize: 8, + fieldMetaOptions: { + isEnabled: true, + sigma: 3, + }, + }, + }, + iconSize: { type: 'STATIC', options: { size: 10 } }, + iconOrientation: { + type: 'STATIC', + options: { orientation: 0 }, + }, + symbolizeAs: { + options: { value: 'icon' }, + }, + icon: { + type: 'STATIC', + options: { value: 'airfield' }, + }, + }, + }, + id: 'uuid.v4()', + label: `apm-* | Line`, + minZoom: 0, + maxZoom: 24, + alpha: 0.5, + visible: true, + type: 'VECTOR', + query: { query: '', language: 'kuery' }, +}; + +export const mockLayerList = [ + { + sourceDescriptor: { type: 'EMS_TMS', isAutoSelect: true }, + id: 'uuid.v4()', + label: null, + minZoom: 0, + maxZoom: 24, + alpha: 1, + visible: true, + style: null, + type: 'VECTOR_TILE', + }, + mockLineLayer, + mockDestinationLayer, + mockSourceLayer, +]; + +export const mockLayerListDouble = [ + { + sourceDescriptor: { type: 'EMS_TMS', isAutoSelect: true }, + id: 'uuid.v4()', + label: null, + minZoom: 0, + maxZoom: 24, + alpha: 1, + visible: true, + style: null, + type: 'VECTOR_TILE', + }, + mockLineLayer, + mockDestinationLayer, + mockSourceLayer, + mockLineLayer, + mockDestinationLayer, + mockSourceLayer, +]; + +export const mockLayerListMixed = [ + { + sourceDescriptor: { type: 'EMS_TMS', isAutoSelect: true }, + id: 'uuid.v4()', + label: null, + minZoom: 0, + maxZoom: 24, + alpha: 1, + visible: true, + style: null, + type: 'VECTOR_TILE', + }, + mockLineLayer, + mockDestinationLayer, + mockSourceLayer, + mockClientServerLineLayer, + mockServerLayer, + mockClientLayer, +]; + +export const mockAPMIndexPattern: IndexPatternSavedObject = { + id: 'apm-*', + type: 'index-pattern', + _version: 'abc', + attributes: { + title: 'apm-*', + }, +}; + +export const mockAPMRegexIndexPattern: IndexPatternSavedObject = { + id: 'apm-7.*', + type: 'index-pattern', + _version: 'abc', + attributes: { + title: 'apm-7.*', + }, +}; + +export const mockFilebeatIndexPattern: IndexPatternSavedObject = { + id: 'filebeat-*', + type: 'index-pattern', + _version: 'abc', + attributes: { + title: 'filebeat-*', + }, +}; + +export const mockAuditbeatIndexPattern: IndexPatternSavedObject = { + id: 'auditbeat-*', + type: 'index-pattern', + _version: 'abc', + attributes: { + title: 'auditbeat-*', + }, +}; + +export const mockAPMTransactionIndexPattern: IndexPatternSavedObject = { + id: 'apm-*-transaction*', + type: 'index-pattern', + _version: 'abc', + attributes: { + title: 'apm-*-transaction*', + }, +}; + +export const mockGlobIndexPattern: IndexPatternSavedObject = { + id: '*', + type: 'index-pattern', + _version: 'abc', + attributes: { + title: '*', + }, +}; diff --git a/x-pack/plugins/siem/public/components/embeddables/__snapshots__/embeddable.test.tsx.snap b/x-pack/plugins/siem/public/network/components/embeddables/__snapshots__/embeddable.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/embeddables/__snapshots__/embeddable.test.tsx.snap rename to x-pack/plugins/siem/public/network/components/embeddables/__snapshots__/embeddable.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/embeddables/__snapshots__/embeddable_header.test.tsx.snap b/x-pack/plugins/siem/public/network/components/embeddables/__snapshots__/embeddable_header.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/embeddables/__snapshots__/embeddable_header.test.tsx.snap rename to x-pack/plugins/siem/public/network/components/embeddables/__snapshots__/embeddable_header.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/embeddables/__snapshots__/embedded_map.test.tsx.snap b/x-pack/plugins/siem/public/network/components/embeddables/__snapshots__/embedded_map.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/embeddables/__snapshots__/embedded_map.test.tsx.snap rename to x-pack/plugins/siem/public/network/components/embeddables/__snapshots__/embedded_map.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap b/x-pack/plugins/siem/public/network/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap rename to x-pack/plugins/siem/public/network/components/embeddables/__snapshots__/index_patterns_missing_prompt.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/embeddables/embeddable.test.tsx b/x-pack/plugins/siem/public/network/components/embeddables/embeddable.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/embeddables/embeddable.test.tsx rename to x-pack/plugins/siem/public/network/components/embeddables/embeddable.test.tsx diff --git a/x-pack/plugins/siem/public/components/embeddables/embeddable.tsx b/x-pack/plugins/siem/public/network/components/embeddables/embeddable.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/embeddables/embeddable.tsx rename to x-pack/plugins/siem/public/network/components/embeddables/embeddable.tsx diff --git a/x-pack/plugins/siem/public/components/embeddables/embeddable_header.test.tsx b/x-pack/plugins/siem/public/network/components/embeddables/embeddable_header.test.tsx similarity index 96% rename from x-pack/plugins/siem/public/components/embeddables/embeddable_header.test.tsx rename to x-pack/plugins/siem/public/network/components/embeddables/embeddable_header.test.tsx index 3b8e137618ab0c..ecbff02353fef5 100644 --- a/x-pack/plugins/siem/public/components/embeddables/embeddable_header.test.tsx +++ b/x-pack/plugins/siem/public/network/components/embeddables/embeddable_header.test.tsx @@ -7,7 +7,7 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; -import { TestProviders } from '../../mock'; +import { TestProviders } from '../../../common/mock'; import { EmbeddableHeader } from './embeddable_header'; describe('EmbeddableHeader', () => { diff --git a/x-pack/plugins/siem/public/components/embeddables/embeddable_header.tsx b/x-pack/plugins/siem/public/network/components/embeddables/embeddable_header.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/embeddables/embeddable_header.tsx rename to x-pack/plugins/siem/public/network/components/embeddables/embeddable_header.tsx diff --git a/x-pack/plugins/siem/public/components/embeddables/embedded_map.test.tsx b/x-pack/plugins/siem/public/network/components/embeddables/embedded_map.test.tsx similarity index 85% rename from x-pack/plugins/siem/public/components/embeddables/embedded_map.test.tsx rename to x-pack/plugins/siem/public/network/components/embeddables/embedded_map.test.tsx index a807b4d6a838bd..33eadad9aa774b 100644 --- a/x-pack/plugins/siem/public/components/embeddables/embedded_map.test.tsx +++ b/x-pack/plugins/siem/public/network/components/embeddables/embedded_map.test.tsx @@ -7,15 +7,15 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { useIndexPatterns } from '../../hooks/use_index_patterns'; +import { useIndexPatterns } from '../../../common/hooks/use_index_patterns'; import { EmbeddedMapComponent } from './embedded_map'; import { SetQuery } from './types'; const mockUseIndexPatterns = useIndexPatterns as jest.Mock; -jest.mock('../../hooks/use_index_patterns'); +jest.mock('../../../common/hooks/use_index_patterns'); mockUseIndexPatterns.mockImplementation(() => [true, []]); -jest.mock('../../lib/kibana'); +jest.mock('../../../common/lib/kibana'); describe('EmbeddedMapComponent', () => { let setQuery: SetQuery; diff --git a/x-pack/plugins/siem/public/components/embeddables/embedded_map.tsx b/x-pack/plugins/siem/public/network/components/embeddables/embedded_map.tsx similarity index 89% rename from x-pack/plugins/siem/public/components/embeddables/embedded_map.tsx rename to x-pack/plugins/siem/public/network/components/embeddables/embedded_map.tsx index d2dd3e54293414..2e9e13839d769a 100644 --- a/x-pack/plugins/siem/public/components/embeddables/embedded_map.tsx +++ b/x-pack/plugins/siem/public/network/components/embeddables/embedded_map.tsx @@ -9,12 +9,15 @@ import React, { useEffect, useState } from 'react'; import { createPortalNode, InPortal } from 'react-reverse-portal'; import styled, { css } from 'styled-components'; -import { EmbeddablePanel, ErrorEmbeddable } from '../../../../../../src/plugins/embeddable/public'; -import { DEFAULT_INDEX_KEY } from '../../../common/constants'; -import { getIndexPatternTitleIdMapping } from '../../hooks/api/helpers'; -import { useIndexPatterns } from '../../hooks/use_index_patterns'; -import { Loader } from '../loader'; -import { displayErrorToast, useStateToaster } from '../toasters'; +import { + EmbeddablePanel, + ErrorEmbeddable, +} from '../../../../../../../src/plugins/embeddable/public'; +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { getIndexPatternTitleIdMapping } from '../../../common/hooks/api/helpers'; +import { useIndexPatterns } from '../../../common/hooks/use_index_patterns'; +import { Loader } from '../../../common/components/loader'; +import { displayErrorToast, useStateToaster } from '../../../common/components/toasters'; import { Embeddable } from './embeddable'; import { EmbeddableHeader } from './embeddable_header'; import { createEmbeddable, findMatchingIndexPatterns } from './embedded_map_helpers'; @@ -22,10 +25,10 @@ import { IndexPatternsMissingPrompt } from './index_patterns_missing_prompt'; import { MapToolTip } from './map_tool_tip/map_tool_tip'; import * as i18n from './translations'; import { SetQuery } from './types'; -import { MapEmbeddable } from '../../../../../legacy/plugins/maps/public'; -import { Query, Filter } from '../../../../../../src/plugins/data/public'; -import { useKibana, useUiSetting$ } from '../../lib/kibana'; -import { getSavedObjectFinder } from '../../../../../../src/plugins/saved_objects/public'; +import { MapEmbeddable } from '../../../../../../legacy/plugins/maps/public'; +import { Query, Filter } from '../../../../../../../src/plugins/data/public'; +import { useKibana, useUiSetting$ } from '../../../common/lib/kibana'; +import { getSavedObjectFinder } from '../../../../../../../src/plugins/saved_objects/public'; interface EmbeddableMapProps { maintainRatio?: boolean; diff --git a/x-pack/plugins/siem/public/components/embeddables/embedded_map_helpers.test.tsx b/x-pack/plugins/siem/public/network/components/embeddables/embedded_map_helpers.test.tsx similarity index 97% rename from x-pack/plugins/siem/public/components/embeddables/embedded_map_helpers.test.tsx rename to x-pack/plugins/siem/public/network/components/embeddables/embedded_map_helpers.test.tsx index aaae43d9684af0..d42ac919e9af07 100644 --- a/x-pack/plugins/siem/public/components/embeddables/embedded_map_helpers.test.tsx +++ b/x-pack/plugins/siem/public/network/components/embeddables/embedded_map_helpers.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { embeddablePluginMock } from '../../../../../../src/plugins/embeddable/public/mocks'; +import { embeddablePluginMock } from '../../../../../../../src/plugins/embeddable/public/mocks'; import { createEmbeddable, findMatchingIndexPatterns } from './embedded_map_helpers'; import { createPortalNode } from 'react-reverse-portal'; import { diff --git a/x-pack/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx b/x-pack/plugins/siem/public/network/components/embeddables/embedded_map_helpers.tsx similarity index 92% rename from x-pack/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx rename to x-pack/plugins/siem/public/network/components/embeddables/embedded_map_helpers.tsx index dd7e1cd6ea9bac..37da8abc029b11 100644 --- a/x-pack/plugins/siem/public/components/embeddables/embedded_map_helpers.tsx +++ b/x-pack/plugins/siem/public/network/components/embeddables/embedded_map_helpers.tsx @@ -10,22 +10,22 @@ import { OutPortal, PortalNode } from 'react-reverse-portal'; import minimatch from 'minimatch'; import { IndexPatternMapping, SetQuery } from './types'; import { getLayerList } from './map_config'; -import { MAP_SAVED_OBJECT_TYPE } from '../../../../maps/public'; +import { MAP_SAVED_OBJECT_TYPE } from '../../../../../maps/public'; import { MapEmbeddable, RenderTooltipContentParams, MapEmbeddableInput, -} from '../../../../../legacy/plugins/maps/public'; +} from '../../../../../../legacy/plugins/maps/public'; import * as i18n from './translations'; -import { Query, Filter } from '../../../../../../src/plugins/data/public'; +import { Query, Filter } from '../../../../../../../src/plugins/data/public'; import { EmbeddableStart, isErrorEmbeddable, EmbeddableOutput, ViewMode, ErrorEmbeddable, -} from '../../../../../../src/plugins/embeddable/public'; -import { IndexPatternSavedObject } from '../../hooks/types'; +} from '../../../../../../../src/plugins/embeddable/public'; +import { IndexPatternSavedObject } from '../../../common/hooks/types'; /** * Creates MapEmbeddable with provided initial configuration diff --git a/x-pack/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.test.tsx b/x-pack/plugins/siem/public/network/components/embeddables/index_patterns_missing_prompt.test.tsx similarity index 93% rename from x-pack/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.test.tsx rename to x-pack/plugins/siem/public/network/components/embeddables/index_patterns_missing_prompt.test.tsx index 4f617644a1fe14..af31cf64df84e6 100644 --- a/x-pack/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.test.tsx +++ b/x-pack/plugins/siem/public/network/components/embeddables/index_patterns_missing_prompt.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { IndexPatternsMissingPromptComponent } from './index_patterns_missing_prompt'; -jest.mock('../../lib/kibana'); +jest.mock('../../../common/lib/kibana'); describe('IndexPatternsMissingPrompt', () => { test('renders correctly against snapshot', () => { diff --git a/x-pack/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.tsx b/x-pack/plugins/siem/public/network/components/embeddables/index_patterns_missing_prompt.tsx similarity index 97% rename from x-pack/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.tsx rename to x-pack/plugins/siem/public/network/components/embeddables/index_patterns_missing_prompt.tsx index abd33505b67b99..aeed6fb2fe20e6 100644 --- a/x-pack/plugins/siem/public/components/embeddables/index_patterns_missing_prompt.tsx +++ b/x-pack/plugins/siem/public/network/components/embeddables/index_patterns_missing_prompt.tsx @@ -8,7 +8,7 @@ import { EuiButton, EuiCode, EuiEmptyPrompt } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { useKibana, useBasePath } from '../../lib/kibana'; +import { useKibana, useBasePath } from '../../../common/lib/kibana'; import * as i18n from './translations'; export const IndexPatternsMissingPromptComponent = () => { diff --git a/x-pack/plugins/siem/public/components/embeddables/map_config.test.ts b/x-pack/plugins/siem/public/network/components/embeddables/map_config.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/embeddables/map_config.test.ts rename to x-pack/plugins/siem/public/network/components/embeddables/map_config.test.ts diff --git a/x-pack/plugins/siem/public/components/embeddables/map_config.ts b/x-pack/plugins/siem/public/network/components/embeddables/map_config.ts similarity index 99% rename from x-pack/plugins/siem/public/components/embeddables/map_config.ts rename to x-pack/plugins/siem/public/network/components/embeddables/map_config.ts index 0d1cd515820c58..88bc6e69949841 100644 --- a/x-pack/plugins/siem/public/components/embeddables/map_config.ts +++ b/x-pack/plugins/siem/public/network/components/embeddables/map_config.ts @@ -13,7 +13,7 @@ import { LayerMappingDetails, } from './types'; import * as i18n from './translations'; -import { SOURCE_TYPES } from '../../../../maps/common/constants'; +import { SOURCE_TYPES } from '../../../../../maps/common/constants'; const euiVisColorPalette = euiPaletteColorBlind(); // Update field mappings to modify what fields will be returned to map tooltip diff --git a/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/line_tool_tip_content.test.tsx.snap b/x-pack/plugins/siem/public/network/components/embeddables/map_tool_tip/__snapshots__/line_tool_tip_content.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/line_tool_tip_content.test.tsx.snap rename to x-pack/plugins/siem/public/network/components/embeddables/map_tool_tip/__snapshots__/line_tool_tip_content.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/map_tool_tip.test.tsx.snap b/x-pack/plugins/siem/public/network/components/embeddables/map_tool_tip/__snapshots__/map_tool_tip.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/map_tool_tip.test.tsx.snap rename to x-pack/plugins/siem/public/network/components/embeddables/map_tool_tip/__snapshots__/map_tool_tip.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/point_tool_tip_content.test.tsx.snap b/x-pack/plugins/siem/public/network/components/embeddables/map_tool_tip/__snapshots__/point_tool_tip_content.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/point_tool_tip_content.test.tsx.snap rename to x-pack/plugins/siem/public/network/components/embeddables/map_tool_tip/__snapshots__/point_tool_tip_content.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/tooltip_footer.test.tsx.snap b/x-pack/plugins/siem/public/network/components/embeddables/map_tool_tip/__snapshots__/tooltip_footer.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/embeddables/map_tool_tip/__snapshots__/tooltip_footer.test.tsx.snap rename to x-pack/plugins/siem/public/network/components/embeddables/map_tool_tip/__snapshots__/tooltip_footer.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.test.tsx b/x-pack/plugins/siem/public/network/components/embeddables/map_tool_tip/line_tool_tip_content.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.test.tsx rename to x-pack/plugins/siem/public/network/components/embeddables/map_tool_tip/line_tool_tip_content.test.tsx diff --git a/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.tsx b/x-pack/plugins/siem/public/network/components/embeddables/map_tool_tip/line_tool_tip_content.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/embeddables/map_tool_tip/line_tool_tip_content.tsx rename to x-pack/plugins/siem/public/network/components/embeddables/map_tool_tip/line_tool_tip_content.tsx diff --git a/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/map_tool_tip.test.tsx b/x-pack/plugins/siem/public/network/components/embeddables/map_tool_tip/map_tool_tip.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/embeddables/map_tool_tip/map_tool_tip.test.tsx rename to x-pack/plugins/siem/public/network/components/embeddables/map_tool_tip/map_tool_tip.test.tsx diff --git a/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/map_tool_tip.tsx b/x-pack/plugins/siem/public/network/components/embeddables/map_tool_tip/map_tool_tip.tsx similarity index 98% rename from x-pack/plugins/siem/public/components/embeddables/map_tool_tip/map_tool_tip.tsx rename to x-pack/plugins/siem/public/network/components/embeddables/map_tool_tip/map_tool_tip.tsx index fc55e3437dc218..0f38c350986b40 100644 --- a/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/map_tool_tip.tsx +++ b/x-pack/plugins/siem/public/network/components/embeddables/map_tool_tip/map_tool_tip.tsx @@ -15,7 +15,7 @@ import { FeatureGeometry, FeatureProperty, MapToolTipProps } from '../types'; import { ToolTipFooter } from './tooltip_footer'; import { LineToolTipContent } from './line_tool_tip_content'; import { PointToolTipContent } from './point_tool_tip_content'; -import { Loader } from '../../loader'; +import { Loader } from '../../../../common/components/loader'; import * as i18n from '../translations'; export const MapToolTipComponent = ({ diff --git a/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx b/x-pack/plugins/siem/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx similarity index 89% rename from x-pack/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx rename to x-pack/plugins/siem/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx index c90af16b0d99a6..d5a7c51ccdeb8d 100644 --- a/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx +++ b/x-pack/plugins/siem/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx @@ -8,11 +8,11 @@ import { shallow } from 'enzyme'; import React from 'react'; import { FeatureProperty } from '../types'; import { getRenderedFieldValue, PointToolTipContentComponent } from './point_tool_tip_content'; -import { TestProviders } from '../../../mock'; -import { getEmptyStringTag } from '../../empty_value'; -import { HostDetailsLink, IPDetailsLink } from '../../links'; -import { useMountAppended } from '../../../utils/use_mount_appended'; -import { FlowTarget } from '../../../graphql/types'; +import { TestProviders } from '../../../../common/mock'; +import { getEmptyStringTag } from '../../../../common/components/empty_value'; +import { HostDetailsLink, IPDetailsLink } from '../../../../common/components/links'; +import { useMountAppended } from '../../../../common/utils/use_mount_appended'; +import { FlowTarget } from '../../../../graphql/types'; describe('PointToolTipContent', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.tsx b/x-pack/plugins/siem/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.tsx similarity index 81% rename from x-pack/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.tsx rename to x-pack/plugins/siem/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.tsx index c635061ca7b7a8..c691407f6166ec 100644 --- a/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/point_tool_tip_content.tsx +++ b/x-pack/plugins/siem/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.tsx @@ -9,13 +9,16 @@ import { sourceDestinationFieldMappings } from '../map_config'; import { AddFilterToGlobalSearchBar, createFilter, -} from '../../page/add_filter_to_global_search_bar'; -import { getEmptyTagValue, getOrEmptyTagFromValue } from '../../empty_value'; -import { DescriptionListStyled } from '../../page'; +} from '../../../../common/components/add_filter_to_global_search_bar'; +import { + getEmptyTagValue, + getOrEmptyTagFromValue, +} from '../../../../common/components/empty_value'; +import { DescriptionListStyled } from '../../../../common/components/page'; import { FeatureProperty } from '../types'; -import { HostDetailsLink, IPDetailsLink } from '../../links'; -import { DefaultFieldRenderer } from '../../field_renderers/field_renderers'; -import { FlowTarget } from '../../../graphql/types'; +import { HostDetailsLink, IPDetailsLink } from '../../../../common/components/links'; +import { DefaultFieldRenderer } from '../../../../timelines/components/field_renderers/field_renderers'; +import { FlowTarget } from '../../../../graphql/types'; interface PointToolTipContentProps { contextId: string; diff --git a/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/tooltip_footer.test.tsx b/x-pack/plugins/siem/public/network/components/embeddables/map_tool_tip/tooltip_footer.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/embeddables/map_tool_tip/tooltip_footer.test.tsx rename to x-pack/plugins/siem/public/network/components/embeddables/map_tool_tip/tooltip_footer.test.tsx diff --git a/x-pack/plugins/siem/public/components/embeddables/map_tool_tip/tooltip_footer.tsx b/x-pack/plugins/siem/public/network/components/embeddables/map_tool_tip/tooltip_footer.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/embeddables/map_tool_tip/tooltip_footer.tsx rename to x-pack/plugins/siem/public/network/components/embeddables/map_tool_tip/tooltip_footer.tsx diff --git a/x-pack/plugins/siem/public/components/embeddables/translations.ts b/x-pack/plugins/siem/public/network/components/embeddables/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/embeddables/translations.ts rename to x-pack/plugins/siem/public/network/components/embeddables/translations.ts diff --git a/x-pack/plugins/siem/public/network/components/embeddables/types.ts b/x-pack/plugins/siem/public/network/components/embeddables/types.ts new file mode 100644 index 00000000000000..e111c2728ba7ea --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/embeddables/types.ts @@ -0,0 +1,58 @@ +/* + * 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 { RenderTooltipContentParams } from '../../../../../../legacy/plugins/maps/public'; +import { inputsModel } from '../../../common/store/inputs'; + +export interface IndexPatternMapping { + title: string; + id: string; +} + +export interface LayerMappingDetails { + metricField: string; + geoField: string; + tooltipProperties: string[]; + label: string; +} + +export interface LayerMapping { + source: LayerMappingDetails; + destination: LayerMappingDetails; +} + +export interface LayerMappingCollection { + [indexPatternTitle: string]: LayerMapping; +} + +export type SetQuery = (params: { + id: string; + inspect: inputsModel.InspectQuery | null; + loading: boolean; + refetch: inputsModel.Refetch; +}) => void; + +export interface MapFeature { + id: number; + layerId: string; +} + +export interface LoadFeatureProps { + layerId: string; + featureId: number; +} + +export interface FeatureProperty { + _propertyKey: string; + _rawValue: string | string[]; +} + +export interface FeatureGeometry { + coordinates: [number]; + type: string; +} + +export type MapToolTipProps = Partial; diff --git a/x-pack/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap b/x-pack/plugins/siem/public/network/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap rename to x-pack/plugins/siem/public/network/components/flow_controls/__snapshots__/flow_direction_select.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/flow_controls/__snapshots__/flow_target_select.test.tsx.snap b/x-pack/plugins/siem/public/network/components/flow_controls/__snapshots__/flow_target_select.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/flow_controls/__snapshots__/flow_target_select.test.tsx.snap rename to x-pack/plugins/siem/public/network/components/flow_controls/__snapshots__/flow_target_select.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/flow_controls/flow_direction_select.test.tsx b/x-pack/plugins/siem/public/network/components/flow_controls/flow_direction_select.test.tsx similarity index 96% rename from x-pack/plugins/siem/public/components/flow_controls/flow_direction_select.test.tsx rename to x-pack/plugins/siem/public/network/components/flow_controls/flow_direction_select.test.tsx index f984b534c188d7..0a35b28db8ce4e 100644 --- a/x-pack/plugins/siem/public/components/flow_controls/flow_direction_select.test.tsx +++ b/x-pack/plugins/siem/public/network/components/flow_controls/flow_direction_select.test.tsx @@ -7,7 +7,7 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; -import { FlowDirection } from '../../graphql/types'; +import { FlowDirection } from '../../../graphql/types'; import { FlowDirectionSelect } from './flow_direction_select'; diff --git a/x-pack/plugins/siem/public/components/flow_controls/flow_direction_select.tsx b/x-pack/plugins/siem/public/network/components/flow_controls/flow_direction_select.tsx similarity index 95% rename from x-pack/plugins/siem/public/components/flow_controls/flow_direction_select.tsx rename to x-pack/plugins/siem/public/network/components/flow_controls/flow_direction_select.tsx index 2b826164063be1..d3698a772300bf 100644 --- a/x-pack/plugins/siem/public/components/flow_controls/flow_direction_select.tsx +++ b/x-pack/plugins/siem/public/network/components/flow_controls/flow_direction_select.tsx @@ -7,7 +7,7 @@ import { EuiFilterButton, EuiFilterGroup } from '@elastic/eui'; import React from 'react'; -import { FlowDirection } from '../../graphql/types'; +import { FlowDirection } from '../../../graphql/types'; import * as i18n from './translations'; diff --git a/x-pack/plugins/siem/public/components/flow_controls/flow_target_select.test.tsx b/x-pack/plugins/siem/public/network/components/flow_controls/flow_target_select.test.tsx similarity index 97% rename from x-pack/plugins/siem/public/components/flow_controls/flow_target_select.test.tsx rename to x-pack/plugins/siem/public/network/components/flow_controls/flow_target_select.test.tsx index 67006d8a7a121a..d033ffc09a82db 100644 --- a/x-pack/plugins/siem/public/components/flow_controls/flow_target_select.test.tsx +++ b/x-pack/plugins/siem/public/network/components/flow_controls/flow_target_select.test.tsx @@ -8,7 +8,7 @@ import { mount, shallow } from 'enzyme'; import { clone } from 'lodash/fp'; import React from 'react'; -import { FlowDirection, FlowTarget } from '../../graphql/types'; +import { FlowDirection, FlowTarget } from '../../../graphql/types'; import { FlowTargetSelect } from './flow_target_select'; diff --git a/x-pack/plugins/siem/public/components/flow_controls/flow_target_select.tsx b/x-pack/plugins/siem/public/network/components/flow_controls/flow_target_select.tsx similarity index 96% rename from x-pack/plugins/siem/public/components/flow_controls/flow_target_select.tsx rename to x-pack/plugins/siem/public/network/components/flow_controls/flow_target_select.tsx index 15d1c663638374..6d6dcfd33b870f 100644 --- a/x-pack/plugins/siem/public/components/flow_controls/flow_target_select.tsx +++ b/x-pack/plugins/siem/public/network/components/flow_controls/flow_target_select.tsx @@ -7,7 +7,7 @@ import { EuiSuperSelect } from '@elastic/eui'; import React from 'react'; -import { FlowDirection, FlowTarget } from '../../graphql/types'; +import { FlowDirection, FlowTarget } from '../../../graphql/types'; import * as i18n from './translations'; diff --git a/x-pack/plugins/siem/public/components/flow_controls/translations.ts b/x-pack/plugins/siem/public/network/components/flow_controls/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/flow_controls/translations.ts rename to x-pack/plugins/siem/public/network/components/flow_controls/translations.ts diff --git a/x-pack/plugins/siem/public/network/components/flow_target_select_connected/index.test.tsx b/x-pack/plugins/siem/public/network/components/flow_target_select_connected/index.test.tsx new file mode 100644 index 00000000000000..edf9e69eeed563 --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/flow_target_select_connected/index.test.tsx @@ -0,0 +1,42 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; + +import { TestProviders } from '../../../common/mock'; +import { FlowTargetSelectConnectedComponent } from './index'; +import { FlowTarget } from '../../../graphql/types'; + +describe('Flow Target Select Connected', () => { + test('renders correctly against snapshot flowTarget source', () => { + const wrapper = mount( + + + + + + ); + expect(wrapper.find('Memo(FlowTargetSelectComponent)').prop('selectedTarget')).toEqual( + FlowTarget.source + ); + }); + + test('renders correctly against snapshot flowTarget destination', () => { + const wrapper = mount( + + + + + + ); + + expect(wrapper.find('Memo(FlowTargetSelectComponent)').prop('selectedTarget')).toEqual( + FlowTarget.destination + ); + }); +}); diff --git a/x-pack/plugins/siem/public/network/components/flow_target_select_connected/index.tsx b/x-pack/plugins/siem/public/network/components/flow_target_select_connected/index.tsx new file mode 100644 index 00000000000000..3ce623cfc97b8f --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/flow_target_select_connected/index.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Location } from 'history'; +import { EuiFlexItem } from '@elastic/eui'; +import React, { useCallback } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; +import styled from 'styled-components'; + +import { FlowDirection, FlowTarget } from '../../../graphql/types'; +import * as i18nIp from '../ip_overview/translations'; + +import { FlowTargetSelect } from '../flow_controls/flow_target_select'; +import { IpOverviewId } from '../../../timelines/components/field_renderers/field_renderers'; + +const SelectTypeItem = styled(EuiFlexItem)` + min-width: 180px; +`; + +SelectTypeItem.displayName = 'SelectTypeItem'; + +interface Props { + flowTarget: FlowTarget; +} + +const getUpdatedFlowTargetPath = ( + location: Location, + currentFlowTarget: FlowTarget, + newFlowTarget: FlowTarget +) => { + const newPathame = location.pathname.replace(currentFlowTarget, newFlowTarget); + + return `${newPathame}${location.search}`; +}; + +export const FlowTargetSelectConnectedComponent: React.FC = ({ flowTarget }) => { + const history = useHistory(); + const location = useLocation(); + + const updateIpDetailsFlowTarget = useCallback( + (newFlowTarget: FlowTarget) => { + const newPath = getUpdatedFlowTargetPath(location, flowTarget, newFlowTarget); + history.push(newPath); + }, + [history, location, flowTarget] + ); + + return ( + + + + ); +}; + +export const FlowTargetSelectConnected = React.memo(FlowTargetSelectConnectedComponent); diff --git a/x-pack/plugins/siem/public/components/ip/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/network/components/ip/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/ip/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/network/components/ip/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/network/components/ip/index.test.tsx b/x-pack/plugins/siem/public/network/components/ip/index.test.tsx new file mode 100644 index 00000000000000..78ba0bb530c9d0 --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/ip/index.test.tsx @@ -0,0 +1,55 @@ +/* + * 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 { TestProviders } from '../../../common/mock/test_providers'; +import { useMountAppended } from '../../../common/utils/use_mount_appended'; + +import { Ip } from '.'; + +describe('Port', () => { + const mount = useMountAppended(); + + test('renders correctly against snapshot', () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); + + test('it renders the the ip address', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="formatted-ip"]') + .first() + .text() + ).toEqual('10.1.2.3'); + }); + + test('it hyperlinks to the network/ip page', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="draggable-content-destination.ip"]') + .find('a') + .first() + .props().href + ).toEqual('#/link-to/network/ip/10.1.2.3/source'); + }); +}); diff --git a/x-pack/plugins/siem/public/network/components/ip/index.tsx b/x-pack/plugins/siem/public/network/components/ip/index.tsx new file mode 100644 index 00000000000000..21e2dd3ebc04dc --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/ip/index.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { FormattedFieldValue } from '../../../timelines/components/timeline/body/renderers/formatted_field'; + +export const SOURCE_IP_FIELD_NAME = 'source.ip'; +export const DESTINATION_IP_FIELD_NAME = 'destination.ip'; + +const IP_FIELD_TYPE = 'ip'; + +/** + * Renders text containing a draggable IP address (e.g. `source.ip`, + * `destination.ip`) that contains a hyperlink + */ +export const Ip = React.memo<{ + contextId: string; + eventId: string; + fieldName: string; + value?: string | null; +}>(({ contextId, eventId, fieldName, value }) => ( + +)); + +Ip.displayName = 'Ip'; diff --git a/x-pack/plugins/siem/public/components/page/network/ip_overview/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/network/components/ip_overview/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/page/network/ip_overview/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/network/components/ip_overview/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/network/components/ip_overview/index.test.tsx b/x-pack/plugins/siem/public/network/components/ip_overview/index.test.tsx new file mode 100644 index 00000000000000..bce811c58e4367 --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/ip_overview/index.test.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { shallow } from 'enzyme'; +import React from 'react'; +import { ActionCreator } from 'typescript-fsa'; + +import { FlowTarget } from '../../../graphql/types'; +import { + apolloClientObservable, + mockGlobalState, + TestProviders, + SUB_PLUGINS_REDUCER, +} from '../../../common/mock'; +import { createStore, State } from '../../../common/store'; +import { networkModel } from '../../store'; + +import { IpOverview } from './index'; +import { mockData } from './mock'; +import { mockAnomalies } from '../../../common/components/ml/mock'; +import { NarrowDateRange } from '../../../common/components/ml/types'; + +describe('IP Overview Component', () => { + const state: State = mockGlobalState; + + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + + beforeEach(() => { + store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + }); + + describe('rendering', () => { + const mockProps = { + anomaliesData: mockAnomalies, + data: mockData.IpOverview, + endDate: new Date('2019-06-18T06:00:00.000Z').valueOf(), + flowTarget: FlowTarget.source, + loading: false, + id: 'ipOverview', + ip: '10.10.10.10', + isLoadingAnomaliesData: false, + narrowDateRange: (jest.fn() as unknown) as NarrowDateRange, + startDate: new Date('2019-06-15T06:00:00.000Z').valueOf(), + type: networkModel.NetworkType.details, + updateFlowTargetAction: (jest.fn() as unknown) as ActionCreator<{ + flowTarget: FlowTarget; + }>, + }; + + test('it renders the default IP Overview', () => { + const wrapper = shallow( + + + + ); + + expect(wrapper.find('IpOverview')).toMatchSnapshot(); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/network/components/ip_overview/index.tsx b/x-pack/plugins/siem/public/network/components/ip_overview/index.tsx new file mode 100644 index 00000000000000..56f6d27dc28ca4 --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/ip_overview/index.tsx @@ -0,0 +1,166 @@ +/* + * 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 { EuiFlexItem } from '@elastic/eui'; +import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; +import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; +import React from 'react'; + +import { DEFAULT_DARK_MODE } from '../../../../common/constants'; +import { DescriptionList } from '../../../../common/utility_types'; +import { useUiSetting$ } from '../../../common/lib/kibana'; +import { FlowTarget, IpOverviewData, Overview } from '../../../graphql/types'; +import { networkModel } from '../../store'; +import { getEmptyTagValue } from '../../../common/components/empty_value'; + +import { + autonomousSystemRenderer, + dateRenderer, + hostIdRenderer, + hostNameRenderer, + locationRenderer, + reputationRenderer, + whoisRenderer, +} from '../../../timelines/components/field_renderers/field_renderers'; +import * as i18n from './translations'; +import { DescriptionListStyled, OverviewWrapper } from '../../../common/components/page'; +import { Loader } from '../../../common/components/loader'; +import { Anomalies, NarrowDateRange } from '../../../common/components/ml/types'; +import { AnomalyScores } from '../../../common/components/ml/score/anomaly_scores'; +import { useMlCapabilities } from '../../../common/components/ml_popover/hooks/use_ml_capabilities'; +import { hasMlUserPermissions } from '../../../../common/machine_learning/has_ml_user_permissions'; +import { InspectButton, InspectButtonContainer } from '../../../common/components/inspect'; + +interface OwnProps { + data: IpOverviewData; + flowTarget: FlowTarget; + id: string; + ip: string; + loading: boolean; + isLoadingAnomaliesData: boolean; + anomaliesData: Anomalies | null; + startDate: number; + endDate: number; + type: networkModel.NetworkType; + narrowDateRange: NarrowDateRange; +} + +export type IpOverviewProps = OwnProps; + +const getDescriptionList = (descriptionList: DescriptionList[], key: number) => { + return ( + + + + ); +}; + +export const IpOverview = React.memo( + ({ + id, + ip, + data, + loading, + flowTarget, + startDate, + endDate, + isLoadingAnomaliesData, + anomaliesData, + narrowDateRange, + }) => { + const capabilities = useMlCapabilities(); + const userPermissions = hasMlUserPermissions(capabilities); + const [darkMode] = useUiSetting$(DEFAULT_DARK_MODE); + const typeData: Overview = data[flowTarget]!; + const column: DescriptionList[] = [ + { + title: i18n.LOCATION, + description: locationRenderer( + [`${flowTarget}.geo.city_name`, `${flowTarget}.geo.region_name`], + data + ), + }, + { + title: i18n.AUTONOMOUS_SYSTEM, + description: typeData + ? autonomousSystemRenderer(typeData.autonomousSystem, flowTarget) + : getEmptyTagValue(), + }, + ]; + + const firstColumn: DescriptionList[] = userPermissions + ? [ + ...column, + { + title: i18n.MAX_ANOMALY_SCORE_BY_JOB, + description: ( + + ), + }, + ] + : column; + + const descriptionLists: Readonly = [ + firstColumn, + [ + { + title: i18n.FIRST_SEEN, + description: typeData ? dateRenderer(typeData.firstSeen) : getEmptyTagValue(), + }, + { + title: i18n.LAST_SEEN, + description: typeData ? dateRenderer(typeData.lastSeen) : getEmptyTagValue(), + }, + ], + [ + { + title: i18n.HOST_ID, + description: typeData + ? hostIdRenderer({ host: data.host, ipFilter: ip }) + : getEmptyTagValue(), + }, + { + title: i18n.HOST_NAME, + description: typeData ? hostNameRenderer(data.host, ip) : getEmptyTagValue(), + }, + ], + [ + { title: i18n.WHOIS, description: whoisRenderer(ip) }, + { title: i18n.REPUTATION, description: reputationRenderer(ip) }, + ], + ]; + + return ( + + + + + {descriptionLists.map((descriptionList, index) => + getDescriptionList(descriptionList, index) + )} + + {loading && ( + + )} + + + ); + } +); + +IpOverview.displayName = 'IpOverview'; diff --git a/x-pack/plugins/siem/public/network/components/ip_overview/mock.ts b/x-pack/plugins/siem/public/network/components/ip_overview/mock.ts new file mode 100644 index 00000000000000..aa86fb177b02ad --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/ip_overview/mock.ts @@ -0,0 +1,59 @@ +/* + * 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 { IpOverviewData } from '../../../graphql/types'; + +export const mockData: Readonly> = { + complete: { + source: { + firstSeen: '2019-02-07T17:19:41.636Z', + lastSeen: '2019-02-07T17:19:41.636Z', + autonomousSystem: { organization: { name: 'Test Org' }, number: 12345 }, + geo: { + continent_name: ['North America'], + city_name: ['New York'], + country_iso_code: ['US'], + country_name: null, + location: { + lat: [40.7214], + lon: [-74.0052], + }, + region_iso_code: ['US-NY'], + region_name: ['New York'], + }, + }, + destination: { + firstSeen: '2019-02-07T17:19:41.648Z', + lastSeen: '2019-02-07T17:19:41.648Z', + autonomousSystem: { organization: { name: 'Test Org' }, number: 12345 }, + geo: { + continent_name: ['North America'], + city_name: ['New York'], + country_iso_code: ['US'], + country_name: null, + location: { + lat: [40.7214], + lon: [-74.0052], + }, + region_iso_code: ['US-NY'], + region_name: ['New York'], + }, + }, + host: { + os: { + kernel: ['4.14.50-v7+'], + name: ['Raspbian GNU/Linux'], + family: [''], + version: ['9 (stretch)'], + platform: ['raspbian'], + }, + name: ['raspberrypi'], + id: ['b19a781f683541a7a25ee345133aa399'], + ip: ['10.10.10.10'], + architecture: ['armv7l'], + }, + }, +}; diff --git a/x-pack/plugins/siem/public/components/page/network/ip_overview/translations.ts b/x-pack/plugins/siem/public/network/components/ip_overview/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/page/network/ip_overview/translations.ts rename to x-pack/plugins/siem/public/network/components/ip_overview/translations.ts diff --git a/x-pack/plugins/siem/public/components/page/network/kpi_network/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/network/components/kpi_network/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/page/network/kpi_network/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/network/components/kpi_network/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/network/components/kpi_network/index.test.tsx b/x-pack/plugins/siem/public/network/components/kpi_network/index.test.tsx new file mode 100644 index 00000000000000..70c952b1107451 --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/kpi_network/index.test.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { shallow } from 'enzyme'; +import React from 'react'; +import { Provider as ReduxStoreProvider } from 'react-redux'; + +import { apolloClientObservable, mockGlobalState, SUB_PLUGINS_REDUCER } from '../../../common/mock'; +import { createStore, State } from '../../../common/store'; +import { KpiNetworkComponent } from '.'; +import { mockData } from './mock'; + +describe('KpiNetwork Component', () => { + const state: State = mockGlobalState; + const from = new Date('2019-06-15T06:00:00.000Z').valueOf(); + const to = new Date('2019-06-18T06:00:00.000Z').valueOf(); + const narrowDateRange = jest.fn(); + + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + + beforeEach(() => { + store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + }); + + describe('rendering', () => { + test('it renders loading icons', () => { + const wrapper = shallow( + + + + ); + + expect(wrapper.find('KpiNetworkComponent')).toMatchSnapshot(); + }); + + test('it renders the default widget', () => { + const wrapper = shallow( + + + + ); + + expect(wrapper.find('KpiNetworkComponent')).toMatchSnapshot(); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/network/components/kpi_network/index.tsx b/x-pack/plugins/siem/public/network/components/kpi_network/index.tsx new file mode 100644 index 00000000000000..ac7381160515d8 --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/kpi_network/index.tsx @@ -0,0 +1,199 @@ +/* + * 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 { + EuiFlexItem, + EuiLoadingSpinner, + EuiFlexGroup, + EuiSpacer, + euiPaletteColorBlind, +} from '@elastic/eui'; +import styled from 'styled-components'; +import { chunk as _chunk } from 'lodash/fp'; + +import { + StatItemsComponent, + StatItemsProps, + useKpiMatrixStatus, + StatItems, +} from '../../../common/components/stat_items'; +import { KpiNetworkData } from '../../../graphql/types'; + +import * as i18n from './translations'; +import { UpdateDateRange } from '../../../common/components/charts/common'; + +const kipsPerRow = 2; +const kpiWidgetHeight = 228; + +const euiVisColorPalette = euiPaletteColorBlind(); +const euiColorVis1 = euiVisColorPalette[1]; +const euiColorVis2 = euiVisColorPalette[2]; +const euiColorVis3 = euiVisColorPalette[3]; + +interface KpiNetworkProps { + data: KpiNetworkData; + from: number; + id: string; + loading: boolean; + to: number; + narrowDateRange: UpdateDateRange; +} + +export const fieldTitleChartMapping: Readonly = [ + { + key: 'UniqueIps', + index: 2, + fields: [ + { + key: 'uniqueSourcePrivateIps', + value: null, + name: i18n.SOURCE_CHART_LABEL, + description: i18n.SOURCE_UNIT_LABEL, + color: euiColorVis2, + icon: 'visMapCoordinate', + }, + { + key: 'uniqueDestinationPrivateIps', + value: null, + name: i18n.DESTINATION_CHART_LABEL, + description: i18n.DESTINATION_UNIT_LABEL, + color: euiColorVis3, + icon: 'visMapCoordinate', + }, + ], + description: i18n.UNIQUE_PRIVATE_IPS, + enableAreaChart: true, + enableBarChart: true, + grow: 2, + }, +]; + +const fieldTitleMatrixMapping: Readonly = [ + { + key: 'networkEvents', + index: 0, + fields: [ + { + key: 'networkEvents', + value: null, + color: euiColorVis1, + }, + ], + description: i18n.NETWORK_EVENTS, + grow: 1, + }, + { + key: 'dnsQueries', + index: 1, + fields: [ + { + key: 'dnsQueries', + value: null, + }, + ], + description: i18n.DNS_QUERIES, + }, + { + key: 'uniqueFlowId', + index: 3, + fields: [ + { + key: 'uniqueFlowId', + value: null, + }, + ], + description: i18n.UNIQUE_FLOW_IDS, + }, + { + key: 'tlsHandshakes', + index: 4, + fields: [ + { + key: 'tlsHandshakes', + value: null, + }, + ], + description: i18n.TLS_HANDSHAKES, + }, +]; + +const FlexGroup = styled(EuiFlexGroup)` + min-height: ${kpiWidgetHeight}px; +`; + +FlexGroup.displayName = 'FlexGroup'; + +export const KpiNetworkBaseComponent = React.memo<{ + fieldsMapping: Readonly; + data: KpiNetworkData; + id: string; + from: number; + to: number; + narrowDateRange: UpdateDateRange; +}>(({ fieldsMapping, data, id, from, to, narrowDateRange }) => { + const statItemsProps: StatItemsProps[] = useKpiMatrixStatus( + fieldsMapping, + data, + id, + from, + to, + narrowDateRange + ); + + return ( + + {statItemsProps.map((mappedStatItemProps, idx) => { + return ; + })} + + ); +}); + +KpiNetworkBaseComponent.displayName = 'KpiNetworkBaseComponent'; + +export const KpiNetworkComponent = React.memo( + ({ data, from, id, loading, to, narrowDateRange }) => { + return loading ? ( + + + + + + ) : ( + + + {_chunk(kipsPerRow, fieldTitleMatrixMapping).map((mappingsPerLine, idx) => ( + + {idx % kipsPerRow === 1 && } + + + ))} + + + + + + ); + } +); + +KpiNetworkComponent.displayName = 'KpiNetworkComponent'; diff --git a/x-pack/plugins/siem/public/network/components/kpi_network/mock.ts b/x-pack/plugins/siem/public/network/components/kpi_network/mock.ts new file mode 100644 index 00000000000000..a8b04ff29f4b67 --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/kpi_network/mock.ts @@ -0,0 +1,230 @@ +/* + * 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 { KpiNetworkData } from '../../../graphql/types'; +import { StatItems } from '../../../common/components/stat_items'; + +export const mockNarrowDateRange = jest.fn(); + +export const mockData: { KpiNetwork: KpiNetworkData } = { + KpiNetwork: { + networkEvents: 16, + uniqueFlowId: 10277307, + uniqueSourcePrivateIps: 383, + uniqueSourcePrivateIpsHistogram: [ + { + x: new Date('2019-02-09T16:00:00.000Z').valueOf(), + y: 8, + }, + { + x: new Date('2019-02-09T19:00:00.000Z').valueOf(), + y: 0, + }, + ], + uniqueDestinationPrivateIps: 18, + uniqueDestinationPrivateIpsHistogram: [ + { + x: new Date('2019-02-09T16:00:00.000Z').valueOf(), + y: 8, + }, + { + x: new Date('2019-02-09T19:00:00.000Z').valueOf(), + y: 0, + }, + ], + dnsQueries: 278, + tlsHandshakes: 10000, + }, +}; + +const mockMappingItems: StatItems = { + key: 'UniqueIps', + index: 0, + fields: [ + { + key: 'uniqueSourcePrivateIps', + value: null, + name: 'Src.', + description: 'source', + color: '#D36086', + icon: 'visMapCoordinate', + }, + { + key: 'uniqueDestinationPrivateIps', + value: null, + name: 'Dest.', + description: 'destination', + color: '#9170B8', + icon: 'visMapCoordinate', + }, + ], + description: 'Unique private IPs', + enableAreaChart: true, + enableBarChart: true, + grow: 2, +}; + +export const mockNoChartMappings: Readonly = [ + { + ...mockMappingItems, + enableAreaChart: false, + enableBarChart: false, + }, +]; + +export const mockDisableChartsInitialData = { + fields: [ + { + key: 'uniqueSourcePrivateIps', + value: undefined, + name: 'Src.', + description: 'source', + color: '#D36086', + icon: 'visMapCoordinate', + }, + { + key: 'uniqueDestinationPrivateIps', + value: undefined, + name: 'Dest.', + description: 'destination', + color: '#9170B8', + icon: 'visMapCoordinate', + }, + ], + description: 'Unique private IPs', + enableAreaChart: false, + enableBarChart: false, + grow: 2, + areaChart: undefined, + barChart: undefined, +}; + +export const mockEnableChartsInitialData = { + fields: [ + { + key: 'uniqueSourcePrivateIps', + value: undefined, + name: 'Src.', + description: 'source', + color: '#D36086', + icon: 'visMapCoordinate', + }, + { + key: 'uniqueDestinationPrivateIps', + value: undefined, + name: 'Dest.', + description: 'destination', + color: '#9170B8', + icon: 'visMapCoordinate', + }, + ], + description: 'Unique private IPs', + enableAreaChart: true, + enableBarChart: true, + grow: 2, + areaChart: [], + barChart: [ + { + color: '#D36086', + key: 'uniqueSourcePrivateIps', + value: [ + { + g: 'uniqueSourcePrivateIps', + x: 'Src.', + y: null, + }, + ], + }, + { + color: '#9170B8', + key: 'uniqueDestinationPrivateIps', + value: [ + { + g: 'uniqueDestinationPrivateIps', + x: 'Dest.', + y: null, + }, + ], + }, + ], +}; + +export const mockEnableChartsData = { + areaChart: [ + { + key: 'uniqueSourcePrivateIpsHistogram', + value: [ + { x: new Date('2019-02-09T16:00:00.000Z').valueOf(), y: 8 }, + { + x: new Date('2019-02-09T19:00:00.000Z').valueOf(), + y: 0, + }, + ], + name: 'Src.', + description: 'source', + color: '#D36086', + icon: 'visMapCoordinate', + }, + { + key: 'uniqueDestinationPrivateIpsHistogram', + value: [ + { x: new Date('2019-02-09T16:00:00.000Z').valueOf(), y: 8 }, + { x: new Date('2019-02-09T19:00:00.000Z').valueOf(), y: 0 }, + ], + name: 'Dest.', + description: 'destination', + color: '#9170B8', + icon: 'visMapCoordinate', + }, + ], + barChart: [ + { + key: 'uniqueSourcePrivateIps', + color: '#D36086', + value: [ + { + x: 'Src.', + y: 383, + g: 'uniqueSourcePrivateIps', + y0: 0, + }, + ], + }, + { + key: 'uniqueDestinationPrivateIps', + color: '#9170B8', + value: [{ x: 'Dest.', y: 18, g: 'uniqueDestinationPrivateIps', y0: 0 }], + }, + ], + description: 'Unique private IPs', + enableAreaChart: true, + enableBarChart: true, + fields: [ + { + key: 'uniqueSourcePrivateIps', + value: 383, + name: 'Src.', + description: 'source', + color: '#D36086', + icon: 'visMapCoordinate', + }, + { + key: 'uniqueDestinationPrivateIps', + value: 18, + name: 'Dest.', + description: 'destination', + color: '#9170B8', + icon: 'visMapCoordinate', + }, + ], + from: 1560578400000, + grow: 2, + id: 'statItem', + index: 2, + statKey: 'UniqueIps', + to: 1560837600000, + narrowDateRange: mockNarrowDateRange, +}; diff --git a/x-pack/plugins/siem/public/components/page/network/kpi_network/translations.ts b/x-pack/plugins/siem/public/network/components/kpi_network/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/page/network/kpi_network/translations.ts rename to x-pack/plugins/siem/public/network/components/kpi_network/translations.ts diff --git a/x-pack/plugins/siem/public/components/page/network/network_dns_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/network/components/network_dns_table/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/page/network/network_dns_table/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/network/components/network_dns_table/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/page/network/network_dns_table/__snapshots__/is_ptr_included.test.tsx.snap b/x-pack/plugins/siem/public/network/components/network_dns_table/__snapshots__/is_ptr_included.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/page/network/network_dns_table/__snapshots__/is_ptr_included.test.tsx.snap rename to x-pack/plugins/siem/public/network/components/network_dns_table/__snapshots__/is_ptr_included.test.tsx.snap diff --git a/x-pack/plugins/siem/public/network/components/network_dns_table/columns.tsx b/x-pack/plugins/siem/public/network/components/network_dns_table/columns.tsx new file mode 100644 index 00000000000000..dbc09daaf0abc6 --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/network_dns_table/columns.tsx @@ -0,0 +1,133 @@ +/* + * 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 numeral from '@elastic/numeral'; +import React from 'react'; + +import { NetworkDnsFields, NetworkDnsItem } from '../../../graphql/types'; +import { + DragEffects, + DraggableWrapper, +} from '../../../common/components/drag_and_drop/draggable_wrapper'; +import { escapeDataProviderId } from '../../../common/components/drag_and_drop/helpers'; +import { defaultToEmptyTag, getEmptyTagValue } from '../../../common/components/empty_value'; +import { Columns } from '../../../common/components/paginated_table'; +import { IS_OPERATOR } from '../../../timelines/components/timeline/data_providers/data_provider'; +import { PreferenceFormattedBytes } from '../../../common/components/formatted_bytes'; +import { Provider } from '../../../timelines/components/timeline/data_providers/provider'; + +import * as i18n from './translations'; +export type NetworkDnsColumns = [ + Columns, + Columns, + Columns, + Columns, + Columns +]; + +export const getNetworkDnsColumns = (): NetworkDnsColumns => [ + { + field: `node.${NetworkDnsFields.dnsName}`, + name: i18n.REGISTERED_DOMAIN, + truncateText: false, + hideForMobile: false, + sortable: true, + render: dnsName => { + if (dnsName != null) { + const id = escapeDataProviderId(`networkDns-table--name-${dnsName}`); + return ( + + snapshot.isDragging ? ( + + + + ) : ( + defaultToEmptyTag(dnsName) + ) + } + /> + ); + } else { + return getEmptyTagValue(); + } + }, + }, + { + align: 'right', + field: `node.${NetworkDnsFields.queryCount}`, + name: i18n.TOTAL_QUERIES, + sortable: true, + truncateText: false, + hideForMobile: false, + render: queryCount => { + if (queryCount != null) { + return numeral(queryCount).format('0'); + } else { + return getEmptyTagValue(); + } + }, + }, + { + align: 'right', + field: `node.${NetworkDnsFields.uniqueDomains}`, + name: i18n.UNIQUE_DOMAINS, + sortable: true, + truncateText: false, + hideForMobile: false, + render: uniqueDomains => { + if (uniqueDomains != null) { + return numeral(uniqueDomains).format('0'); + } else { + return getEmptyTagValue(); + } + }, + }, + { + align: 'right', + field: `node.${NetworkDnsFields.dnsBytesIn}`, + name: i18n.DNS_BYTES_IN, + sortable: true, + truncateText: false, + hideForMobile: false, + render: dnsBytesIn => { + if (dnsBytesIn != null) { + return ; + } else { + return getEmptyTagValue(); + } + }, + }, + { + align: 'right', + field: `node.${NetworkDnsFields.dnsBytesOut}`, + name: i18n.DNS_BYTES_OUT, + sortable: true, + truncateText: false, + hideForMobile: false, + render: dnsBytesOut => { + if (dnsBytesOut != null) { + return ; + } else { + return getEmptyTagValue(); + } + }, + }, +]; diff --git a/x-pack/plugins/siem/public/network/components/network_dns_table/index.test.tsx b/x-pack/plugins/siem/public/network/components/network_dns_table/index.test.tsx new file mode 100644 index 00000000000000..f2d8ce6cb6c448 --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/network_dns_table/index.test.tsx @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { shallow } from 'enzyme'; +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { MockedProvider } from 'react-apollo/test-utils'; +import { Provider as ReduxStoreProvider } from 'react-redux'; + +import { + apolloClientObservable, + mockGlobalState, + TestProviders, + SUB_PLUGINS_REDUCER, +} from '../../../common/mock'; +import { State, createStore } from '../../../common/store'; +import { networkModel } from '../../store'; +import { useMountAppended } from '../../../common/utils/use_mount_appended'; + +import { NetworkDnsTable } from '.'; +import { mockData } from './mock'; + +describe('NetworkTopNFlow Table Component', () => { + const loadPage = jest.fn(); + const state: State = mockGlobalState; + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + const mount = useMountAppended(); + + beforeEach(() => { + store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + }); + + describe('rendering', () => { + test('it renders the default NetworkTopNFlow table', () => { + const wrapper = shallow( + + + + ); + + expect(wrapper.find('Connect(NetworkDnsTableComponent)')).toMatchSnapshot(); + }); + }); + + describe('Sorting', () => { + test('when you click on the column header, you should show the sorting icon', () => { + const wrapper = mount( + + + + + + ); + + expect(store.getState().network.page.queries!.dns.sort).toEqual({ + direction: 'desc', + field: 'queryCount', + }); + + wrapper + .find('.euiTable thead tr th button') + .first() + .simulate('click'); + + wrapper.update(); + + expect(store.getState().network.page.queries!.dns.sort).toEqual({ + direction: 'asc', + field: 'dnsName', + }); + expect( + wrapper + .find('.euiTable thead tr th button') + .first() + .find('svg') + ).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/network/components/network_dns_table/index.tsx b/x-pack/plugins/siem/public/network/components/network_dns_table/index.tsx new file mode 100644 index 00000000000000..fc763298f08f47 --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/network_dns_table/index.tsx @@ -0,0 +1,169 @@ +/* + * 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, { useCallback, useMemo } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; + +import { networkActions, networkModel, networkSelectors } from '../../store'; +import { + Direction, + NetworkDnsEdges, + NetworkDnsFields, + NetworkDnsSortField, +} from '../../../graphql/types'; +import { State } from '../../../common/store'; +import { Criteria, ItemsPerRow, PaginatedTable } from '../../../common/components/paginated_table'; + +import { getNetworkDnsColumns } from './columns'; +import { IsPtrIncluded } from './is_ptr_included'; +import * as i18n from './translations'; + +const tableType = networkModel.NetworkTableType.dns; + +interface OwnProps { + data: NetworkDnsEdges[]; + fakeTotalCount: number; + id: string; + isInspect: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + showMorePagesIndicator: boolean; + totalCount: number; + type: networkModel.NetworkType; +} + +type NetworkDnsTableProps = OwnProps & PropsFromRedux; + +const rowItems: ItemsPerRow[] = [ + { + text: i18n.ROWS_5, + numberOfRow: 5, + }, + { + text: i18n.ROWS_10, + numberOfRow: 10, + }, +]; + +export const NetworkDnsTableComponent = React.memo( + ({ + activePage, + data, + fakeTotalCount, + id, + isInspect, + isPtrIncluded, + limit, + loading, + loadPage, + showMorePagesIndicator, + sort, + totalCount, + type, + updateNetworkTable, + }) => { + const updateLimitPagination = useCallback( + newLimit => + updateNetworkTable({ + networkType: type, + tableType, + updates: { limit: newLimit }, + }), + [type, updateNetworkTable] + ); + + const updateActivePage = useCallback( + newPage => + updateNetworkTable({ + networkType: type, + tableType, + updates: { activePage: newPage }, + }), + [type, updateNetworkTable] + ); + + const onChange = useCallback( + (criteria: Criteria) => { + if (criteria.sort != null) { + const newDnsSortField: NetworkDnsSortField = { + field: criteria.sort.field.split('.')[1] as NetworkDnsFields, + direction: criteria.sort.direction as Direction, + }; + if (!deepEqual(newDnsSortField, sort)) { + updateNetworkTable({ + networkType: type, + tableType, + updates: { sort: newDnsSortField }, + }); + } + } + }, + [sort, type, updateNetworkTable] + ); + + const onChangePtrIncluded = useCallback( + () => + updateNetworkTable({ + networkType: type, + tableType, + updates: { isPtrIncluded: !isPtrIncluded }, + }), + [type, updateNetworkTable, isPtrIncluded] + ); + + const columns = useMemo(() => getNetworkDnsColumns(), []); + + return ( + + } + headerTitle={i18n.TOP_DNS_DOMAINS} + headerTooltip={i18n.TOOLTIP} + headerUnit={i18n.UNIT(totalCount)} + id={id} + itemsPerRow={rowItems} + isInspect={isInspect} + limit={limit} + loading={loading} + loadPage={loadPage} + onChange={onChange} + pageOfItems={data} + showMorePagesIndicator={showMorePagesIndicator} + sorting={{ + field: `node.${sort.field}`, + direction: sort.direction, + }} + totalCount={fakeTotalCount} + updateActivePage={updateActivePage} + updateLimitPagination={updateLimitPagination} + /> + ); + } +); + +NetworkDnsTableComponent.displayName = 'NetworkDnsTableComponent'; + +const makeMapStateToProps = () => { + const getNetworkDnsSelector = networkSelectors.dnsSelector(); + const mapStateToProps = (state: State) => getNetworkDnsSelector(state); + return mapStateToProps; +}; + +const mapDispatchToProps = { + updateNetworkTable: networkActions.updateNetworkTable, +}; + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const NetworkDnsTable = connector(NetworkDnsTableComponent); diff --git a/x-pack/plugins/siem/public/components/page/network/network_dns_table/is_ptr_included.test.tsx b/x-pack/plugins/siem/public/network/components/network_dns_table/is_ptr_included.test.tsx similarity index 95% rename from x-pack/plugins/siem/public/components/page/network/network_dns_table/is_ptr_included.test.tsx rename to x-pack/plugins/siem/public/network/components/network_dns_table/is_ptr_included.test.tsx index 31a1b1667087a7..36dca6981a7ae9 100644 --- a/x-pack/plugins/siem/public/components/page/network/network_dns_table/is_ptr_included.test.tsx +++ b/x-pack/plugins/siem/public/network/components/network_dns_table/is_ptr_included.test.tsx @@ -7,7 +7,7 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; -import { FlowDirection } from '../../../../graphql/types'; +import { FlowDirection } from '../../../graphql/types'; import { IsPtrIncluded } from './is_ptr_included'; diff --git a/x-pack/plugins/siem/public/components/page/network/network_dns_table/is_ptr_included.tsx b/x-pack/plugins/siem/public/network/components/network_dns_table/is_ptr_included.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/page/network/network_dns_table/is_ptr_included.tsx rename to x-pack/plugins/siem/public/network/components/network_dns_table/is_ptr_included.tsx diff --git a/x-pack/plugins/siem/public/network/components/network_dns_table/mock.ts b/x-pack/plugins/siem/public/network/components/network_dns_table/mock.ts new file mode 100644 index 00000000000000..d094256fa40269 --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/network_dns_table/mock.ts @@ -0,0 +1,182 @@ +/* + * 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 { NetworkDnsData } from '../../../graphql/types'; + +export const mockData: { NetworkDns: NetworkDnsData } = { + NetworkDns: { + totalCount: 80, + edges: [ + { + node: { + _id: 'nflxvideo.net', + dnsBytesIn: 2964, + dnsBytesOut: 12546, + dnsName: 'nflxvideo.net', + queryCount: 52, + uniqueDomains: 21, + }, + cursor: { value: 'nflxvideo.net' }, + }, + { + node: { + _id: 'apple.com', + dnsBytesIn: 2680, + dnsBytesOut: 31687, + dnsName: 'apple.com', + queryCount: 75, + uniqueDomains: 20, + }, + cursor: { value: 'apple.com' }, + }, + { + node: { + _id: 'googlevideo.com', + dnsBytesIn: 1890, + dnsBytesOut: 16292, + dnsName: 'googlevideo.com', + queryCount: 38, + uniqueDomains: 19, + }, + cursor: { value: 'googlevideo.com' }, + }, + { + node: { + _id: 'netflix.com', + dnsBytesIn: 60525, + dnsBytesOut: 218193, + dnsName: 'netflix.com', + queryCount: 1532, + uniqueDomains: 12, + }, + cursor: { value: 'netflix.com' }, + }, + { + node: { + _id: 'samsungcloudsolution.com', + dnsBytesIn: 1480, + dnsBytesOut: 11702, + dnsName: 'samsungcloudsolution.com', + queryCount: 31, + uniqueDomains: 8, + }, + cursor: { value: 'samsungcloudsolution.com' }, + }, + { + node: { + _id: 'doubleclick.net', + dnsBytesIn: 1505, + dnsBytesOut: 14372, + dnsName: 'doubleclick.net', + queryCount: 35, + uniqueDomains: 7, + }, + cursor: { value: 'doubleclick.net' }, + }, + { + node: { + _id: 'digitalocean.com', + dnsBytesIn: 2035, + dnsBytesOut: 4111, + dnsName: 'digitalocean.com', + queryCount: 35, + uniqueDomains: 6, + }, + cursor: { value: 'digitalocean.com' }, + }, + { + node: { + _id: 'samsungelectronics.com', + dnsBytesIn: 3916, + dnsBytesOut: 36592, + dnsName: 'samsungelectronics.com', + queryCount: 89, + uniqueDomains: 6, + }, + cursor: { value: 'samsungelectronics.com' }, + }, + { + node: { + _id: 'google.com', + dnsBytesIn: 896, + dnsBytesOut: 8072, + dnsName: 'google.com', + queryCount: 23, + uniqueDomains: 5, + }, + cursor: { value: 'google.com' }, + }, + { + node: { + _id: 'samsungcloudsolution.net', + dnsBytesIn: 1490, + dnsBytesOut: 11518, + dnsName: 'samsungcloudsolution.net', + queryCount: 30, + uniqueDomains: 5, + }, + cursor: { value: 'samsungcloudsolution.net' }, + }, + ], + pageInfo: { + activePage: 1, + fakeTotalCount: 50, + showMorePagesIndicator: true, + }, + histogram: [ + { + x: 'nflxvideo.net', + g: 'nflxvideo.net', + y: 12546, + }, + { + x: 'apple.com', + g: 'apple.com', + y: 31687, + }, + { + x: 'googlevideo.com', + g: 'googlevideo.com', + y: 16292, + }, + { + x: 'netflix.com', + g: 'netflix.com', + y: 218193, + }, + { + x: 'samsungcloudsolution.com', + g: 'samsungcloudsolution.com', + y: 11702, + }, + { + x: 'doubleclick.net', + g: 'doubleclick.net', + y: 14372, + }, + { + x: 'digitalocean.com', + g: 'digitalocean.com', + y: 4111, + }, + { + x: 'samsungelectronics.com', + g: 'samsungelectronics.com', + y: 36592, + }, + { + x: 'google.com', + g: 'google.com', + y: 8072, + }, + { + x: 'samsungcloudsolution.net', + g: 'samsungcloudsolution.net', + y: 11518, + }, + ], + }, +}; diff --git a/x-pack/plugins/siem/public/components/page/network/network_dns_table/translations.ts b/x-pack/plugins/siem/public/network/components/network_dns_table/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/page/network/network_dns_table/translations.ts rename to x-pack/plugins/siem/public/network/components/network_dns_table/translations.ts diff --git a/x-pack/plugins/siem/public/components/page/network/network_http_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/network/components/network_http_table/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/page/network/network_http_table/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/network/components/network_http_table/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/network/components/network_http_table/columns.tsx b/x-pack/plugins/siem/public/network/components/network_http_table/columns.tsx new file mode 100644 index 00000000000000..4642fdd2f2c93f --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/network_http_table/columns.tsx @@ -0,0 +1,118 @@ +/* + * 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. + */ + +/* eslint-disable react/display-name */ + +import React from 'react'; +import numeral from '@elastic/numeral'; +import { NetworkHttpEdges, NetworkHttpFields, NetworkHttpItem } from '../../../graphql/types'; +import { escapeDataProviderId } from '../../../common/components/drag_and_drop/helpers'; +import { getEmptyTagValue } from '../../../common/components/empty_value'; +import { IPDetailsLink } from '../../../common/components/links'; +import { Columns } from '../../../common/components/paginated_table'; + +import * as i18n from './translations'; +import { + getRowItemDraggable, + getRowItemDraggables, +} from '../../../common/components/tables/helpers'; +export type NetworkHttpColumns = [ + Columns, + Columns, + Columns, + Columns, + Columns, + Columns, + Columns +]; + +export const getNetworkHttpColumns = (tableId: string): NetworkHttpColumns => [ + { + name: i18n.METHOD, + render: ({ node: { methods, path } }) => { + return Array.isArray(methods) && methods.length > 0 + ? getRowItemDraggables({ + attrName: 'http.request.method', + displayCount: 3, + idPrefix: escapeDataProviderId(`${tableId}-table-methods-${path}`), + rowItems: methods, + }) + : getEmptyTagValue(); + }, + }, + { + name: i18n.DOMAIN, + render: ({ node: { domains, path } }) => + Array.isArray(domains) && domains.length > 0 + ? getRowItemDraggables({ + attrName: 'url.domain', + displayCount: 3, + idPrefix: escapeDataProviderId(`${tableId}-table-domains-${path}`), + rowItems: domains, + }) + : getEmptyTagValue(), + }, + { + field: `node.${NetworkHttpFields.path}`, + name: i18n.PATH, + render: path => + path != null + ? getRowItemDraggable({ + attrName: 'url.path', + idPrefix: escapeDataProviderId(`${tableId}-table-path-${path}`), + rowItem: path, + }) + : getEmptyTagValue(), + }, + { + name: i18n.STATUS, + render: ({ node: { statuses, path } }) => + Array.isArray(statuses) && statuses.length > 0 + ? getRowItemDraggables({ + attrName: 'http.response.status_code', + displayCount: 3, + idPrefix: escapeDataProviderId(`${tableId}-table-statuses-${path}`), + rowItems: statuses, + }) + : getEmptyTagValue(), + }, + { + name: i18n.LAST_HOST, + render: ({ node: { lastHost, path } }) => + lastHost != null + ? getRowItemDraggable({ + attrName: 'host.name', + idPrefix: escapeDataProviderId(`${tableId}-table-lastHost-${path}`), + rowItem: lastHost, + }) + : getEmptyTagValue(), + }, + { + name: i18n.LAST_SOURCE_IP, + render: ({ node: { lastSourceIp, path } }) => + lastSourceIp != null + ? getRowItemDraggable({ + attrName: 'source.ip', + idPrefix: escapeDataProviderId(`${tableId}-table-lastSourceIp-${path}`), + rowItem: lastSourceIp, + render: () => , + }) + : getEmptyTagValue(), + }, + { + align: 'right', + field: `node.${NetworkHttpFields.requestCount}`, + name: i18n.REQUESTS, + sortable: true, + render: requestCount => { + if (requestCount != null) { + return numeral(requestCount).format('0,000'); + } else { + return getEmptyTagValue(); + } + }, + }, +]; diff --git a/x-pack/plugins/siem/public/network/components/network_http_table/index.test.tsx b/x-pack/plugins/siem/public/network/components/network_http_table/index.test.tsx new file mode 100644 index 00000000000000..3c4e1559e9f7b2 --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/network_http_table/index.test.tsx @@ -0,0 +1,109 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React from 'react'; +import { MockedProvider } from 'react-apollo/test-utils'; +import { Provider as ReduxStoreProvider } from 'react-redux'; + +import { + apolloClientObservable, + mockGlobalState, + TestProviders, + SUB_PLUGINS_REDUCER, +} from '../../../common/mock'; +import { useMountAppended } from '../../../common/utils/use_mount_appended'; +import { createStore, State } from '../../../common/store'; +import { networkModel } from '../../store'; + +import { NetworkHttpTable } from '.'; +import { mockData } from './mock'; + +describe('NetworkHttp Table Component', () => { + const loadPage = jest.fn(); + const state: State = mockGlobalState; + + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + const mount = useMountAppended(); + + beforeEach(() => { + store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + }); + + describe('rendering', () => { + test('it renders the default NetworkHttp table', () => { + const wrapper = shallow( + + + + ); + + expect(wrapper.find('Connect(Component)')).toMatchSnapshot(); + }); + }); + + describe('Sorting', () => { + test('when you click on the column header, you should show the sorting icon', () => { + const wrapper = mount( + + + + + + ); + + expect(store.getState().network.page.queries!.http.sort).toEqual({ + direction: 'desc', + }); + + wrapper + .find('.euiTable thead tr th button') + .first() + .simulate('click'); + + wrapper.update(); + + expect(store.getState().network.page.queries!.http.sort).toEqual({ + direction: 'asc', + }); + expect( + wrapper + .find('.euiTable thead tr th button') + .first() + .find('svg') + ).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/network/components/network_http_table/index.tsx b/x-pack/plugins/siem/public/network/components/network_http_table/index.tsx new file mode 100644 index 00000000000000..cab7106584e0f3 --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/network_http_table/index.tsx @@ -0,0 +1,145 @@ +/* + * 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, { useCallback, useMemo } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; + +import { networkActions, networkModel, networkSelectors } from '../../store'; +import { Direction, NetworkHttpEdges, NetworkHttpFields } from '../../../graphql/types'; +import { State } from '../../../common/store'; +import { Criteria, ItemsPerRow, PaginatedTable } from '../../../common/components/paginated_table'; + +import { getNetworkHttpColumns } from './columns'; +import * as i18n from './translations'; + +interface OwnProps { + data: NetworkHttpEdges[]; + fakeTotalCount: number; + id: string; + isInspect: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + showMorePagesIndicator: boolean; + totalCount: number; + type: networkModel.NetworkType; +} + +type NetworkHttpTableProps = OwnProps & PropsFromRedux; + +const rowItems: ItemsPerRow[] = [ + { + text: i18n.ROWS_5, + numberOfRow: 5, + }, + { + text: i18n.ROWS_10, + numberOfRow: 10, + }, +]; + +const NetworkHttpTableComponent: React.FC = ({ + activePage, + data, + fakeTotalCount, + id, + isInspect, + limit, + loading, + loadPage, + showMorePagesIndicator, + sort, + totalCount, + type, + updateNetworkTable, +}) => { + const tableType = + type === networkModel.NetworkType.page + ? networkModel.NetworkTableType.http + : networkModel.IpDetailsTableType.http; + + const updateLimitPagination = useCallback( + newLimit => + updateNetworkTable({ + networkType: type, + tableType, + updates: { limit: newLimit }, + }), + [type, updateNetworkTable, tableType] + ); + + const updateActivePage = useCallback( + newPage => + updateNetworkTable({ + networkType: type, + tableType, + updates: { activePage: newPage }, + }), + [type, updateNetworkTable, tableType] + ); + + const onChange = useCallback( + (criteria: Criteria) => { + if (criteria.sort != null && criteria.sort.direction !== sort.direction) { + updateNetworkTable({ + networkType: type, + tableType, + updates: { + sort: { + direction: criteria.sort.direction as Direction, + }, + }, + }); + } + }, + [tableType, sort.direction, type, updateNetworkTable] + ); + + const sorting = { field: `node.${NetworkHttpFields.requestCount}`, direction: sort.direction }; + + const columns = useMemo(() => getNetworkHttpColumns(tableType), [tableType]); + + return ( + + ); +}; + +NetworkHttpTableComponent.displayName = 'NetworkHttpTableComponent'; + +const makeMapStateToProps = () => { + const getNetworkHttpSelector = networkSelectors.httpSelector(); + const mapStateToProps = (state: State, { type }: OwnProps) => getNetworkHttpSelector(state, type); + return mapStateToProps; +}; + +const mapDispatchToProps = { + updateNetworkTable: networkActions.updateNetworkTable, +}; + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const NetworkHttpTable = connector(React.memo(NetworkHttpTableComponent)); diff --git a/x-pack/plugins/siem/public/network/components/network_http_table/mock.ts b/x-pack/plugins/siem/public/network/components/network_http_table/mock.ts new file mode 100644 index 00000000000000..f82f911d601ffd --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/network_http_table/mock.ts @@ -0,0 +1,88 @@ +/* + * 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 { NetworkHttpData } from '../../../graphql/types'; + +export const mockData: { NetworkHttp: NetworkHttpData } = { + NetworkHttp: { + edges: [ + { + node: { + _id: '/computeMetadata/v1/instance/virtual-clock/drift-token', + domains: ['metadata.google.internal'], + methods: ['get'], + statuses: [], + lastHost: 'suricata-iowa', + lastSourceIp: '10.128.0.21', + path: '/computeMetadata/v1/instance/virtual-clock/drift-token', + requestCount: 1440, + }, + cursor: { + value: '/computeMetadata/v1/instance/virtual-clock/drift-token', + tiebreaker: null, + }, + }, + { + node: { + _id: '/computeMetadata/v1/', + domains: ['metadata.google.internal'], + methods: ['get'], + statuses: ['200'], + lastHost: 'suricata-iowa', + lastSourceIp: '10.128.0.21', + path: '/computeMetadata/v1/', + requestCount: 1020, + }, + cursor: { + value: '/computeMetadata/v1/', + tiebreaker: null, + }, + }, + { + node: { + _id: '/computeMetadata/v1/instance/network-interfaces/', + domains: ['metadata.google.internal'], + methods: ['get'], + statuses: [], + lastHost: 'suricata-iowa', + lastSourceIp: '10.128.0.21', + path: '/computeMetadata/v1/instance/network-interfaces/', + requestCount: 960, + }, + cursor: { + value: '/computeMetadata/v1/instance/network-interfaces/', + tiebreaker: null, + }, + }, + { + node: { + _id: '/downloads/ca_setup.exe', + domains: ['www.oxid.it'], + methods: ['get'], + statuses: ['200'], + lastHost: 'jessie', + lastSourceIp: '10.0.2.15', + path: '/downloads/ca_setup.exe', + requestCount: 3, + }, + cursor: { + value: '/downloads/ca_setup.exe', + tiebreaker: null, + }, + }, + ], + inspect: { + dsl: [''], + response: [''], + }, + pageInfo: { + activePage: 0, + fakeTotalCount: 4, + showMorePagesIndicator: false, + }, + totalCount: 4, + }, +}; diff --git a/x-pack/plugins/siem/public/components/page/network/network_http_table/translations.ts b/x-pack/plugins/siem/public/network/components/network_http_table/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/page/network/network_http_table/translations.ts rename to x-pack/plugins/siem/public/network/components/network_http_table/translations.ts diff --git a/x-pack/plugins/siem/public/components/page/network/network_top_countries_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/network/components/network_top_countries_table/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/page/network/network_top_countries_table/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/network/components/network_top_countries_table/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/network/components/network_top_countries_table/columns.tsx b/x-pack/plugins/siem/public/network/components/network_top_countries_table/columns.tsx new file mode 100644 index 00000000000000..60d691f48deb8c --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/network_top_countries_table/columns.tsx @@ -0,0 +1,182 @@ +/* + * 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 { get } from 'lodash/fp'; +import numeral from '@elastic/numeral'; +import React from 'react'; +import { IIndexPattern } from 'src/plugins/data/public'; + +import { CountryFlagAndName } from '../source_destination/country_flag'; +import { + FlowTargetSourceDest, + NetworkTopCountriesEdges, + TopNetworkTablesEcsField, +} from '../../../graphql/types'; +import { networkModel } from '../../store'; +import { + DragEffects, + DraggableWrapper, +} from '../../../common/components/drag_and_drop/draggable_wrapper'; +import { escapeDataProviderId } from '../../../common/components/drag_and_drop/helpers'; +import { getEmptyTagValue } from '../../../common/components/empty_value'; +import { Columns } from '../../../common/components/paginated_table'; +import { IS_OPERATOR } from '../../../timelines/components/timeline/data_providers/data_provider'; +import { Provider } from '../../../timelines/components/timeline/data_providers/provider'; +import * as i18n from './translations'; +import { PreferenceFormattedBytes } from '../../../common/components/formatted_bytes'; + +export type NetworkTopCountriesColumns = [ + Columns, + Columns, + Columns, + Columns, + Columns, + Columns +]; + +export type NetworkTopCountriesColumnsIpDetails = [ + Columns, + Columns, + Columns, + Columns, + Columns +]; + +export const getNetworkTopCountriesColumns = ( + indexPattern: IIndexPattern, + flowTarget: FlowTargetSourceDest, + type: networkModel.NetworkType, + tableId: string +): NetworkTopCountriesColumns => [ + { + name: i18n.COUNTRY, + render: ({ node }) => { + const geo = get(`${flowTarget}.country`, node); + const geoAttr = `${flowTarget}.geo.country_iso_code`; + const id = escapeDataProviderId(`${tableId}-table-${flowTarget}-country-${geo}`); + if (geo != null) { + return ( + + snapshot.isDragging ? ( + + + + ) : ( + <> + + + ) + } + /> + ); + } else { + return getEmptyTagValue(); + } + }, + width: '20%', + }, + { + align: 'right', + field: 'node.network.bytes_in', + name: i18n.BYTES_IN, + sortable: true, + render: bytes => { + if (bytes != null) { + return ; + } else { + return getEmptyTagValue(); + } + }, + }, + { + align: 'right', + field: 'node.network.bytes_out', + name: i18n.BYTES_OUT, + sortable: true, + render: bytes => { + if (bytes != null) { + return ; + } else { + return getEmptyTagValue(); + } + }, + }, + { + align: 'right', + field: `node.${flowTarget}.flows`, + name: i18n.FLOWS, + sortable: true, + render: flows => { + if (flows != null) { + return numeral(flows).format('0,000'); + } else { + return getEmptyTagValue(); + } + }, + }, + { + align: 'right', + field: `node.${flowTarget}.${flowTarget}_ips`, + name: flowTarget === FlowTargetSourceDest.source ? i18n.SOURCE_IPS : i18n.DESTINATION_IPS, + sortable: true, + render: ips => { + if (ips != null) { + return numeral(ips).format('0,000'); + } else { + return getEmptyTagValue(); + } + }, + }, + { + align: 'right', + field: `node.${flowTarget}.${getOppositeField(flowTarget)}_ips`, + name: + getOppositeField(flowTarget) === FlowTargetSourceDest.source + ? i18n.SOURCE_IPS + : i18n.DESTINATION_IPS, + sortable: true, + render: ips => { + if (ips != null) { + return numeral(ips).format('0,000'); + } else { + return getEmptyTagValue(); + } + }, + }, +]; + +export const getCountriesColumnsCurated = ( + indexPattern: IIndexPattern, + flowTarget: FlowTargetSourceDest, + type: networkModel.NetworkType, + tableId: string +): NetworkTopCountriesColumns | NetworkTopCountriesColumnsIpDetails => { + const columns = getNetworkTopCountriesColumns(indexPattern, flowTarget, type, tableId); + + // Columns to exclude from host details pages + if (type === networkModel.NetworkType.details) { + columns.pop(); + return columns; + } + + return columns; +}; + +const getOppositeField = (flowTarget: FlowTargetSourceDest): FlowTargetSourceDest => + flowTarget === FlowTargetSourceDest.source + ? FlowTargetSourceDest.destination + : FlowTargetSourceDest.source; diff --git a/x-pack/plugins/siem/public/network/components/network_top_countries_table/index.test.tsx b/x-pack/plugins/siem/public/network/components/network_top_countries_table/index.test.tsx new file mode 100644 index 00000000000000..a449ed8dfa9ced --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/network_top_countries_table/index.test.tsx @@ -0,0 +1,153 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React from 'react'; +import { MockedProvider } from 'react-apollo/test-utils'; +import { Provider as ReduxStoreProvider } from 'react-redux'; + +import { FlowTargetSourceDest } from '../../../graphql/types'; +import { + apolloClientObservable, + mockGlobalState, + mockIndexPattern, + TestProviders, + SUB_PLUGINS_REDUCER, +} from '../../../common/mock'; +import { useMountAppended } from '../../../common/utils/use_mount_appended'; +import { createStore, State } from '../../../common/store'; +import { networkModel } from '../../store'; + +import { NetworkTopCountriesTable } from '.'; +import { mockData } from './mock'; + +describe('NetworkTopCountries Table Component', () => { + const loadPage = jest.fn(); + const state: State = mockGlobalState; + const mount = useMountAppended(); + + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + + beforeEach(() => { + store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + }); + + describe('rendering', () => { + test('it renders the default NetworkTopCountries table', () => { + const wrapper = shallow( + + + + ); + + expect(wrapper.find('Connect(NetworkTopCountriesTableComponent)')).toMatchSnapshot(); + }); + test('it renders the IP Details NetworkTopCountries table', () => { + const wrapper = shallow( + + + + ); + + expect(wrapper.find('Connect(NetworkTopCountriesTableComponent)')).toMatchSnapshot(); + }); + }); + + describe('Sorting on Table', () => { + test('when you click on the column header, you should show the sorting icon', () => { + const wrapper = mount( + + + + + + ); + expect(store.getState().network.page.queries.topCountriesSource.sort).toEqual({ + direction: 'desc', + field: 'bytes_out', + }); + + wrapper + .find('.euiTable thead tr th button') + .at(1) + .simulate('click'); + + wrapper.update(); + + expect(store.getState().network.page.queries.topCountriesSource.sort).toEqual({ + direction: 'asc', + field: 'bytes_out', + }); + expect( + wrapper + .find('.euiTable thead tr th button') + .first() + .text() + ).toEqual('Bytes inClick to sort in ascending order'); + expect( + wrapper + .find('.euiTable thead tr th button') + .at(1) + .text() + ).toEqual('Bytes outClick to sort in descending order'); + expect( + wrapper + .find('.euiTable thead tr th button') + .at(1) + .find('svg') + ).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/network/components/network_top_countries_table/index.tsx b/x-pack/plugins/siem/public/network/components/network_top_countries_table/index.tsx new file mode 100644 index 00000000000000..c2a280d30d1063 --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/network_top_countries_table/index.tsx @@ -0,0 +1,192 @@ +/* + * 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 { last } from 'lodash/fp'; +import React, { useCallback, useMemo } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; +import { IIndexPattern } from 'src/plugins/data/public'; + +import { networkActions, networkModel, networkSelectors } from '../../store'; +import { + Direction, + FlowTargetSourceDest, + NetworkTopCountriesEdges, + NetworkTopTablesFields, + NetworkTopTablesSortField, +} from '../../../graphql/types'; +import { State } from '../../../common/store'; + +import { Criteria, ItemsPerRow, PaginatedTable } from '../../../common/components/paginated_table'; + +import { getCountriesColumnsCurated } from './columns'; +import * as i18n from './translations'; + +interface OwnProps { + data: NetworkTopCountriesEdges[]; + fakeTotalCount: number; + flowTargeted: FlowTargetSourceDest; + id: string; + indexPattern: IIndexPattern; + isInspect: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + showMorePagesIndicator: boolean; + totalCount: number; + type: networkModel.NetworkType; +} + +type NetworkTopCountriesTableProps = OwnProps & PropsFromRedux; + +const rowItems: ItemsPerRow[] = [ + { + text: i18n.ROWS_5, + numberOfRow: 5, + }, + { + text: i18n.ROWS_10, + numberOfRow: 10, + }, +]; + +export const NetworkTopCountriesTableId = 'networkTopCountries-top-talkers'; + +const NetworkTopCountriesTableComponent = React.memo( + ({ + activePage, + data, + fakeTotalCount, + flowTargeted, + id, + indexPattern, + isInspect, + limit, + loading, + loadPage, + showMorePagesIndicator, + sort, + totalCount, + type, + updateNetworkTable, + }) => { + let tableType: networkModel.TopCountriesTableType; + const headerTitle: string = + flowTargeted === FlowTargetSourceDest.source + ? i18n.SOURCE_COUNTRIES + : i18n.DESTINATION_COUNTRIES; + + if (type === networkModel.NetworkType.page) { + tableType = + flowTargeted === FlowTargetSourceDest.source + ? networkModel.NetworkTableType.topCountriesSource + : networkModel.NetworkTableType.topCountriesDestination; + } else { + tableType = + flowTargeted === FlowTargetSourceDest.source + ? networkModel.IpDetailsTableType.topCountriesSource + : networkModel.IpDetailsTableType.topCountriesDestination; + } + + const field = + sort.field === NetworkTopTablesFields.bytes_out || + sort.field === NetworkTopTablesFields.bytes_in + ? `node.network.${sort.field}` + : `node.${flowTargeted}.${sort.field}`; + + const updateLimitPagination = useCallback( + newLimit => + updateNetworkTable({ + networkType: type, + tableType, + updates: { limit: newLimit }, + }), + [type, updateNetworkTable, tableType] + ); + + const updateActivePage = useCallback( + newPage => + updateNetworkTable({ + networkType: type, + tableType, + updates: { activePage: newPage }, + }), + [type, updateNetworkTable, tableType] + ); + + const onChange = useCallback( + (criteria: Criteria) => { + if (criteria.sort != null) { + const splitField = criteria.sort.field.split('.'); + const lastField = last(splitField); + const newSortDirection = + lastField !== sort.field ? Direction.desc : criteria.sort.direction; // sort by desc on init click + const newTopCountriesSort: NetworkTopTablesSortField = { + field: lastField as NetworkTopTablesFields, + direction: newSortDirection as Direction, + }; + if (!deepEqual(newTopCountriesSort, sort)) { + updateNetworkTable({ + networkType: type, + tableType, + updates: { + sort: newTopCountriesSort, + }, + }); + } + } + }, + [type, sort, tableType, updateNetworkTable] + ); + + const columns = useMemo( + () => + getCountriesColumnsCurated(indexPattern, flowTargeted, type, NetworkTopCountriesTableId), + [indexPattern, flowTargeted, type] + ); + + return ( + + ); + } +); + +NetworkTopCountriesTableComponent.displayName = 'NetworkTopCountriesTableComponent'; + +const makeMapStateToProps = () => { + const getTopCountriesSelector = networkSelectors.topCountriesSelector(); + return (state: State, { type, flowTargeted }: OwnProps) => + getTopCountriesSelector(state, type, flowTargeted); +}; + +const mapDispatchToProps = { + updateNetworkTable: networkActions.updateNetworkTable, +}; + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const NetworkTopCountriesTable = connector(NetworkTopCountriesTableComponent); diff --git a/x-pack/plugins/siem/public/network/components/network_top_countries_table/mock.ts b/x-pack/plugins/siem/public/network/components/network_top_countries_table/mock.ts new file mode 100644 index 00000000000000..cee775c93d66fd --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/network_top_countries_table/mock.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { NetworkTopCountriesData } from '../../../graphql/types'; + +export const mockData: { NetworkTopCountries: NetworkTopCountriesData } = { + NetworkTopCountries: { + totalCount: 524, + edges: [ + { + node: { + source: { + country: 'DE', + destination_ips: 12, + flows: 12345, + source_ips: 55, + }, + destination: null, + network: { + bytes_in: 3826633497, + bytes_out: 1083495734, + }, + }, + cursor: { + value: '8.8.8.8', + }, + }, + { + node: { + source: { + flows: 12345, + destination_ips: 12, + source_ips: 55, + country: 'US', + }, + destination: null, + network: { + bytes_in: 3826633497, + bytes_out: 1083495734, + }, + }, + cursor: { + value: '9.9.9.9', + }, + }, + ], + pageInfo: { + activePage: 1, + fakeTotalCount: 50, + showMorePagesIndicator: true, + }, + }, +}; diff --git a/x-pack/plugins/siem/public/components/page/network/network_top_countries_table/translations.ts b/x-pack/plugins/siem/public/network/components/network_top_countries_table/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/page/network/network_top_countries_table/translations.ts rename to x-pack/plugins/siem/public/network/components/network_top_countries_table/translations.ts diff --git a/x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/network/components/network_top_n_flow_table/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/network/components/network_top_n_flow_table/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/network/components/network_top_n_flow_table/columns.tsx b/x-pack/plugins/siem/public/network/components/network_top_n_flow_table/columns.tsx new file mode 100644 index 00000000000000..64626c450b9eca --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/network_top_n_flow_table/columns.tsx @@ -0,0 +1,257 @@ +/* + * 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 { get } from 'lodash/fp'; +import numeral from '@elastic/numeral'; +import React from 'react'; + +import { CountryFlag } from '../source_destination/country_flag'; +import { + AutonomousSystemItem, + FlowTargetSourceDest, + NetworkTopNFlowEdges, + TopNetworkTablesEcsField, +} from '../../../graphql/types'; +import { networkModel } from '../../store'; +import { + DragEffects, + DraggableWrapper, +} from '../../../common/components/drag_and_drop/draggable_wrapper'; +import { escapeDataProviderId } from '../../../common/components/drag_and_drop/helpers'; +import { getEmptyTagValue } from '../../../common/components/empty_value'; +import { IPDetailsLink } from '../../../common/components/links'; +import { Columns } from '../../../common/components/paginated_table'; +import { IS_OPERATOR } from '../../../timelines/components/timeline/data_providers/data_provider'; +import { Provider } from '../../../timelines/components/timeline/data_providers/provider'; +import * as i18n from './translations'; +import { + getRowItemDraggable, + getRowItemDraggables, +} from '../../../common/components/tables/helpers'; +import { PreferenceFormattedBytes } from '../../../common/components/formatted_bytes'; + +export type NetworkTopNFlowColumns = [ + Columns, + Columns, + Columns, + Columns, + Columns, + Columns, + Columns +]; + +export type NetworkTopNFlowColumnsIpDetails = [ + Columns, + Columns, + Columns, + Columns, + Columns, + Columns +]; + +export const getNetworkTopNFlowColumns = ( + flowTarget: FlowTargetSourceDest, + tableId: string +): NetworkTopNFlowColumns => [ + { + name: i18n.IP_TITLE, + render: ({ node }) => { + const ipAttr = `${flowTarget}.ip`; + const ip: string | null = get(ipAttr, node); + const geoAttr = `${flowTarget}.location.geo.country_iso_code[0]`; + const geoAttrName = `${flowTarget}.geo.country_iso_code`; + const geo: string | null = get(geoAttr, node); + const id = escapeDataProviderId(`${tableId}-table-${flowTarget}-ip-${ip}`); + + if (ip != null) { + return ( + <> + + snapshot.isDragging ? ( + + + + ) : ( + + ) + } + /> + + {geo && ( + + snapshot.isDragging ? ( + + + + ) : ( + <> + {' '} + {geo} + + ) + } + /> + )} + + ); + } else { + return getEmptyTagValue(); + } + }, + width: '20%', + }, + { + name: i18n.DOMAIN, + render: ({ node }) => { + const domainAttr = `${flowTarget}.domain`; + const ipAttr = `${flowTarget}.ip`; + const domains: string[] = get(domainAttr, node); + const ip: string | null = get(ipAttr, node); + + if (Array.isArray(domains) && domains.length > 0) { + const id = escapeDataProviderId(`${tableId}-table-${ip}`); + return getRowItemDraggables({ + rowItems: domains, + attrName: domainAttr, + idPrefix: id, + displayCount: 1, + }); + } else { + return getEmptyTagValue(); + } + }, + width: '20%', + }, + { + name: i18n.AUTONOMOUS_SYSTEM, + render: ({ node, cursor: { value: ipAddress } }) => { + const asAttr = `${flowTarget}.autonomous_system`; + const as: AutonomousSystemItem | null = get(asAttr, node); + if (as != null) { + const id = escapeDataProviderId(`${tableId}-table-${flowTarget}-ip-${ipAddress}`); + return ( + <> + {as.name && + getRowItemDraggable({ + rowItem: as.name, + attrName: `${flowTarget}.as.organization.name`, + idPrefix: `${id}-name`, + })} + + {as.number && ( + <> + {' '} + {getRowItemDraggable({ + rowItem: `${as.number}`, + attrName: `${flowTarget}.as.number`, + idPrefix: `${id}-number`, + })} + + )} + + ); + } else { + return getEmptyTagValue(); + } + }, + width: '20%', + }, + { + align: 'right', + field: 'node.network.bytes_in', + name: i18n.BYTES_IN, + sortable: true, + render: bytes => { + if (bytes != null) { + return ; + } else { + return getEmptyTagValue(); + } + }, + }, + { + align: 'right', + field: 'node.network.bytes_out', + name: i18n.BYTES_OUT, + sortable: true, + render: bytes => { + if (bytes != null) { + return ; + } else { + return getEmptyTagValue(); + } + }, + }, + { + align: 'right', + field: `node.${flowTarget}.flows`, + name: i18n.FLOWS, + sortable: true, + render: flows => { + if (flows != null) { + return numeral(flows).format('0,000'); + } else { + return getEmptyTagValue(); + } + }, + }, + { + align: 'right', + field: `node.${flowTarget}.${getOppositeField(flowTarget)}_ips`, + name: flowTarget === FlowTargetSourceDest.source ? i18n.DESTINATION_IPS : i18n.SOURCE_IPS, + sortable: true, + render: ips => { + if (ips != null) { + return numeral(ips).format('0,000'); + } else { + return getEmptyTagValue(); + } + }, + }, +]; + +export const getNFlowColumnsCurated = ( + flowTarget: FlowTargetSourceDest, + type: networkModel.NetworkType, + tableId: string +): NetworkTopNFlowColumns | NetworkTopNFlowColumnsIpDetails => { + const columns = getNetworkTopNFlowColumns(flowTarget, tableId); + + // Columns to exclude from host details pages + if (type === networkModel.NetworkType.details) { + columns.pop(); + return columns; + } + + return columns; +}; + +const getOppositeField = (flowTarget: FlowTargetSourceDest): FlowTargetSourceDest => + flowTarget === FlowTargetSourceDest.source + ? FlowTargetSourceDest.destination + : FlowTargetSourceDest.source; diff --git a/x-pack/plugins/siem/public/network/components/network_top_n_flow_table/index.test.tsx b/x-pack/plugins/siem/public/network/components/network_top_n_flow_table/index.test.tsx new file mode 100644 index 00000000000000..58a7ef744adeed --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/network_top_n_flow_table/index.test.tsx @@ -0,0 +1,149 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React from 'react'; +import { MockedProvider } from 'react-apollo/test-utils'; +import { Provider as ReduxStoreProvider } from 'react-redux'; + +import { FlowTargetSourceDest } from '../../../graphql/types'; +import { + apolloClientObservable, + mockGlobalState, + TestProviders, + SUB_PLUGINS_REDUCER, +} from '../../../common/mock'; +import { useMountAppended } from '../../../common/utils/use_mount_appended'; +import { createStore, State } from '../../../common/store'; +import { networkModel } from '../../store'; +import { NetworkTopNFlowTable } from '.'; +import { mockData } from './mock'; + +describe('NetworkTopNFlow Table Component', () => { + const loadPage = jest.fn(); + const state: State = mockGlobalState; + + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + const mount = useMountAppended(); + + beforeEach(() => { + store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + }); + + describe('rendering', () => { + test('it renders the default NetworkTopNFlow table on the Network page', () => { + const wrapper = shallow( + + + + ); + + expect(wrapper.find('Connect(Component)')).toMatchSnapshot(); + }); + + test('it renders the default NetworkTopNFlow table on the IP Details page', () => { + const wrapper = shallow( + + + + ); + + expect(wrapper.find('Connect(Component)')).toMatchSnapshot(); + }); + }); + + describe('Sorting on Table', () => { + test('when you click on the column header, you should show the sorting icon', () => { + const wrapper = mount( + + + + + + ); + expect(store.getState().network.page.queries.topNFlowSource.sort).toEqual({ + direction: 'desc', + field: 'bytes_out', + }); + + wrapper + .find('.euiTable thead tr th button') + .at(1) + .simulate('click'); + + wrapper.update(); + + expect(store.getState().network.page.queries.topNFlowSource.sort).toEqual({ + direction: 'asc', + field: 'bytes_out', + }); + expect( + wrapper + .find('.euiTable thead tr th button') + .first() + .text() + ).toEqual('Bytes inClick to sort in ascending order'); + expect( + wrapper + .find('.euiTable thead tr th button') + .at(1) + .text() + ).toEqual('Bytes outClick to sort in descending order'); + expect( + wrapper + .find('.euiTable thead tr th button') + .at(1) + .find('svg') + ).toBeTruthy(); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/network/components/network_top_n_flow_table/index.tsx b/x-pack/plugins/siem/public/network/components/network_top_n_flow_table/index.tsx new file mode 100644 index 00000000000000..617dd9d08a9db4 --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/network_top_n_flow_table/index.tsx @@ -0,0 +1,173 @@ +/* + * 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 { last } from 'lodash/fp'; +import React, { useCallback, useMemo } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; + +import { + Direction, + FlowTargetSourceDest, + NetworkTopNFlowEdges, + NetworkTopTablesFields, + NetworkTopTablesSortField, +} from '../../../graphql/types'; +import { State } from '../../../common/store'; +import { Criteria, ItemsPerRow, PaginatedTable } from '../../../common/components/paginated_table'; +import { networkActions, networkModel, networkSelectors } from '../../store'; +import { getNFlowColumnsCurated } from './columns'; +import * as i18n from './translations'; + +interface OwnProps { + data: NetworkTopNFlowEdges[]; + fakeTotalCount: number; + flowTargeted: FlowTargetSourceDest; + id: string; + isInspect: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + showMorePagesIndicator: boolean; + totalCount: number; + type: networkModel.NetworkType; +} + +type NetworkTopNFlowTableProps = OwnProps & PropsFromRedux; + +const rowItems: ItemsPerRow[] = [ + { + text: i18n.ROWS_5, + numberOfRow: 5, + }, + { + text: i18n.ROWS_10, + numberOfRow: 10, + }, +]; + +export const NetworkTopNFlowTableId = 'networkTopSourceFlow-top-talkers'; + +const NetworkTopNFlowTableComponent: React.FC = ({ + activePage, + data, + fakeTotalCount, + flowTargeted, + id, + isInspect, + limit, + loading, + loadPage, + showMorePagesIndicator, + sort, + totalCount, + type, + updateNetworkTable, +}) => { + const columns = useMemo( + () => getNFlowColumnsCurated(flowTargeted, type, NetworkTopNFlowTableId), + [flowTargeted, type] + ); + + let tableType: networkModel.TopNTableType; + const headerTitle: string = + flowTargeted === FlowTargetSourceDest.source ? i18n.SOURCE_IP : i18n.DESTINATION_IP; + + if (type === networkModel.NetworkType.page) { + tableType = + flowTargeted === FlowTargetSourceDest.source + ? networkModel.NetworkTableType.topNFlowSource + : networkModel.NetworkTableType.topNFlowDestination; + } else { + tableType = + flowTargeted === FlowTargetSourceDest.source + ? networkModel.IpDetailsTableType.topNFlowSource + : networkModel.IpDetailsTableType.topNFlowDestination; + } + + const onChange = useCallback( + (criteria: Criteria) => { + if (criteria.sort != null) { + const splitField = criteria.sort.field.split('.'); + const field = last(splitField); + const newSortDirection = field !== sort.field ? Direction.desc : criteria.sort.direction; // sort by desc on init click + const newTopNFlowSort: NetworkTopTablesSortField = { + field: field as NetworkTopTablesFields, + direction: newSortDirection as Direction, + }; + if (!deepEqual(newTopNFlowSort, sort)) { + updateNetworkTable({ + networkType: type, + tableType, + updates: { + sort: newTopNFlowSort, + }, + }); + } + } + }, + [sort, type, tableType, updateNetworkTable] + ); + + const field = + sort.field === NetworkTopTablesFields.bytes_out || + sort.field === NetworkTopTablesFields.bytes_in + ? `node.network.${sort.field}` + : `node.${flowTargeted}.${sort.field}`; + + const updateActivePage = useCallback( + newPage => + updateNetworkTable({ + networkType: type, + tableType, + updates: { activePage: newPage }, + }), + [updateNetworkTable, type, tableType] + ); + + const updateLimitPagination = useCallback( + newLimit => updateNetworkTable({ networkType: type, tableType, updates: { limit: newLimit } }), + [updateNetworkTable, type, tableType] + ); + + return ( + + ); +}; + +const makeMapStateToProps = () => { + const getTopNFlowSelector = networkSelectors.topNFlowSelector(); + return (state: State, { type, flowTargeted }: OwnProps) => + getTopNFlowSelector(state, type, flowTargeted); +}; + +const mapDispatchToProps = { + updateNetworkTable: networkActions.updateNetworkTable, +}; + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const NetworkTopNFlowTable = connector(React.memo(NetworkTopNFlowTableComponent)); diff --git a/x-pack/plugins/siem/public/network/components/network_top_n_flow_table/mock.ts b/x-pack/plugins/siem/public/network/components/network_top_n_flow_table/mock.ts new file mode 100644 index 00000000000000..bd21d78ba77c5d --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/network_top_n_flow_table/mock.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 { NetworkTopNFlowData, FlowTargetSourceDest } from '../../../graphql/types'; + +export const mockData: { NetworkTopNFlow: NetworkTopNFlowData } = { + NetworkTopNFlow: { + totalCount: 524, + edges: [ + { + node: { + source: { + autonomous_system: { + name: 'Google, Inc', + number: 15169, + }, + domain: ['test.domain.com'], + flows: 12345, + destination_ips: 12, + ip: '8.8.8.8', + location: { + geo: { + continent_name: ['North America'], + country_name: null, + country_iso_code: ['US'], + city_name: ['Mountain View'], + region_iso_code: ['US-CA'], + region_name: ['California'], + }, + flowTarget: FlowTargetSourceDest.source, + }, + }, + destination: null, + network: { + bytes_in: 3826633497, + bytes_out: 1083495734, + }, + }, + cursor: { + value: '8.8.8.8', + }, + }, + { + node: { + source: { + autonomous_system: { + name: 'TM Net, Internet Service Provider', + number: 4788, + }, + domain: ['test.domain.net', 'test.old.domain.net'], + flows: 12345, + destination_ips: 12, + ip: '9.9.9.9', + location: { + geo: { + continent_name: ['Asia'], + country_name: null, + country_iso_code: ['MY'], + city_name: ['Petaling Jaya'], + region_iso_code: ['MY-10'], + region_name: ['Selangor'], + }, + flowTarget: FlowTargetSourceDest.source, + }, + }, + destination: null, + network: { + bytes_in: 3826633497, + bytes_out: 1083495734, + }, + }, + cursor: { + value: '9.9.9.9', + }, + }, + ], + pageInfo: { + activePage: 1, + fakeTotalCount: 50, + showMorePagesIndicator: true, + }, + }, +}; diff --git a/x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/translations.ts b/x-pack/plugins/siem/public/network/components/network_top_n_flow_table/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/page/network/network_top_n_flow_table/translations.ts rename to x-pack/plugins/siem/public/network/components/network_top_n_flow_table/translations.ts diff --git a/x-pack/plugins/siem/public/components/port/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/network/components/port/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/port/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/network/components/port/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/network/components/port/index.test.tsx b/x-pack/plugins/siem/public/network/components/port/index.test.tsx new file mode 100644 index 00000000000000..1f78f1d96cdaef --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/port/index.test.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { shallow } from 'enzyme'; +import React from 'react'; + +import { TestProviders } from '../../../common/mock/test_providers'; +import { useMountAppended } from '../../../common/utils/use_mount_appended'; + +import { Port } from '.'; + +describe('Port', () => { + const mount = useMountAppended(); + + test('renders correctly against snapshot', () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); + + test('it renders the port', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="port"]') + .first() + .text() + ).toEqual('443'); + }); + + test('it hyperlinks links destination.port to an external service that describes the purpose of the port', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="port-or-service-name-link"]') + .first() + .props().href + ).toEqual( + 'https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=443' + ); + }); + + test('it renders an external link', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="external-link-icon"]') + .first() + .exists() + ).toBe(true); + }); +}); diff --git a/x-pack/plugins/siem/public/network/components/port/index.tsx b/x-pack/plugins/siem/public/network/components/port/index.tsx new file mode 100644 index 00000000000000..6f54f11ccfbe14 --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/port/index.tsx @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { DefaultDraggable } from '../../../common/components/draggables'; +import { getEmptyValue } from '../../../common/components/empty_value'; +import { ExternalLinkIcon } from '../../../common/components/external_link_icon'; +import { PortOrServiceNameLink } from '../../../common/components/links'; + +export const CLIENT_PORT_FIELD_NAME = 'client.port'; +export const DESTINATION_PORT_FIELD_NAME = 'destination.port'; +export const SERVER_PORT_FIELD_NAME = 'server.port'; +export const SOURCE_PORT_FIELD_NAME = 'source.port'; +export const URL_PORT_FIELD_NAME = 'url.port'; + +export const PORT_NAMES = [ + CLIENT_PORT_FIELD_NAME, + DESTINATION_PORT_FIELD_NAME, + SERVER_PORT_FIELD_NAME, + SOURCE_PORT_FIELD_NAME, + URL_PORT_FIELD_NAME, +]; + +export const Port = React.memo<{ + contextId: string; + eventId: string; + fieldName: string; + value: string | undefined | null; +}>(({ contextId, eventId, fieldName, value }) => ( + + + + +)); + +Port.displayName = 'Port'; diff --git a/x-pack/plugins/siem/public/components/source_destination/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/network/components/source_destination/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/source_destination/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/network/components/source_destination/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/source_destination/country_flag.tsx b/x-pack/plugins/siem/public/network/components/source_destination/country_flag.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/source_destination/country_flag.tsx rename to x-pack/plugins/siem/public/network/components/source_destination/country_flag.tsx diff --git a/x-pack/plugins/siem/public/components/source_destination/field_names.ts b/x-pack/plugins/siem/public/network/components/source_destination/field_names.ts similarity index 100% rename from x-pack/plugins/siem/public/components/source_destination/field_names.ts rename to x-pack/plugins/siem/public/network/components/source_destination/field_names.ts diff --git a/x-pack/plugins/siem/public/components/source_destination/geo_fields.tsx b/x-pack/plugins/siem/public/network/components/source_destination/geo_fields.tsx similarity index 98% rename from x-pack/plugins/siem/public/components/source_destination/geo_fields.tsx rename to x-pack/plugins/siem/public/network/components/source_destination/geo_fields.tsx index baeca10ee0fae6..3618ee40dc8d59 100644 --- a/x-pack/plugins/siem/public/components/source_destination/geo_fields.tsx +++ b/x-pack/plugins/siem/public/network/components/source_destination/geo_fields.tsx @@ -9,7 +9,7 @@ import { get, uniq } from 'lodash/fp'; import React from 'react'; import styled from 'styled-components'; -import { DefaultDraggable } from '../draggables'; +import { DefaultDraggable } from '../../../common/components/draggables'; import { CountryFlag } from './country_flag'; import { GeoFieldsProps, SourceDestinationType } from './types'; diff --git a/x-pack/plugins/siem/public/network/components/source_destination/index.test.tsx b/x-pack/plugins/siem/public/network/components/source_destination/index.test.tsx new file mode 100644 index 00000000000000..96545813bbbab3 --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/source_destination/index.test.tsx @@ -0,0 +1,419 @@ +/* + * 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 numeral from '@elastic/numeral'; +import { shallow } from 'enzyme'; +import { get } from 'lodash/fp'; +import React from 'react'; + +import { asArrayIfExists } from '../../../common/lib/helpers'; +import { getMockNetflowData } from '../../../common/mock'; +import { TestProviders } from '../../../common/mock/test_providers'; +import { ID_FIELD_NAME } from '../../../common/components/event_details/event_id'; +import { useMountAppended } from '../../../common/utils/use_mount_appended'; +import { DESTINATION_IP_FIELD_NAME, SOURCE_IP_FIELD_NAME } from '../ip'; +import { DESTINATION_PORT_FIELD_NAME, SOURCE_PORT_FIELD_NAME } from '../port'; +import { + DESTINATION_BYTES_FIELD_NAME, + DESTINATION_PACKETS_FIELD_NAME, + SOURCE_BYTES_FIELD_NAME, + SOURCE_PACKETS_FIELD_NAME, +} from '../source_destination/source_destination_arrows'; +import * as i18n from '../../../timelines/components/timeline/body/renderers/translations'; + +import { SourceDestination } from '.'; +import { + DESTINATION_GEO_CITY_NAME_FIELD_NAME, + DESTINATION_GEO_CONTINENT_NAME_FIELD_NAME, + DESTINATION_GEO_COUNTRY_ISO_CODE_FIELD_NAME, + DESTINATION_GEO_COUNTRY_NAME_FIELD_NAME, + DESTINATION_GEO_REGION_NAME_FIELD_NAME, + SOURCE_GEO_CITY_NAME_FIELD_NAME, + SOURCE_GEO_CONTINENT_NAME_FIELD_NAME, + SOURCE_GEO_COUNTRY_ISO_CODE_FIELD_NAME, + SOURCE_GEO_COUNTRY_NAME_FIELD_NAME, + SOURCE_GEO_REGION_NAME_FIELD_NAME, +} from './geo_fields'; +import { + NETWORK_BYTES_FIELD_NAME, + NETWORK_COMMUNITY_ID_FIELD_NAME, + NETWORK_DIRECTION_FIELD_NAME, + NETWORK_PACKETS_FIELD_NAME, + NETWORK_PROTOCOL_FIELD_NAME, + NETWORK_TRANSPORT_FIELD_NAME, +} from './field_names'; + +const getSourceDestinationInstance = () => ( + +); + +describe('SourceDestination', () => { + const mount = useMountAppended(); + + test('renders correctly against snapshot', () => { + const wrapper = shallow(
{getSourceDestinationInstance()}
); + expect(wrapper).toMatchSnapshot(); + }); + + test('it renders a destination label', () => { + const wrapper = mount({getSourceDestinationInstance()}); + + expect( + wrapper + .find('[data-test-subj="destination-label"]') + .first() + .text() + ).toEqual(i18n.DESTINATION); + }); + + test('it renders destination.bytes', () => { + const wrapper = mount({getSourceDestinationInstance()}); + + expect( + wrapper + .find('[data-test-subj="destination-bytes"]') + .first() + .text() + ).toEqual('40B'); + }); + + test('it renders percent destination.bytes', () => { + const wrapper = mount({getSourceDestinationInstance()}); + const destinationBytes = asArrayIfExists( + get(DESTINATION_BYTES_FIELD_NAME, getMockNetflowData()) + ); + const sumBytes = asArrayIfExists(get(NETWORK_BYTES_FIELD_NAME, getMockNetflowData())); + let percent = ''; + if (destinationBytes != null && sumBytes != null) { + percent = `(${numeral((destinationBytes[0] / sumBytes[0]) * 100).format('0.00')}%)`; + } + + expect( + wrapper + .find('[data-test-subj="destination-bytes-percent"]') + .first() + .text() + ).toEqual(percent); + }); + + test('it renders destination.geo.continent_name', () => { + const wrapper = mount({getSourceDestinationInstance()}); + + expect( + wrapper + .find('[data-test-subj="destination.geo.continent_name"]') + .first() + .text() + ).toEqual('North America'); + }); + + test('it renders destination.geo.country_name', () => { + const wrapper = mount({getSourceDestinationInstance()}); + + expect( + wrapper + .find('[data-test-subj="destination.geo.country_name"]') + .first() + .text() + ).toEqual('United States'); + }); + + test('it renders destination.geo.country_iso_code', () => { + const wrapper = mount({getSourceDestinationInstance()}); + + expect( + wrapper + .find('[data-test-subj="destination.geo.country_iso_code"]') + .first() + .text() + ).toEqual('US'); + }); + + test('it renders destination.geo.region_name', () => { + const wrapper = mount({getSourceDestinationInstance()}); + + expect( + wrapper + .find('[data-test-subj="destination.geo.region_name"]') + .first() + .text() + ).toEqual('New York'); + }); + + test('it renders destination.geo.city_name', () => { + const wrapper = mount({getSourceDestinationInstance()}); + + expect( + wrapper + .find('[data-test-subj="destination.geo.city_name"]') + .first() + .text() + ).toEqual('New York'); + }); + + test('it renders the destination ip and port, separated with a colon', () => { + const wrapper = mount({getSourceDestinationInstance()}); + + expect( + wrapper + .find('[data-test-subj="destination-ip-and-port"]') + .first() + .text() + ).toEqual('10.1.2.3:80'); + }); + + test('it renders destination.packets', () => { + const wrapper = mount({getSourceDestinationInstance()}); + + expect( + wrapper + .find('[data-test-subj="destination-packets"]') + .first() + .text() + ).toEqual('1 pkts'); + }); + + test('it hyperlinks links destination.port to an external service that describes the purpose of the port', () => { + const wrapper = mount({getSourceDestinationInstance()}); + + expect( + wrapper + .find('[data-test-subj="destination-ip-and-port"]') + .find('[data-test-subj="port-or-service-name-link"]') + .first() + .props().href + ).toEqual( + 'https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=80' + ); + }); + + test('it renders network.bytes', () => { + const wrapper = mount({getSourceDestinationInstance()}); + + expect( + wrapper + .find('[data-test-subj="network-bytes"]') + .first() + .text() + ).toEqual('100B'); + }); + + test('it renders network.community_id', () => { + const wrapper = mount({getSourceDestinationInstance()}); + + expect( + wrapper + .find('[data-test-subj="network-community-id"]') + .first() + .text() + ).toEqual('we.live.in.a'); + }); + + test('it renders network.direction', () => { + const wrapper = mount({getSourceDestinationInstance()}); + + expect( + wrapper + .find('[data-test-subj="network-direction"]') + .first() + .text() + ).toEqual('outgoing'); + }); + + test('it renders network.packets', () => { + const wrapper = mount({getSourceDestinationInstance()}); + + expect( + wrapper + .find('[data-test-subj="network-packets"]') + .first() + .text() + ).toEqual('3 pkts'); + }); + + test('it renders network.protocol', () => { + const wrapper = mount({getSourceDestinationInstance()}); + + expect( + wrapper + .find('[data-test-subj="network-protocol"]') + .first() + .text() + ).toEqual('http'); + }); + + test('it renders a source label', () => { + const wrapper = mount({getSourceDestinationInstance()}); + + expect( + wrapper + .find('[data-test-subj="source-label"]') + .first() + .text() + ).toEqual(i18n.SOURCE); + }); + + test('it renders source.bytes', () => { + const wrapper = mount({getSourceDestinationInstance()}); + + expect( + wrapper + .find('[data-test-subj="source-bytes"]') + .first() + .text() + ).toEqual('60B'); + }); + + test('it renders percent source.bytes', () => { + const wrapper = mount({getSourceDestinationInstance()}); + const sourceBytes = asArrayIfExists(get(SOURCE_BYTES_FIELD_NAME, getMockNetflowData())); + const sumBytes = asArrayIfExists(get(NETWORK_BYTES_FIELD_NAME, getMockNetflowData())); + let percent = ''; + if (sourceBytes != null && sumBytes != null) { + percent = `(${numeral((sourceBytes[0] / sumBytes[0]) * 100).format('0.00')}%)`; + } + + expect( + wrapper + .find('[data-test-subj="source-bytes-percent"]') + .first() + .text() + ).toEqual(percent); + }); + + test('it renders source.geo.continent_name', () => { + const wrapper = mount({getSourceDestinationInstance()}); + + expect( + wrapper + .find('[data-test-subj="source.geo.continent_name"]') + .first() + .text() + ).toEqual('North America'); + }); + + test('it renders source.geo.country_name', () => { + const wrapper = mount({getSourceDestinationInstance()}); + + expect( + wrapper + .find('[data-test-subj="source.geo.country_name"]') + .first() + .text() + ).toEqual('United States'); + }); + + test('it renders source.geo.country_iso_code', () => { + const wrapper = mount({getSourceDestinationInstance()}); + + expect( + wrapper + .find('[data-test-subj="source.geo.country_iso_code"]') + .first() + .text() + ).toEqual('US'); + }); + + test('it renders source.geo.region_name', () => { + const wrapper = mount({getSourceDestinationInstance()}); + + expect( + wrapper + .find('[data-test-subj="source.geo.region_name"]') + .first() + .text() + ).toEqual('Georgia'); + }); + + test('it renders source.geo.city_name', () => { + const wrapper = mount({getSourceDestinationInstance()}); + + expect( + wrapper + .find('[data-test-subj="source.geo.city_name"]') + .first() + .text() + ).toEqual('Atlanta'); + }); + + test('it renders the source ip and port, separated with a colon', () => { + const wrapper = mount({getSourceDestinationInstance()}); + + expect( + wrapper + .find('[data-test-subj="source-ip-and-port"]') + .first() + .text() + ).toEqual('192.168.1.2:9987'); + }); + + test('it renders source.packets', () => { + const wrapper = mount({getSourceDestinationInstance()}); + + expect( + wrapper + .find('[data-test-subj="source-packets"]') + .first() + .text() + ).toEqual('2 pkts'); + }); + + test('it renders network.transport', () => { + const wrapper = mount({getSourceDestinationInstance()}); + + expect( + wrapper + .find('[data-test-subj="network-transport"]') + .first() + .text() + ).toEqual('tcp'); + }); +}); diff --git a/x-pack/plugins/siem/public/components/source_destination/index.tsx b/x-pack/plugins/siem/public/network/components/source_destination/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/source_destination/index.tsx rename to x-pack/plugins/siem/public/network/components/source_destination/index.tsx diff --git a/x-pack/plugins/siem/public/components/source_destination/ip_with_port.tsx b/x-pack/plugins/siem/public/network/components/source_destination/ip_with_port.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/source_destination/ip_with_port.tsx rename to x-pack/plugins/siem/public/network/components/source_destination/ip_with_port.tsx diff --git a/x-pack/plugins/siem/public/components/source_destination/label.tsx b/x-pack/plugins/siem/public/network/components/source_destination/label.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/source_destination/label.tsx rename to x-pack/plugins/siem/public/network/components/source_destination/label.tsx diff --git a/x-pack/plugins/siem/public/network/components/source_destination/network.tsx b/x-pack/plugins/siem/public/network/components/source_destination/network.tsx new file mode 100644 index 00000000000000..cb1f72bca02c61 --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/source_destination/network.tsx @@ -0,0 +1,140 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { uniq } from 'lodash/fp'; +import React from 'react'; +import styled from 'styled-components'; + +import { DirectionBadge } from '../direction'; +import { DefaultDraggable, DraggableBadge } from '../../../common/components/draggables'; + +import * as i18n from './translations'; +import { + NETWORK_BYTES_FIELD_NAME, + NETWORK_COMMUNITY_ID_FIELD_NAME, + NETWORK_PACKETS_FIELD_NAME, + NETWORK_PROTOCOL_FIELD_NAME, + NETWORK_TRANSPORT_FIELD_NAME, +} from './field_names'; +import { PreferenceFormattedBytes } from '../../../common/components/formatted_bytes'; + +const EuiFlexItemMarginRight = styled(EuiFlexItem)` + margin-right: 3px; +`; + +EuiFlexItemMarginRight.displayName = 'EuiFlexItemMarginRight'; + +const Stats = styled(EuiText)` + margin: 0 5px; +`; + +Stats.displayName = 'Stats'; + +/** + * Renders a row of draggable badges containing fields from the + * `Network` category of fields + */ +export const Network = React.memo<{ + bytes?: string[] | null; + communityId?: string[] | null; + contextId: string; + direction?: string[] | null; + eventId: string; + packets?: string[] | null; + protocol?: string[] | null; + transport?: string[] | null; +}>(({ bytes, communityId, contextId, direction, eventId, packets, protocol, transport }) => ( + + {direction != null + ? uniq(direction).map(dir => ( + + + + )) + : null} + + {protocol != null + ? uniq(protocol).map(proto => ( + + + + )) + : null} + + {bytes != null + ? uniq(bytes).map(b => + !isNaN(Number(b)) ? ( + + + + + + + + + + ) : null + ) + : null} + + {packets != null + ? uniq(packets).map(p => ( + + + + {`${p} ${i18n.PACKETS}`} + + + + )) + : null} + + {transport != null + ? uniq(transport).map(trans => ( + + + + )) + : null} + + {communityId != null + ? uniq(communityId).map(trans => ( + + + + )) + : null} + +)); + +Network.displayName = 'Network'; diff --git a/x-pack/plugins/siem/public/components/source_destination/source_destination_arrows.tsx b/x-pack/plugins/siem/public/network/components/source_destination/source_destination_arrows.tsx similarity index 98% rename from x-pack/plugins/siem/public/components/source_destination/source_destination_arrows.tsx rename to x-pack/plugins/siem/public/network/components/source_destination/source_destination_arrows.tsx index 005ebc14dcdcc6..95cc76a349c177 100644 --- a/x-pack/plugins/siem/public/components/source_destination/source_destination_arrows.tsx +++ b/x-pack/plugins/siem/public/network/components/source_destination/source_destination_arrows.tsx @@ -16,8 +16,8 @@ import { getPercent, hasOneValue, } from '../arrows/helpers'; -import { DefaultDraggable } from '../draggables'; -import { PreferenceFormattedBytes } from '../formatted_bytes'; +import { DefaultDraggable } from '../../../common/components/draggables'; +import { PreferenceFormattedBytes } from '../../../common/components/formatted_bytes'; import * as i18n from './translations'; diff --git a/x-pack/plugins/siem/public/components/source_destination/source_destination_ip.test.tsx b/x-pack/plugins/siem/public/network/components/source_destination/source_destination_ip.test.tsx similarity index 99% rename from x-pack/plugins/siem/public/components/source_destination/source_destination_ip.test.tsx rename to x-pack/plugins/siem/public/network/components/source_destination/source_destination_ip.test.tsx index 60ab59c3796ff0..18459352f89f05 100644 --- a/x-pack/plugins/siem/public/components/source_destination/source_destination_ip.test.tsx +++ b/x-pack/plugins/siem/public/network/components/source_destination/source_destination_ip.test.tsx @@ -7,14 +7,14 @@ import { get } from 'lodash/fp'; import React from 'react'; -import { asArrayIfExists } from '../../lib/helpers'; -import { getMockNetflowData } from '../../mock'; -import { TestProviders } from '../../mock/test_providers'; -import { ID_FIELD_NAME } from '../event_details/event_id'; +import { asArrayIfExists } from '../../../common/lib/helpers'; +import { getMockNetflowData } from '../../../common/mock'; +import { TestProviders } from '../../../common/mock/test_providers'; +import { ID_FIELD_NAME } from '../../../common/components/event_details/event_id'; import { DESTINATION_IP_FIELD_NAME, SOURCE_IP_FIELD_NAME } from '../ip'; import { DESTINATION_PORT_FIELD_NAME, SOURCE_PORT_FIELD_NAME } from '../port'; -import * as i18n from '../timeline/body/renderers/translations'; -import { useMountAppended } from '../../utils/use_mount_appended'; +import * as i18n from '../../../timelines/components/timeline/body/renderers/translations'; +import { useMountAppended } from '../../../common/utils/use_mount_appended'; import { getPorts, diff --git a/x-pack/plugins/siem/public/components/source_destination/source_destination_ip.tsx b/x-pack/plugins/siem/public/network/components/source_destination/source_destination_ip.tsx similarity index 98% rename from x-pack/plugins/siem/public/components/source_destination/source_destination_ip.tsx rename to x-pack/plugins/siem/public/network/components/source_destination/source_destination_ip.tsx index 62f01dfc020f5a..4a242961d91fda 100644 --- a/x-pack/plugins/siem/public/components/source_destination/source_destination_ip.tsx +++ b/x-pack/plugins/siem/public/network/components/source_destination/source_destination_ip.tsx @@ -11,7 +11,7 @@ import deepEqual from 'fast-deep-equal'; import { DESTINATION_IP_FIELD_NAME, SOURCE_IP_FIELD_NAME } from '../ip'; import { DESTINATION_PORT_FIELD_NAME, SOURCE_PORT_FIELD_NAME, Port } from '../port'; -import * as i18n from '../timeline/body/renderers/translations'; +import * as i18n from '../../../timelines/components/timeline/body/renderers/translations'; import { GeoFields } from './geo_fields'; import { IpWithPort } from './ip_with_port'; diff --git a/x-pack/plugins/siem/public/components/source_destination/source_destination_with_arrows.tsx b/x-pack/plugins/siem/public/network/components/source_destination/source_destination_with_arrows.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/source_destination/source_destination_with_arrows.tsx rename to x-pack/plugins/siem/public/network/components/source_destination/source_destination_with_arrows.tsx diff --git a/x-pack/plugins/siem/public/components/source_destination/translations.ts b/x-pack/plugins/siem/public/network/components/source_destination/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/source_destination/translations.ts rename to x-pack/plugins/siem/public/network/components/source_destination/translations.ts diff --git a/x-pack/plugins/siem/public/components/source_destination/types.ts b/x-pack/plugins/siem/public/network/components/source_destination/types.ts similarity index 100% rename from x-pack/plugins/siem/public/components/source_destination/types.ts rename to x-pack/plugins/siem/public/network/components/source_destination/types.ts diff --git a/x-pack/plugins/siem/public/components/page/network/tls_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/network/components/tls_table/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/page/network/tls_table/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/network/components/tls_table/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/network/components/tls_table/columns.tsx b/x-pack/plugins/siem/public/network/components/tls_table/columns.tsx new file mode 100644 index 00000000000000..5a6317291430ec --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/tls_table/columns.tsx @@ -0,0 +1,102 @@ +/* + * 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. + */ + +/* eslint-disable react/display-name */ + +import React from 'react'; +import moment from 'moment'; +import { TlsNode } from '../../../graphql/types'; +import { Columns } from '../../../common/components/paginated_table'; + +import { + getRowItemDraggables, + getRowItemDraggable, +} from '../../../common/components/tables/helpers'; +import { LocalizedDateTooltip } from '../../../common/components/localized_date_tooltip'; +import { PreferenceFormattedDate } from '../../../common/components/formatted_date'; + +import * as i18n from './translations'; + +export type TlsColumns = [ + Columns, + Columns, + Columns, + Columns, + Columns +]; + +export const getTlsColumns = (tableId: string): TlsColumns => [ + { + field: 'node', + name: i18n.ISSUER, + truncateText: false, + hideForMobile: false, + sortable: false, + render: ({ _id, issuers }) => + getRowItemDraggables({ + rowItems: issuers, + attrName: 'tls.server.issuer', + idPrefix: `${tableId}-${_id}-table-issuers`, + }), + }, + { + field: 'node', + name: i18n.SUBJECT, + truncateText: false, + hideForMobile: false, + sortable: false, + render: ({ _id, subjects }) => + getRowItemDraggables({ + rowItems: subjects, + attrName: 'tls.server.subject', + idPrefix: `${tableId}-${_id}-table-subjects`, + }), + }, + { + field: 'node._id', + name: i18n.SHA1_FINGERPRINT, + truncateText: false, + hideForMobile: false, + sortable: true, + render: sha1 => + getRowItemDraggable({ + rowItem: sha1, + attrName: 'tls.server_certificate.fingerprint.sha1', + idPrefix: `${tableId}-${sha1}-table-sha1`, + }), + }, + { + field: 'node', + name: i18n.JA3_FINGERPRINT, + truncateText: false, + hideForMobile: false, + sortable: false, + render: ({ _id, ja3 }) => + getRowItemDraggables({ + rowItems: ja3, + attrName: 'tls.fingerprints.ja3.hash', + idPrefix: `${tableId}-${_id}-table-ja3`, + }), + }, + { + field: 'node', + name: i18n.VALID_UNTIL, + truncateText: false, + hideForMobile: false, + sortable: false, + render: ({ _id, notAfter }) => + getRowItemDraggables({ + rowItems: notAfter, + attrName: 'tls.server_certificate.not_after', + idPrefix: `${tableId}-${_id}-table-notAfter`, + render: validUntil => ( + + + + ), + }), + }, +]; diff --git a/x-pack/plugins/siem/public/network/components/tls_table/index.test.tsx b/x-pack/plugins/siem/public/network/components/tls_table/index.test.tsx new file mode 100644 index 00000000000000..7f2cfc8ba9ba4a --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/tls_table/index.test.tsx @@ -0,0 +1,102 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React from 'react'; +import { MockedProvider } from 'react-apollo/test-utils'; +import { Provider as ReduxStoreProvider } from 'react-redux'; + +import { + apolloClientObservable, + mockGlobalState, + TestProviders, + SUB_PLUGINS_REDUCER, +} from '../../../common/mock'; +import { useMountAppended } from '../../../common/utils/use_mount_appended'; +import { createStore, State } from '../../../common/store'; +import { networkModel } from '../../store'; +import { TlsTable } from '.'; +import { mockTlsData } from './mock'; + +describe('Tls Table Component', () => { + const loadPage = jest.fn(); + const state: State = mockGlobalState; + + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + const mount = useMountAppended(); + + beforeEach(() => { + store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + }); + + describe('Rendering', () => { + test('it renders the default Domains table', () => { + const wrapper = shallow( + + + + ); + + expect(wrapper.find('Connect(TlsTableComponent)')).toMatchSnapshot(); + }); + }); + + describe('Sorting on Table', () => { + test('when you click on the column header, you should show the sorting icon', () => { + const wrapper = mount( + + + + + + ); + expect(store.getState().network.details.queries!.tls.sort).toEqual({ + direction: 'desc', + field: '_id', + }); + + wrapper + .find('.euiTable thead tr th button') + .first() + .simulate('click'); + + wrapper.update(); + + expect(store.getState().network.details.queries!.tls.sort).toEqual({ + direction: 'asc', + field: '_id', + }); + + expect( + wrapper + .find('.euiTable thead tr th button') + .first() + .text() + ).toEqual('SHA1 fingerprintClick to sort in descending order'); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/network/components/tls_table/index.tsx b/x-pack/plugins/siem/public/network/components/tls_table/index.tsx new file mode 100644 index 00000000000000..34bde8f42eaf90 --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/tls_table/index.tsx @@ -0,0 +1,168 @@ +/* + * 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, { useCallback, useMemo } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; + +import { networkActions, networkModel, networkSelectors } from '../../store'; +import { TlsEdges, TlsSortField, TlsFields, Direction } from '../../../graphql/types'; +import { State } from '../../../common/store'; +import { + Criteria, + ItemsPerRow, + PaginatedTable, + SortingBasicTable, +} from '../../../common/components/paginated_table'; +import { getTlsColumns } from './columns'; +import * as i18n from './translations'; + +interface OwnProps { + data: TlsEdges[]; + fakeTotalCount: number; + id: string; + isInspect: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + showMorePagesIndicator: boolean; + totalCount: number; + type: networkModel.NetworkType; +} + +type TlsTableProps = OwnProps & PropsFromRedux; + +const rowItems: ItemsPerRow[] = [ + { + text: i18n.ROWS_5, + numberOfRow: 5, + }, + { + text: i18n.ROWS_10, + numberOfRow: 10, + }, +]; + +export const tlsTableId = 'tls-table'; + +const TlsTableComponent = React.memo( + ({ + activePage, + data, + fakeTotalCount, + id, + isInspect, + limit, + loading, + loadPage, + showMorePagesIndicator, + sort, + totalCount, + type, + updateNetworkTable, + }) => { + const tableType: networkModel.TopTlsTableType = + type === networkModel.NetworkType.page + ? networkModel.NetworkTableType.tls + : networkModel.IpDetailsTableType.tls; + + const updateLimitPagination = useCallback( + newLimit => + updateNetworkTable({ + networkType: type, + tableType, + updates: { limit: newLimit }, + }), + [type, updateNetworkTable, tableType] + ); + + const updateActivePage = useCallback( + newPage => + updateNetworkTable({ + networkType: type, + tableType, + updates: { activePage: newPage }, + }), + [type, updateNetworkTable, tableType] + ); + + const onChange = useCallback( + (criteria: Criteria) => { + if (criteria.sort != null) { + const splitField = criteria.sort.field.split('.'); + const newTlsSort: TlsSortField = { + field: getSortFromString(splitField[splitField.length - 1]), + direction: criteria.sort.direction as Direction, + }; + if (!deepEqual(newTlsSort, sort)) { + updateNetworkTable({ + networkType: type, + tableType, + updates: { sort: newTlsSort }, + }); + } + } + }, + [sort, type, tableType, updateNetworkTable] + ); + + const columns = useMemo(() => getTlsColumns(tlsTableId), [tlsTableId]); + + return ( + + ); + } +); + +TlsTableComponent.displayName = 'TlsTableComponent'; + +const makeMapStateToProps = () => { + const getTlsSelector = networkSelectors.tlsSelector(); + return (state: State, { type }: OwnProps) => getTlsSelector(state, type); +}; + +const mapDispatchToProps = { + updateNetworkTable: networkActions.updateNetworkTable, +}; + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const TlsTable = connector(TlsTableComponent); + +const getSortField = (sortField: TlsSortField): SortingBasicTable => ({ + field: `node.${sortField.field}`, + direction: sortField.direction, +}); + +const getSortFromString = (sortField: string): TlsFields => { + switch (sortField) { + case '_id': + return TlsFields._id; + default: + return TlsFields._id; + } +}; diff --git a/x-pack/plugins/siem/public/network/components/tls_table/mock.ts b/x-pack/plugins/siem/public/network/components/tls_table/mock.ts new file mode 100644 index 00000000000000..a90907eb388545 --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/tls_table/mock.ts @@ -0,0 +1,54 @@ +/* + * 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 { TlsData } from '../../../graphql/types'; + +export const mockTlsData: TlsData = { + totalCount: 2, + edges: [ + { + node: { + _id: '2fe3bdf168af35b9e0ce5dc583bab007c40d47de', + subjects: ['*.elastic.co'], + ja3: ['7851693188210d3b271aa1713d8c68c2', 'fb4726d465c5f28b84cd6d14cedd13a7'], + issuers: ['DigiCert SHA2 Secure Server CA'], + notAfter: ['2021-04-22T12:00:00.000Z'], + }, + cursor: { + value: '2fe3bdf168af35b9e0ce5dc583bab007c40d47de', + }, + }, + { + node: { + _id: '61749734b3246f1584029deb4f5276c64da00ada', + subjects: ['api.snapcraft.io'], + ja3: ['839868ad711dc55bde0d37a87f14740d'], + issuers: ['DigiCert SHA2 Secure Server CA'], + notAfter: ['2019-05-22T12:00:00.000Z'], + }, + cursor: { + value: '61749734b3246f1584029deb4f5276c64da00ada', + }, + }, + { + node: { + _id: '6560d3b7dd001c989b85962fa64beb778cdae47a', + subjects: ['changelogs.ubuntu.com'], + ja3: ['da12c94da8021bbaf502907ad086e7bc'], + issuers: ["Let's Encrypt Authority X3"], + notAfter: ['2019-06-27T01:09:59.000Z'], + }, + cursor: { + value: '6560d3b7dd001c989b85962fa64beb778cdae47a', + }, + }, + ], + pageInfo: { + activePage: 1, + fakeTotalCount: 50, + showMorePagesIndicator: true, + }, +}; diff --git a/x-pack/plugins/siem/public/components/page/network/tls_table/translations.ts b/x-pack/plugins/siem/public/network/components/tls_table/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/page/network/tls_table/translations.ts rename to x-pack/plugins/siem/public/network/components/tls_table/translations.ts diff --git a/x-pack/plugins/siem/public/components/page/network/users_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/network/components/users_table/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/page/network/users_table/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/network/components/users_table/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/network/components/users_table/columns.tsx b/x-pack/plugins/siem/public/network/components/users_table/columns.tsx new file mode 100644 index 00000000000000..d3ad2cd707ecdd --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/users_table/columns.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FlowTarget, UsersItem } from '../../../graphql/types'; +import { defaultToEmptyTag } from '../../../common/components/empty_value'; +import { Columns } from '../../../common/components/paginated_table'; + +import * as i18n from './translations'; +import { + getRowItemDraggables, + getRowItemDraggable, +} from '../../../common/components/tables/helpers'; + +export type UsersColumns = [ + Columns, + Columns, + Columns, + Columns, + Columns +]; + +export const getUsersColumns = (flowTarget: FlowTarget, tableId: string): UsersColumns => [ + { + field: 'node.user.name', + name: i18n.USER_NAME, + truncateText: false, + hideForMobile: false, + sortable: true, + render: userName => + getRowItemDraggable({ + rowItem: userName, + attrName: 'user.name', + idPrefix: `${tableId}-table-${flowTarget}-user`, + }), + }, + { + field: 'node.user.id', + name: i18n.USER_ID, + truncateText: false, + hideForMobile: false, + sortable: false, + render: userIds => + getRowItemDraggables({ + rowItems: userIds, + attrName: 'user.id', + idPrefix: `${tableId}-table-${flowTarget}`, + }), + }, + { + field: 'node.user.groupName', + name: i18n.GROUP_NAME, + truncateText: false, + hideForMobile: false, + sortable: false, + render: groupNames => + getRowItemDraggables({ + rowItems: groupNames, + attrName: 'user.group.name', + idPrefix: `${tableId}-table-${flowTarget}`, + }), + }, + { + field: 'node.user.groupId', + name: i18n.GROUP_ID, + truncateText: false, + hideForMobile: false, + sortable: false, + render: groupId => + getRowItemDraggables({ + rowItems: groupId, + attrName: 'user.group.id', + idPrefix: `${tableId}-table-${flowTarget}`, + }), + }, + { + align: 'right', + field: 'node.user.count', + name: i18n.DOCUMENT_COUNT, + truncateText: false, + hideForMobile: false, + sortable: true, + render: docCount => defaultToEmptyTag(docCount), + }, +]; diff --git a/x-pack/plugins/siem/public/network/components/users_table/index.test.tsx b/x-pack/plugins/siem/public/network/components/users_table/index.test.tsx new file mode 100644 index 00000000000000..2597249797da58 --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/users_table/index.test.tsx @@ -0,0 +1,109 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React from 'react'; +import { MockedProvider } from 'react-apollo/test-utils'; +import { Provider as ReduxStoreProvider } from 'react-redux'; + +import { FlowTarget } from '../../../graphql/types'; +import { + apolloClientObservable, + mockGlobalState, + TestProviders, + SUB_PLUGINS_REDUCER, +} from '../../../common/mock'; +import { useMountAppended } from '../../../common/utils/use_mount_appended'; +import { createStore, State } from '../../../common/store'; +import { networkModel } from '../../store'; + +import { UsersTable } from '.'; +import { mockUsersData } from './mock'; + +describe('Users Table Component', () => { + const loadPage = jest.fn(); + const state: State = mockGlobalState; + + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + const mount = useMountAppended(); + + beforeEach(() => { + store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + }); + + describe('Rendering', () => { + test('it renders the default Users table', () => { + const wrapper = shallow( + + + + ); + + expect(wrapper.find('Connect(UsersTableComponent)')).toMatchSnapshot(); + }); + }); + + describe('Sorting on Table', () => { + test('when you click on the column header, you should show the sorting icon', () => { + const wrapper = mount( + + + + + + ); + expect(store.getState().network.details.queries!.users.sort).toEqual({ + direction: 'asc', + field: 'name', + }); + + wrapper + .find('.euiTable thead tr th button') + .first() + .simulate('click'); + + wrapper.update(); + + expect(store.getState().network.details.queries!.users.sort).toEqual({ + direction: 'desc', + field: 'name', + }); + expect( + wrapper + .find('.euiTable thead tr th button') + .first() + .text() + ).toEqual('UserClick to sort in ascending order'); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/network/components/users_table/index.tsx b/x-pack/plugins/siem/public/network/components/users_table/index.tsx new file mode 100644 index 00000000000000..5e5bac20141bcc --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/users_table/index.tsx @@ -0,0 +1,192 @@ +/* + * 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, { useCallback, useMemo } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; + +import { networkActions, networkModel, networkSelectors } from '../../store'; +import { + Direction, + FlowTarget, + UsersEdges, + UsersFields, + UsersSortField, +} from '../../../graphql/types'; +import { State } from '../../../common/store'; +import { + Criteria, + ItemsPerRow, + PaginatedTable, + SortingBasicTable, +} from '../../../common/components/paginated_table'; + +import { getUsersColumns } from './columns'; +import * as i18n from './translations'; +import { assertUnreachable } from '../../../common/lib/helpers'; +const tableType = networkModel.IpDetailsTableType.users; + +interface OwnProps { + data: UsersEdges[]; + flowTarget: FlowTarget; + fakeTotalCount: number; + id: string; + isInspect: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + showMorePagesIndicator: boolean; + totalCount: number; + type: networkModel.NetworkType; +} + +type UsersTableProps = OwnProps & PropsFromRedux; + +const rowItems: ItemsPerRow[] = [ + { + text: i18n.ROWS_5, + numberOfRow: 5, + }, + { + text: i18n.ROWS_10, + numberOfRow: 10, + }, +]; + +export const usersTableId = 'users-table'; + +const UsersTableComponent = React.memo( + ({ + activePage, + data, + fakeTotalCount, + flowTarget, + id, + isInspect, + limit, + loading, + loadPage, + showMorePagesIndicator, + totalCount, + type, + updateNetworkTable, + sort, + }) => { + const updateLimitPagination = useCallback( + newLimit => + updateNetworkTable({ + networkType: type, + tableType, + updates: { limit: newLimit }, + }), + [type, updateNetworkTable] + ); + + const updateActivePage = useCallback( + newPage => + updateNetworkTable({ + networkType: type, + tableType, + updates: { activePage: newPage }, + }), + [type, updateNetworkTable] + ); + + const onChange = useCallback( + (criteria: Criteria) => { + if (criteria.sort != null) { + const splitField = criteria.sort.field.split('.'); + const newUsersSort: UsersSortField = { + field: getSortFromString(splitField[splitField.length - 1]), + direction: criteria.sort.direction as Direction, + }; + if (!deepEqual(newUsersSort, sort)) { + updateNetworkTable({ + networkType: type, + tableType, + updates: { sort: newUsersSort }, + }); + } + } + }, + [sort, type, updateNetworkTable] + ); + + const columns = useMemo(() => getUsersColumns(flowTarget, usersTableId), [ + flowTarget, + usersTableId, + ]); + + return ( + + ); + } +); + +UsersTableComponent.displayName = 'UsersTableComponent'; + +const makeMapStateToProps = () => { + const getUsersSelector = networkSelectors.usersSelector(); + return (state: State) => ({ + ...getUsersSelector(state), + }); +}; + +const mapDispatchToProps = { + updateNetworkTable: networkActions.updateNetworkTable, +}; + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const UsersTable = connector(UsersTableComponent); + +const getSortField = (sortField: UsersSortField): SortingBasicTable => { + switch (sortField.field) { + case UsersFields.name: + return { + field: `node.user.${sortField.field}`, + direction: sortField.direction, + }; + case UsersFields.count: + return { + field: `node.user.${sortField.field}`, + direction: sortField.direction, + }; + } + return assertUnreachable(sortField.field); +}; + +const getSortFromString = (sortField: string): UsersFields => { + switch (sortField) { + case UsersFields.name.valueOf(): + return UsersFields.name; + case UsersFields.count.valueOf(): + return UsersFields.count; + default: + return UsersFields.name; + } +}; diff --git a/x-pack/plugins/siem/public/network/components/users_table/mock.ts b/x-pack/plugins/siem/public/network/components/users_table/mock.ts new file mode 100644 index 00000000000000..50bef1867aa3b4 --- /dev/null +++ b/x-pack/plugins/siem/public/network/components/users_table/mock.ts @@ -0,0 +1,66 @@ +/* + * 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 { UsersData } from '../../../graphql/types'; + +export const mockUsersData: UsersData = { + edges: [ + { + node: { + _id: '_apt', + user: { + id: ['104'], + name: '_apt', + groupId: ['65534'], + groupName: ['nogroup'], + count: 10, + }, + }, + cursor: { + value: '_apt', + tiebreaker: null, + }, + }, + { + node: { + _id: 'root', + user: { + id: ['0'], + name: 'root', + groupId: ['116', '0'], + groupName: ['Debian-exim', 'root'], + count: 108, + }, + }, + cursor: { + value: 'root', + tiebreaker: null, + }, + }, + { + node: { + _id: 'systemd-resolve', + user: { + id: ['102'], + name: 'systemd-resolve', + groupId: [], + groupName: [], + count: 4, + }, + }, + cursor: { + value: 'systemd-resolve', + tiebreaker: null, + }, + }, + ], + totalCount: 3, + pageInfo: { + activePage: 1, + fakeTotalCount: 3, + showMorePagesIndicator: true, + }, +}; diff --git a/x-pack/plugins/siem/public/components/page/network/users_table/translations.ts b/x-pack/plugins/siem/public/network/components/users_table/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/page/network/users_table/translations.ts rename to x-pack/plugins/siem/public/network/components/users_table/translations.ts diff --git a/x-pack/plugins/siem/public/containers/ip_overview/index.gql_query.ts b/x-pack/plugins/siem/public/network/containers/ip_overview/index.gql_query.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/ip_overview/index.gql_query.ts rename to x-pack/plugins/siem/public/network/containers/ip_overview/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/network/containers/ip_overview/index.tsx b/x-pack/plugins/siem/public/network/containers/ip_overview/index.tsx new file mode 100644 index 00000000000000..551ecebf2c05a7 --- /dev/null +++ b/x-pack/plugins/siem/public/network/containers/ip_overview/index.tsx @@ -0,0 +1,84 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect, ConnectedProps } from 'react-redux'; + +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { GetIpOverviewQuery, IpOverviewData } from '../../../graphql/types'; +import { inputsModel, inputsSelectors, State } from '../../../common/store'; +import { useUiSetting } from '../../../common/lib/kibana'; +import { createFilter, getDefaultFetchPolicy } from '../../../common/containers/helpers'; +import { QueryTemplateProps } from '../../../common/containers/query_template'; +import { networkModel } from '../../store'; +import { ipOverviewQuery } from './index.gql_query'; + +const ID = 'ipOverviewQuery'; + +export interface IpOverviewArgs { + id: string; + inspect: inputsModel.InspectQuery; + ipOverviewData: IpOverviewData; + loading: boolean; + refetch: inputsModel.Refetch; +} + +export interface IpOverviewProps extends QueryTemplateProps { + children: (args: IpOverviewArgs) => React.ReactNode; + type: networkModel.NetworkType; + ip: string; +} + +const IpOverviewComponentQuery = React.memo( + ({ id = ID, isInspected, children, filterQuery, skip, sourceId, ip }) => ( + + query={ipOverviewQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + skip={skip} + variables={{ + sourceId, + filterQuery: createFilter(filterQuery), + ip, + defaultIndex: useUiSetting(DEFAULT_INDEX_KEY), + inspect: isInspected, + }} + > + {({ data, loading, refetch }) => { + const init: IpOverviewData = { host: {} }; + const ipOverviewData: IpOverviewData = getOr(init, 'source.IpOverview', data); + return children({ + id, + inspect: getOr(null, 'source.IpOverview.inspect', data), + ipOverviewData, + loading, + refetch, + }); + }} + + ) +); + +IpOverviewComponentQuery.displayName = 'IpOverviewComponentQuery'; + +const makeMapStateToProps = () => { + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = ID }: IpOverviewProps) => { + const { isInspected } = getQuery(state, id); + return { + isInspected, + }; + }; + return mapStateToProps; +}; + +const connector = connect(makeMapStateToProps); + +type PropsFromRedux = ConnectedProps; + +export const IpOverviewQuery = connector(IpOverviewComponentQuery); diff --git a/x-pack/plugins/siem/public/containers/kpi_network/index.gql_query.ts b/x-pack/plugins/siem/public/network/containers/kpi_network/index.gql_query.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/kpi_network/index.gql_query.ts rename to x-pack/plugins/siem/public/network/containers/kpi_network/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/network/containers/kpi_network/index.tsx b/x-pack/plugins/siem/public/network/containers/kpi_network/index.tsx new file mode 100644 index 00000000000000..edba8b4c2e65ca --- /dev/null +++ b/x-pack/plugins/siem/public/network/containers/kpi_network/index.tsx @@ -0,0 +1,85 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect, ConnectedProps } from 'react-redux'; + +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { GetKpiNetworkQuery, KpiNetworkData } from '../../../graphql/types'; +import { inputsModel, inputsSelectors, State } from '../../../common/store'; +import { useUiSetting } from '../../../common/lib/kibana'; +import { createFilter, getDefaultFetchPolicy } from '../../../common/containers/helpers'; +import { QueryTemplateProps } from '../../../common/containers/query_template'; + +import { kpiNetworkQuery } from './index.gql_query'; + +const ID = 'kpiNetworkQuery'; + +export interface KpiNetworkArgs { + id: string; + inspect: inputsModel.InspectQuery; + kpiNetwork: KpiNetworkData; + loading: boolean; + refetch: inputsModel.Refetch; +} + +export interface KpiNetworkProps extends QueryTemplateProps { + children: (args: KpiNetworkArgs) => React.ReactNode; +} + +const KpiNetworkComponentQuery = React.memo( + ({ id = ID, children, filterQuery, isInspected, skip, sourceId, startDate, endDate }) => ( + + query={kpiNetworkQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + skip={skip} + variables={{ + sourceId, + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + filterQuery: createFilter(filterQuery), + defaultIndex: useUiSetting(DEFAULT_INDEX_KEY), + inspect: isInspected, + }} + > + {({ data, loading, refetch }) => { + const kpiNetwork = getOr({}, `source.KpiNetwork`, data); + return children({ + id, + inspect: getOr(null, 'source.KpiNetwork.inspect', data), + kpiNetwork, + loading, + refetch, + }); + }} + + ) +); + +KpiNetworkComponentQuery.displayName = 'KpiNetworkComponentQuery'; + +const makeMapStateToProps = () => { + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = ID }: KpiNetworkProps) => { + const { isInspected } = getQuery(state, id); + return { + isInspected, + }; + }; + return mapStateToProps; +}; + +const connector = connect(makeMapStateToProps); + +type PropsFromRedux = ConnectedProps; + +export const KpiNetworkQuery = connector(KpiNetworkComponentQuery); diff --git a/x-pack/plugins/siem/public/containers/network_dns/index.gql_query.ts b/x-pack/plugins/siem/public/network/containers/network_dns/index.gql_query.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/network_dns/index.gql_query.ts rename to x-pack/plugins/siem/public/network/containers/network_dns/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/network/containers/network_dns/index.tsx b/x-pack/plugins/siem/public/network/containers/network_dns/index.tsx new file mode 100644 index 00000000000000..2bae19ce89aec0 --- /dev/null +++ b/x-pack/plugins/siem/public/network/containers/network_dns/index.tsx @@ -0,0 +1,216 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; + +import { DocumentNode } from 'graphql'; +import { ScaleType } from '@elastic/charts'; +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { + GetNetworkDnsQuery, + NetworkDnsEdges, + NetworkDnsSortField, + PageInfoPaginated, + MatrixOverOrdinalHistogramData, +} from '../../../graphql/types'; +import { inputsModel, State, inputsSelectors } from '../../../common/store'; +import { withKibana, WithKibanaProps } from '../../../common/lib/kibana'; +import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; +import { createFilter, getDefaultFetchPolicy } from '../../../common/containers/helpers'; +import { + QueryTemplatePaginated, + QueryTemplatePaginatedProps, +} from '../../../common/containers/query_template_paginated'; +import { networkDnsQuery } from './index.gql_query'; +import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from '../../../common/store/constants'; +import { MatrixHistogram } from '../../../common/components/matrix_histogram'; +import { + MatrixHistogramOption, + GetSubTitle, +} from '../../../common/components/matrix_histogram/types'; +import { UpdateDateRange } from '../../../common/components/charts/common'; +import { SetQuery } from '../../../hosts/pages/navigation/types'; +import { networkModel, networkSelectors } from '../../store'; + +const ID = 'networkDnsQuery'; +export const HISTOGRAM_ID = 'networkDnsHistogramQuery'; +export interface NetworkDnsArgs { + id: string; + inspect: inputsModel.InspectQuery; + isInspected: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + networkDns: NetworkDnsEdges[]; + pageInfo: PageInfoPaginated; + refetch: inputsModel.Refetch; + stackByField?: string; + totalCount: number; + histogram: MatrixOverOrdinalHistogramData[]; +} + +export interface OwnProps extends QueryTemplatePaginatedProps { + children: (args: NetworkDnsArgs) => React.ReactNode; + type: networkModel.NetworkType; +} + +interface DnsHistogramOwnProps extends QueryTemplatePaginatedProps { + dataKey: string | string[]; + defaultStackByOption: MatrixHistogramOption; + errorMessage: string; + isDnsHistogram?: boolean; + query: DocumentNode; + scaleType: ScaleType; + setQuery: SetQuery; + showLegend?: boolean; + stackByOptions: MatrixHistogramOption[]; + subtitle?: string | GetSubTitle; + title: string; + type: networkModel.NetworkType; + updateDateRange: UpdateDateRange; + yTickFormatter?: (value: number) => string; +} + +export interface NetworkDnsComponentReduxProps { + activePage: number; + sort: NetworkDnsSortField; + isInspected: boolean; + isPtrIncluded: boolean; + limit: number; +} + +type NetworkDnsProps = OwnProps & NetworkDnsComponentReduxProps & WithKibanaProps; + +export class NetworkDnsComponentQuery extends QueryTemplatePaginated< + NetworkDnsProps, + GetNetworkDnsQuery.Query, + GetNetworkDnsQuery.Variables +> { + public render() { + const { + activePage, + children, + sort, + endDate, + filterQuery, + id = ID, + isInspected, + isPtrIncluded, + kibana, + limit, + skip, + sourceId, + startDate, + } = this.props; + const variables: GetNetworkDnsQuery.Variables = { + defaultIndex: kibana.services.uiSettings.get(DEFAULT_INDEX_KEY), + filterQuery: createFilter(filterQuery), + inspect: isInspected, + isPtrIncluded, + pagination: generateTablePaginationOptions(activePage, limit), + sort, + sourceId, + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + }; + + return ( + + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + query={networkDnsQuery} + skip={skip} + variables={variables} + > + {({ data, loading, fetchMore, networkStatus, refetch }) => { + const networkDns = getOr([], `source.NetworkDns.edges`, data); + this.setFetchMore(fetchMore); + this.setFetchMoreOptions((newActivePage: number) => ({ + variables: { + pagination: generateTablePaginationOptions(newActivePage, limit), + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) { + return prev; + } + return { + ...fetchMoreResult, + source: { + ...fetchMoreResult.source, + NetworkDns: { + ...fetchMoreResult.source.NetworkDns, + edges: [...fetchMoreResult.source.NetworkDns.edges], + }, + }, + }; + }, + })); + const isLoading = this.isItAValidLoading(loading, variables, networkStatus); + return children({ + id, + inspect: getOr(null, 'source.NetworkDns.inspect', data), + isInspected, + loading: isLoading, + loadPage: this.wrappedLoadMore, + networkDns, + pageInfo: getOr({}, 'source.NetworkDns.pageInfo', data), + refetch: this.memoizedRefetchQuery(variables, limit, refetch), + totalCount: getOr(-1, 'source.NetworkDns.totalCount', data), + histogram: getOr(null, 'source.NetworkDns.histogram', data), + }); + }} + + ); + } +} + +const makeMapStateToProps = () => { + const getNetworkDnsSelector = networkSelectors.dnsSelector(); + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = ID }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + ...getNetworkDnsSelector(state), + isInspected, + id, + }; + }; + + return mapStateToProps; +}; + +const makeMapHistogramStateToProps = () => { + const getNetworkDnsSelector = networkSelectors.dnsSelector(); + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = HISTOGRAM_ID }: DnsHistogramOwnProps) => { + const { isInspected } = getQuery(state, id); + return { + ...getNetworkDnsSelector(state), + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + isInspected, + id, + }; + }; + + return mapStateToProps; +}; + +export const NetworkDnsQuery = compose>( + connect(makeMapStateToProps), + withKibana +)(NetworkDnsComponentQuery); + +export const NetworkDnsHistogramQuery = compose>( + connect(makeMapHistogramStateToProps), + withKibana +)(MatrixHistogram); diff --git a/x-pack/plugins/siem/public/containers/network_http/index.gql_query.ts b/x-pack/plugins/siem/public/network/containers/network_http/index.gql_query.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/network_http/index.gql_query.ts rename to x-pack/plugins/siem/public/network/containers/network_http/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/network/containers/network_http/index.tsx b/x-pack/plugins/siem/public/network/containers/network_http/index.tsx new file mode 100644 index 00000000000000..60845d452d69e4 --- /dev/null +++ b/x-pack/plugins/siem/public/network/containers/network_http/index.tsx @@ -0,0 +1,160 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; + +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { + GetNetworkHttpQuery, + NetworkHttpEdges, + NetworkHttpSortField, + PageInfoPaginated, +} from '../../../graphql/types'; +import { inputsModel, inputsSelectors, State } from '../../../common/store'; +import { withKibana, WithKibanaProps } from '../../../common/lib/kibana'; +import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; +import { createFilter, getDefaultFetchPolicy } from '../../../common/containers/helpers'; +import { + QueryTemplatePaginated, + QueryTemplatePaginatedProps, +} from '../../../common/containers/query_template_paginated'; +import { networkModel, networkSelectors } from '../../store'; +import { networkHttpQuery } from './index.gql_query'; + +const ID = 'networkHttpQuery'; + +export interface NetworkHttpArgs { + id: string; + ip?: string; + inspect: inputsModel.InspectQuery; + isInspected: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + networkHttp: NetworkHttpEdges[]; + pageInfo: PageInfoPaginated; + refetch: inputsModel.Refetch; + totalCount: number; +} + +export interface OwnProps extends QueryTemplatePaginatedProps { + children: (args: NetworkHttpArgs) => React.ReactNode; + ip?: string; + type: networkModel.NetworkType; +} + +export interface NetworkHttpComponentReduxProps { + activePage: number; + isInspected: boolean; + limit: number; + sort: NetworkHttpSortField; +} + +type NetworkHttpProps = OwnProps & NetworkHttpComponentReduxProps & WithKibanaProps; + +class NetworkHttpComponentQuery extends QueryTemplatePaginated< + NetworkHttpProps, + GetNetworkHttpQuery.Query, + GetNetworkHttpQuery.Variables +> { + public render() { + const { + activePage, + children, + endDate, + filterQuery, + id = ID, + ip, + isInspected, + kibana, + limit, + skip, + sourceId, + sort, + startDate, + } = this.props; + const variables: GetNetworkHttpQuery.Variables = { + defaultIndex: kibana.services.uiSettings.get(DEFAULT_INDEX_KEY), + filterQuery: createFilter(filterQuery), + inspect: isInspected, + ip, + pagination: generateTablePaginationOptions(activePage, limit), + sort, + sourceId, + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + }; + return ( + + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + query={networkHttpQuery} + skip={skip} + variables={variables} + > + {({ data, loading, fetchMore, networkStatus, refetch }) => { + const networkHttp = getOr([], `source.NetworkHttp.edges`, data); + this.setFetchMore(fetchMore); + this.setFetchMoreOptions((newActivePage: number) => ({ + variables: { + pagination: generateTablePaginationOptions(newActivePage, limit), + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) { + return prev; + } + return { + ...fetchMoreResult, + source: { + ...fetchMoreResult.source, + NetworkHttp: { + ...fetchMoreResult.source.NetworkHttp, + edges: [...fetchMoreResult.source.NetworkHttp.edges], + }, + }, + }; + }, + })); + const isLoading = this.isItAValidLoading(loading, variables, networkStatus); + return children({ + id, + inspect: getOr(null, 'source.NetworkHttp.inspect', data), + isInspected, + loading: isLoading, + loadPage: this.wrappedLoadMore, + networkHttp, + pageInfo: getOr({}, 'source.NetworkHttp.pageInfo', data), + refetch: this.memoizedRefetchQuery(variables, limit, refetch), + totalCount: getOr(-1, 'source.NetworkHttp.totalCount', data), + }); + }} + + ); + } +} + +const makeMapStateToProps = () => { + const getHttpSelector = networkSelectors.httpSelector(); + const getQuery = inputsSelectors.globalQueryByIdSelector(); + return (state: State, { id = ID, type }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + ...getHttpSelector(state, type), + isInspected, + }; + }; +}; + +export const NetworkHttpQuery = compose>( + connect(makeMapStateToProps), + withKibana +)(NetworkHttpComponentQuery); diff --git a/x-pack/plugins/siem/public/containers/network_top_countries/index.gql_query.ts b/x-pack/plugins/siem/public/network/containers/network_top_countries/index.gql_query.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/network_top_countries/index.gql_query.ts rename to x-pack/plugins/siem/public/network/containers/network_top_countries/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/network/containers/network_top_countries/index.tsx b/x-pack/plugins/siem/public/network/containers/network_top_countries/index.tsx new file mode 100644 index 00000000000000..b167cba460818f --- /dev/null +++ b/x-pack/plugins/siem/public/network/containers/network_top_countries/index.tsx @@ -0,0 +1,164 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; + +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { + FlowTargetSourceDest, + GetNetworkTopCountriesQuery, + NetworkTopCountriesEdges, + NetworkTopTablesSortField, + PageInfoPaginated, +} from '../../../graphql/types'; +import { inputsModel, inputsSelectors, State } from '../../../common/store'; +import { withKibana, WithKibanaProps } from '../../../common/lib/kibana'; +import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; +import { createFilter, getDefaultFetchPolicy } from '../../../common/containers/helpers'; +import { + QueryTemplatePaginated, + QueryTemplatePaginatedProps, +} from '../../../common/containers/query_template_paginated'; +import { networkTopCountriesQuery } from './index.gql_query'; +import { networkModel, networkSelectors } from '../../store'; + +const ID = 'networkTopCountriesQuery'; + +export interface NetworkTopCountriesArgs { + id: string; + ip?: string; + inspect: inputsModel.InspectQuery; + isInspected: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + networkTopCountries: NetworkTopCountriesEdges[]; + pageInfo: PageInfoPaginated; + refetch: inputsModel.Refetch; + totalCount: number; +} + +export interface OwnProps extends QueryTemplatePaginatedProps { + children: (args: NetworkTopCountriesArgs) => React.ReactNode; + flowTarget: FlowTargetSourceDest; + ip?: string; + type: networkModel.NetworkType; +} + +export interface NetworkTopCountriesComponentReduxProps { + activePage: number; + isInspected: boolean; + limit: number; + sort: NetworkTopTablesSortField; +} + +type NetworkTopCountriesProps = OwnProps & NetworkTopCountriesComponentReduxProps & WithKibanaProps; + +class NetworkTopCountriesComponentQuery extends QueryTemplatePaginated< + NetworkTopCountriesProps, + GetNetworkTopCountriesQuery.Query, + GetNetworkTopCountriesQuery.Variables +> { + public render() { + const { + activePage, + children, + endDate, + flowTarget, + filterQuery, + kibana, + id = `${ID}-${flowTarget}`, + ip, + isInspected, + limit, + skip, + sourceId, + startDate, + sort, + } = this.props; + const variables: GetNetworkTopCountriesQuery.Variables = { + defaultIndex: kibana.services.uiSettings.get(DEFAULT_INDEX_KEY), + filterQuery: createFilter(filterQuery), + flowTarget, + inspect: isInspected, + ip, + pagination: generateTablePaginationOptions(activePage, limit), + sort, + sourceId, + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + }; + return ( + + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + query={networkTopCountriesQuery} + skip={skip} + variables={variables} + > + {({ data, loading, fetchMore, networkStatus, refetch }) => { + const networkTopCountries = getOr([], `source.NetworkTopCountries.edges`, data); + this.setFetchMore(fetchMore); + this.setFetchMoreOptions((newActivePage: number) => ({ + variables: { + pagination: generateTablePaginationOptions(newActivePage, limit), + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) { + return prev; + } + return { + ...fetchMoreResult, + source: { + ...fetchMoreResult.source, + NetworkTopCountries: { + ...fetchMoreResult.source.NetworkTopCountries, + edges: [...fetchMoreResult.source.NetworkTopCountries.edges], + }, + }, + }; + }, + })); + const isLoading = this.isItAValidLoading(loading, variables, networkStatus); + return children({ + id, + inspect: getOr(null, 'source.NetworkTopCountries.inspect', data), + isInspected, + loading: isLoading, + loadPage: this.wrappedLoadMore, + networkTopCountries, + pageInfo: getOr({}, 'source.NetworkTopCountries.pageInfo', data), + refetch: this.memoizedRefetchQuery(variables, limit, refetch), + totalCount: getOr(-1, 'source.NetworkTopCountries.totalCount', data), + }); + }} + + ); + } +} + +const makeMapStateToProps = () => { + const getTopCountriesSelector = networkSelectors.topCountriesSelector(); + const getQuery = inputsSelectors.globalQueryByIdSelector(); + return (state: State, { flowTarget, id = `${ID}-${flowTarget}`, type }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + ...getTopCountriesSelector(state, type, flowTarget), + isInspected, + }; + }; +}; + +export const NetworkTopCountriesQuery = compose>( + connect(makeMapStateToProps), + withKibana +)(NetworkTopCountriesComponentQuery); diff --git a/x-pack/plugins/siem/public/containers/network_top_n_flow/index.gql_query.ts b/x-pack/plugins/siem/public/network/containers/network_top_n_flow/index.gql_query.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/network_top_n_flow/index.gql_query.ts rename to x-pack/plugins/siem/public/network/containers/network_top_n_flow/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/network/containers/network_top_n_flow/index.tsx b/x-pack/plugins/siem/public/network/containers/network_top_n_flow/index.tsx new file mode 100644 index 00000000000000..770574b0813c1c --- /dev/null +++ b/x-pack/plugins/siem/public/network/containers/network_top_n_flow/index.tsx @@ -0,0 +1,164 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; + +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { + FlowTargetSourceDest, + GetNetworkTopNFlowQuery, + NetworkTopNFlowEdges, + NetworkTopTablesSortField, + PageInfoPaginated, +} from '../../../graphql/types'; +import { withKibana, WithKibanaProps } from '../../../common/lib/kibana'; +import { inputsModel, inputsSelectors, State } from '../../../common/store'; +import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; +import { createFilter, getDefaultFetchPolicy } from '../../../common/containers/helpers'; +import { + QueryTemplatePaginated, + QueryTemplatePaginatedProps, +} from '../../../common/containers/query_template_paginated'; +import { networkTopNFlowQuery } from './index.gql_query'; +import { networkModel, networkSelectors } from '../../store'; + +const ID = 'networkTopNFlowQuery'; + +export interface NetworkTopNFlowArgs { + id: string; + ip?: string; + inspect: inputsModel.InspectQuery; + isInspected: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + networkTopNFlow: NetworkTopNFlowEdges[]; + pageInfo: PageInfoPaginated; + refetch: inputsModel.Refetch; + totalCount: number; +} + +export interface OwnProps extends QueryTemplatePaginatedProps { + children: (args: NetworkTopNFlowArgs) => React.ReactNode; + flowTarget: FlowTargetSourceDest; + ip?: string; + type: networkModel.NetworkType; +} + +export interface NetworkTopNFlowComponentReduxProps { + activePage: number; + isInspected: boolean; + limit: number; + sort: NetworkTopTablesSortField; +} + +type NetworkTopNFlowProps = OwnProps & NetworkTopNFlowComponentReduxProps & WithKibanaProps; + +class NetworkTopNFlowComponentQuery extends QueryTemplatePaginated< + NetworkTopNFlowProps, + GetNetworkTopNFlowQuery.Query, + GetNetworkTopNFlowQuery.Variables +> { + public render() { + const { + activePage, + children, + endDate, + flowTarget, + filterQuery, + kibana, + id = `${ID}-${flowTarget}`, + ip, + isInspected, + limit, + skip, + sourceId, + startDate, + sort, + } = this.props; + const variables: GetNetworkTopNFlowQuery.Variables = { + defaultIndex: kibana.services.uiSettings.get(DEFAULT_INDEX_KEY), + filterQuery: createFilter(filterQuery), + flowTarget, + inspect: isInspected, + ip, + pagination: generateTablePaginationOptions(activePage, limit), + sort, + sourceId, + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + }; + return ( + + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + query={networkTopNFlowQuery} + skip={skip} + variables={variables} + > + {({ data, loading, fetchMore, networkStatus, refetch }) => { + const networkTopNFlow = getOr([], `source.NetworkTopNFlow.edges`, data); + this.setFetchMore(fetchMore); + this.setFetchMoreOptions((newActivePage: number) => ({ + variables: { + pagination: generateTablePaginationOptions(newActivePage, limit), + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) { + return prev; + } + return { + ...fetchMoreResult, + source: { + ...fetchMoreResult.source, + NetworkTopNFlow: { + ...fetchMoreResult.source.NetworkTopNFlow, + edges: [...fetchMoreResult.source.NetworkTopNFlow.edges], + }, + }, + }; + }, + })); + const isLoading = this.isItAValidLoading(loading, variables, networkStatus); + return children({ + id, + inspect: getOr(null, 'source.NetworkTopNFlow.inspect', data), + isInspected, + loading: isLoading, + loadPage: this.wrappedLoadMore, + networkTopNFlow, + pageInfo: getOr({}, 'source.NetworkTopNFlow.pageInfo', data), + refetch: this.memoizedRefetchQuery(variables, limit, refetch), + totalCount: getOr(-1, 'source.NetworkTopNFlow.totalCount', data), + }); + }} + + ); + } +} + +const makeMapStateToProps = () => { + const getTopNFlowSelector = networkSelectors.topNFlowSelector(); + const getQuery = inputsSelectors.globalQueryByIdSelector(); + return (state: State, { flowTarget, id = `${ID}-${flowTarget}`, type }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + ...getTopNFlowSelector(state, type, flowTarget), + isInspected, + }; + }; +}; + +export const NetworkTopNFlowQuery = compose>( + connect(makeMapStateToProps), + withKibana +)(NetworkTopNFlowComponentQuery); diff --git a/x-pack/plugins/siem/public/containers/tls/index.gql_query.ts b/x-pack/plugins/siem/public/network/containers/tls/index.gql_query.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/tls/index.gql_query.ts rename to x-pack/plugins/siem/public/network/containers/tls/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/network/containers/tls/index.tsx b/x-pack/plugins/siem/public/network/containers/tls/index.tsx new file mode 100644 index 00000000000000..a50f2a131b75b8 --- /dev/null +++ b/x-pack/plugins/siem/public/network/containers/tls/index.tsx @@ -0,0 +1,163 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect } from 'react-redux'; +import { compose } from 'redux'; + +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { + PageInfoPaginated, + TlsEdges, + TlsSortField, + GetTlsQuery, + FlowTargetSourceDest, +} from '../../../graphql/types'; +import { inputsModel, State, inputsSelectors } from '../../../common/store'; +import { withKibana, WithKibanaProps } from '../../../common/lib/kibana'; +import { createFilter, getDefaultFetchPolicy } from '../../../common/containers/helpers'; +import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; +import { + QueryTemplatePaginated, + QueryTemplatePaginatedProps, +} from '../../../common/containers/query_template_paginated'; +import { networkModel, networkSelectors } from '../../store'; +import { tlsQuery } from './index.gql_query'; + +const ID = 'tlsQuery'; + +export interface TlsArgs { + id: string; + inspect: inputsModel.InspectQuery; + isInspected: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + pageInfo: PageInfoPaginated; + refetch: inputsModel.Refetch; + tls: TlsEdges[]; + totalCount: number; +} + +export interface OwnProps extends QueryTemplatePaginatedProps { + children: (args: TlsArgs) => React.ReactNode; + flowTarget: FlowTargetSourceDest; + ip: string; + type: networkModel.NetworkType; +} + +export interface TlsComponentReduxProps { + activePage: number; + isInspected: boolean; + limit: number; + sort: TlsSortField; +} + +type TlsProps = OwnProps & TlsComponentReduxProps & WithKibanaProps; + +class TlsComponentQuery extends QueryTemplatePaginated< + TlsProps, + GetTlsQuery.Query, + GetTlsQuery.Variables +> { + public render() { + const { + activePage, + children, + endDate, + filterQuery, + flowTarget, + id = ID, + ip, + isInspected, + kibana, + limit, + skip, + sourceId, + startDate, + sort, + } = this.props; + const variables: GetTlsQuery.Variables = { + defaultIndex: kibana.services.uiSettings.get(DEFAULT_INDEX_KEY), + filterQuery: createFilter(filterQuery), + flowTarget, + inspect: isInspected, + ip, + pagination: generateTablePaginationOptions(activePage, limit), + sort, + sourceId, + timerange: { + interval: '12h', + from: startDate ? startDate : 0, + to: endDate ? endDate : Date.now(), + }, + }; + return ( + + query={tlsQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + skip={skip} + variables={variables} + > + {({ data, loading, fetchMore, networkStatus, refetch }) => { + const tls = getOr([], 'source.Tls.edges', data); + this.setFetchMore(fetchMore); + this.setFetchMoreOptions((newActivePage: number) => ({ + variables: { + pagination: generateTablePaginationOptions(newActivePage, limit), + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) { + return prev; + } + return { + ...fetchMoreResult, + source: { + ...fetchMoreResult.source, + Tls: { + ...fetchMoreResult.source.Tls, + edges: [...fetchMoreResult.source.Tls.edges], + }, + }, + }; + }, + })); + const isLoading = this.isItAValidLoading(loading, variables, networkStatus); + return children({ + id, + inspect: getOr(null, 'source.Tls.inspect', data), + isInspected, + loading: isLoading, + loadPage: this.wrappedLoadMore, + pageInfo: getOr({}, 'source.Tls.pageInfo', data), + refetch: this.memoizedRefetchQuery(variables, limit, refetch), + tls, + totalCount: getOr(-1, 'source.Tls.totalCount', data), + }); + }} + + ); + } +} + +const makeMapStateToProps = () => { + const getTlsSelector = networkSelectors.tlsSelector(); + const getQuery = inputsSelectors.globalQueryByIdSelector(); + return (state: State, { flowTarget, id = ID, type }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + ...getTlsSelector(state, type, flowTarget), + isInspected, + }; + }; +}; + +export const TlsQuery = compose>( + connect(makeMapStateToProps), + withKibana +)(TlsComponentQuery); diff --git a/x-pack/plugins/siem/public/containers/users/index.gql_query.ts b/x-pack/plugins/siem/public/network/containers/users/index.gql_query.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/users/index.gql_query.ts rename to x-pack/plugins/siem/public/network/containers/users/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/network/containers/users/index.tsx b/x-pack/plugins/siem/public/network/containers/users/index.tsx new file mode 100644 index 00000000000000..efbeb3eb005426 --- /dev/null +++ b/x-pack/plugins/siem/public/network/containers/users/index.tsx @@ -0,0 +1,157 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect, ConnectedProps } from 'react-redux'; +import { compose } from 'redux'; + +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { GetUsersQuery, FlowTarget, PageInfoPaginated, UsersEdges } from '../../../graphql/types'; +import { inputsModel, State, inputsSelectors } from '../../../common/store'; +import { withKibana, WithKibanaProps } from '../../../common/lib/kibana'; +import { createFilter, getDefaultFetchPolicy } from '../../../common/containers/helpers'; +import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; +import { + QueryTemplatePaginated, + QueryTemplatePaginatedProps, +} from '../../../common/containers/query_template_paginated'; +import { networkModel, networkSelectors } from '../../store'; + +import { usersQuery } from './index.gql_query'; + +const ID = 'usersQuery'; + +export interface UsersArgs { + id: string; + inspect: inputsModel.InspectQuery; + isInspected: boolean; + loading: boolean; + loadPage: (newActivePage: number) => void; + pageInfo: PageInfoPaginated; + refetch: inputsModel.Refetch; + totalCount: number; + users: UsersEdges[]; +} + +export interface OwnProps extends QueryTemplatePaginatedProps { + children: (args: UsersArgs) => React.ReactNode; + flowTarget: FlowTarget; + ip: string; + type: networkModel.NetworkType; +} + +type UsersProps = OwnProps & PropsFromRedux & WithKibanaProps; + +class UsersComponentQuery extends QueryTemplatePaginated< + UsersProps, + GetUsersQuery.Query, + GetUsersQuery.Variables +> { + public render() { + const { + activePage, + children, + endDate, + filterQuery, + flowTarget, + id = ID, + ip, + isInspected, + kibana, + limit, + skip, + sourceId, + startDate, + sort, + } = this.props; + const variables: GetUsersQuery.Variables = { + defaultIndex: kibana.services.uiSettings.get(DEFAULT_INDEX_KEY), + filterQuery: createFilter(filterQuery), + flowTarget, + inspect: isInspected, + ip, + pagination: generateTablePaginationOptions(activePage, limit), + sort, + sourceId, + timerange: { + interval: '12h', + from: startDate!, + to: endDate!, + }, + }; + return ( + + query={usersQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + skip={skip} + variables={variables} + > + {({ data, loading, fetchMore, networkStatus, refetch }) => { + const users = getOr([], `source.Users.edges`, data); + this.setFetchMore(fetchMore); + this.setFetchMoreOptions((newActivePage: number) => ({ + variables: { + pagination: generateTablePaginationOptions(newActivePage, limit), + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) { + return prev; + } + return { + ...fetchMoreResult, + source: { + ...fetchMoreResult.source, + Users: { + ...fetchMoreResult.source.Users, + edges: [...fetchMoreResult.source.Users.edges], + }, + }, + }; + }, + })); + const isLoading = this.isItAValidLoading(loading, variables, networkStatus); + return children({ + id, + inspect: getOr(null, 'source.Users.inspect', data), + isInspected, + loading: isLoading, + loadPage: this.wrappedLoadMore, + pageInfo: getOr({}, 'source.Users.pageInfo', data), + refetch: this.memoizedRefetchQuery(variables, limit, refetch), + totalCount: getOr(-1, 'source.Users.totalCount', data), + users, + }); + }} + + ); + } +} + +const makeMapStateToProps = () => { + const getUsersSelector = networkSelectors.usersSelector(); + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = ID }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + ...getUsersSelector(state), + isInspected, + }; + }; + + return mapStateToProps; +}; + +const connector = connect(makeMapStateToProps); + +type PropsFromRedux = ConnectedProps; + +export const UsersQuery = compose>( + connector, + withKibana +)(UsersComponentQuery); diff --git a/x-pack/plugins/siem/public/network/index.ts b/x-pack/plugins/siem/public/network/index.ts new file mode 100644 index 00000000000000..6590e5ee5161c1 --- /dev/null +++ b/x-pack/plugins/siem/public/network/index.ts @@ -0,0 +1,23 @@ +/* + * 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 { SecuritySubPluginWithStore } from '../app/types'; +import { getNetworkRoutes } from './routes'; +import { initialNetworkState, networkReducer, NetworkState } from './store'; + +export class Network { + public setup() {} + + public start(): SecuritySubPluginWithStore<'network', NetworkState> { + return { + routes: getNetworkRoutes(), + store: { + initialState: { network: initialNetworkState }, + reducer: { network: networkReducer }, + }, + }; + } +} diff --git a/x-pack/plugins/siem/public/network/pages/index.tsx b/x-pack/plugins/siem/public/network/pages/index.tsx new file mode 100644 index 00000000000000..c6f13c118c3093 --- /dev/null +++ b/x-pack/plugins/siem/public/network/pages/index.tsx @@ -0,0 +1,100 @@ +/* + * 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, { useMemo } from 'react'; +import { Redirect, Route, Switch, RouteComponentProps } from 'react-router-dom'; + +import { useMlCapabilities } from '../../common/components/ml_popover/hooks/use_ml_capabilities'; +import { hasMlUserPermissions } from '../../../common/machine_learning/has_ml_user_permissions'; +import { FlowTarget } from '../../graphql/types'; + +import { IPDetails } from './ip_details'; +import { Network } from './network'; +import { GlobalTime } from '../../common/containers/global_time'; +import { SiemPageName } from '../../app/types'; +import { getNetworkRoutePath } from './navigation'; +import { NetworkRouteType } from './navigation/types'; + +type Props = Partial> & { url: string }; + +const networkPagePath = `/:pageName(${SiemPageName.network})`; +const ipDetailsPageBasePath = `${networkPagePath}/ip/:detailName`; + +const NetworkContainerComponent: React.FC = () => { + const capabilities = useMlCapabilities(); + const capabilitiesFetched = capabilities.capabilitiesFetched; + const userHasMlUserPermissions = useMemo(() => hasMlUserPermissions(capabilities), [ + capabilities, + ]); + const networkRoutePath = useMemo( + () => getNetworkRoutePath(networkPagePath, capabilitiesFetched, userHasMlUserPermissions), + [capabilitiesFetched, userHasMlUserPermissions] + ); + + return ( + + {({ to, from, setQuery, deleteQuery, isInitializing }) => ( + + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + + )} + + ); +}; + +export const NetworkContainer = React.memo(NetworkContainerComponent); diff --git a/x-pack/plugins/siem/public/pages/network/ip_details/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/network/pages/ip_details/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/pages/network/ip_details/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/network/pages/ip_details/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/network/pages/ip_details/index.test.tsx b/x-pack/plugins/siem/public/network/pages/ip_details/index.test.tsx new file mode 100644 index 00000000000000..79af38f0cf8873 --- /dev/null +++ b/x-pack/plugins/siem/public/network/pages/ip_details/index.test.tsx @@ -0,0 +1,159 @@ +/* + * 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 { cloneDeep } from 'lodash/fp'; +import React from 'react'; +import { Router } from 'react-router-dom'; +import { MockedProvider } from 'react-apollo/test-utils'; +import { ActionCreator } from 'typescript-fsa'; + +import '../../../common/mock/match_media'; + +import { mocksSource } from '../../../common/containers/source/mock'; +import { FlowTarget } from '../../../graphql/types'; +import { + apolloClientObservable, + mockGlobalState, + TestProviders, + SUB_PLUGINS_REDUCER, +} from '../../../common/mock'; +import { useMountAppended } from '../../../common/utils/use_mount_appended'; +import { createStore, State } from '../../../common/store'; +import { InputsModelId } from '../../../common/store/inputs/constants'; +import { IPDetailsComponent, IPDetails } from './index'; + +type Action = 'PUSH' | 'POP' | 'REPLACE'; +const pop: Action = 'POP'; + +type GlobalWithFetch = NodeJS.Global & { fetch: jest.Mock }; + +// Test will fail because we will to need to mock some core services to make the test work +// For now let's forget about SiemSearchBar and QueryBar +jest.mock('../../../common/components/search_bar', () => ({ + SiemSearchBar: () => null, +})); +jest.mock('../../../common/components/query_bar', () => ({ + QueryBar: () => null, +})); + +let localSource: Array<{ + request: {}; + result: { + data: { + source: { + status: { + indicesExist: boolean; + }; + }; + }; + }; +}>; + +const getMockHistory = (ip: string) => ({ + length: 2, + location: { + pathname: `/network/ip/${ip}`, + search: '', + state: '', + hash: '', + }, + action: pop, + push: jest.fn(), + replace: jest.fn(), + go: jest.fn(), + goBack: jest.fn(), + goForward: jest.fn(), + block: jest.fn(), + createHref: jest.fn(), + listen: jest.fn(), +}); + +const to = new Date('2018-03-23T18:49:23.132Z').valueOf(); +const from = new Date('2018-03-24T03:33:52.253Z').valueOf(); +const getMockProps = (ip: string) => ({ + to, + from, + isInitializing: false, + setQuery: jest.fn(), + query: { query: 'coolQueryhuh?', language: 'keury' }, + filters: [], + flowTarget: FlowTarget.source, + history: getMockHistory(ip), + location: { + pathname: `/network/ip/${ip}`, + search: '', + state: '', + hash: '', + }, + detailName: ip, + match: { params: { detailName: ip, search: '' }, isExact: true, path: '', url: '' }, + setAbsoluteRangeDatePicker: (jest.fn() as unknown) as ActionCreator<{ + id: InputsModelId; + from: number; + to: number; + }>, + setIpDetailsTablesActivePageToZero: (jest.fn() as unknown) as ActionCreator, +}); + +describe('Ip Details', () => { + const mount = useMountAppended(); + + beforeAll(() => { + (global as GlobalWithFetch).fetch = jest.fn().mockImplementationOnce(() => + Promise.resolve({ + ok: true, + json: () => { + return null; + }, + }) + ); + }); + + afterAll(() => { + delete (global as GlobalWithFetch).fetch; + }); + + const state: State = mockGlobalState; + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + + beforeEach(() => { + store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + localSource = cloneDeep(mocksSource); + }); + + test('it renders', () => { + const wrapper = shallow(); + expect(wrapper.find('[data-test-subj="ip-details-page"]').exists()).toBe(true); + }); + + test('it matches the snapshot', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + test('it renders ipv6 headline', async () => { + localSource[0].result.data.source.status.indicesExist = true; + const ip = 'fe80--24ce-f7ff-fede-a571'; + const wrapper = mount( + + + + + + + + ); + // Why => https://github.com/apollographql/react-apollo/issues/1711 + await new Promise(resolve => setTimeout(resolve)); + wrapper.update(); + expect( + wrapper + .find('[data-test-subj="ip-details-headline"] [data-test-subj="header-page-title"]') + .text() + ).toEqual('fe80::24ce:f7ff:fede:a571'); + }); +}); diff --git a/x-pack/plugins/siem/public/network/pages/ip_details/index.tsx b/x-pack/plugins/siem/public/network/pages/ip_details/index.tsx new file mode 100644 index 00000000000000..9ae09d6c6cec74 --- /dev/null +++ b/x-pack/plugins/siem/public/network/pages/ip_details/index.tsx @@ -0,0 +1,301 @@ +/* + * 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 { EuiHorizontalRule, EuiSpacer, EuiFlexItem } from '@elastic/eui'; +import React, { useCallback, useEffect } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import { StickyContainer } from 'react-sticky'; + +import { FiltersGlobal } from '../../../common/components/filters_global'; +import { HeaderPage } from '../../../common/components/header_page'; +import { LastEventTime } from '../../../common/components/last_event_time'; +import { AnomalyTableProvider } from '../../../common/components/ml/anomaly/anomaly_table_provider'; +import { networkToCriteria } from '../../../common/components/ml/criteria/network_to_criteria'; +import { scoreIntervalToDateTime } from '../../../common/components/ml/score/score_interval_to_datetime'; +import { AnomaliesNetworkTable } from '../../../common/components/ml/tables/anomalies_network_table'; +import { manageQuery } from '../../../common/components/page/manage_query'; +import { FlowTargetSelectConnected } from '../../components/flow_target_select_connected'; +import { IpOverview } from '../../components/ip_overview'; +import { SiemSearchBar } from '../../../common/components/search_bar'; +import { WrapperPage } from '../../../common/components/wrapper_page'; +import { IpOverviewQuery } from '../../containers/ip_overview'; +import { + indicesExistOrDataTemporarilyUnavailable, + WithSource, +} from '../../../common/containers/source'; +import { FlowTargetSourceDest, LastEventIndexKey } from '../../../graphql/types'; +import { useKibana } from '../../../common/lib/kibana'; +import { decodeIpv6 } from '../../../common/lib/helpers'; +import { convertToBuildEsQuery } from '../../../common/lib/keury'; +import { ConditionalFlexGroup } from '../../pages/navigation/conditional_flex_group'; +import { State, inputsSelectors } from '../../../common/store'; +import { setAbsoluteRangeDatePicker as dispatchAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions'; +import { setIpDetailsTablesActivePageToZero as dispatchIpDetailsTablesActivePageToZero } from '../../store/actions'; +import { SpyRoute } from '../../../common/utils/route/spy_routes'; +import { NetworkEmptyPage } from '../network_empty_page'; +import { NetworkHttpQueryTable } from './network_http_query_table'; +import { NetworkTopCountriesQueryTable } from './network_top_countries_query_table'; +import { NetworkTopNFlowQueryTable } from './network_top_n_flow_query_table'; +import { TlsQueryTable } from './tls_query_table'; +import { IPDetailsComponentProps } from './types'; +import { UsersQueryTable } from './users_query_table'; +import { AnomaliesQueryTabBody } from '../../../common/containers/anomalies/anomalies_query_tab_body'; +import { esQuery } from '../../../../../../../src/plugins/data/public'; +import { networkModel } from '../../store'; +export { getBreadcrumbs } from './utils'; + +const IpOverviewManage = manageQuery(IpOverview); + +export const IPDetailsComponent: React.FC = ({ + detailName, + filters, + flowTarget, + from, + isInitializing, + query, + setAbsoluteRangeDatePicker, + setIpDetailsTablesActivePageToZero, + setQuery, + to, +}) => { + const type = networkModel.NetworkType.details; + const narrowDateRange = useCallback( + (score, interval) => { + const fromTo = scoreIntervalToDateTime(score, interval); + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }); + }, + [setAbsoluteRangeDatePicker] + ); + const kibana = useKibana(); + + useEffect(() => { + setIpDetailsTablesActivePageToZero(); + }, [detailName, setIpDetailsTablesActivePageToZero]); + + return ( + <> + + {({ indicesExist, indexPattern }) => { + const ip = decodeIpv6(detailName); + const filterQuery = convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + indexPattern, + queries: [query], + filters, + }); + + return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( + + + + + + + } + title={ip} + > + + + + + {({ id, inspect, ipOverviewData, loading, refetch }) => ( + + {({ isLoadingAnomaliesData, anomaliesData }) => ( + + )} + + )} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) : ( + + + + + + ); + }} + + + + + ); +}; +IPDetailsComponent.displayName = 'IPDetailsComponent'; + +const makeMapStateToProps = () => { + const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); + const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); + + return (state: State) => ({ + query: getGlobalQuerySelector(state), + filters: getGlobalFiltersQuerySelector(state), + }); +}; + +const mapDispatchToProps = { + setAbsoluteRangeDatePicker: dispatchAbsoluteRangeDatePicker, + setIpDetailsTablesActivePageToZero: dispatchIpDetailsTablesActivePageToZero, +}; + +export const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const IPDetails = connector(React.memo(IPDetailsComponent)); diff --git a/x-pack/plugins/siem/public/pages/network/ip_details/network_http_query_table.tsx b/x-pack/plugins/siem/public/network/pages/ip_details/network_http_query_table.tsx similarity index 86% rename from x-pack/plugins/siem/public/pages/network/ip_details/network_http_query_table.tsx rename to x-pack/plugins/siem/public/network/pages/ip_details/network_http_query_table.tsx index d071cc67414c9f..551de698cfa08d 100644 --- a/x-pack/plugins/siem/public/pages/network/ip_details/network_http_query_table.tsx +++ b/x-pack/plugins/siem/public/network/pages/ip_details/network_http_query_table.tsx @@ -6,10 +6,10 @@ import React from 'react'; import { getOr } from 'lodash/fp'; -import { manageQuery } from '../../../components/page/manage_query'; +import { manageQuery } from '../../../common/components/page/manage_query'; import { OwnProps } from './types'; -import { NetworkHttpQuery } from '../../../containers/network_http'; -import { NetworkHttpTable } from '../../../components/page/network/network_http_table'; +import { NetworkHttpQuery } from '../../containers/network_http'; +import { NetworkHttpTable } from '../../components/network_http_table'; const NetworkHttpTableManage = manageQuery(NetworkHttpTable); diff --git a/x-pack/plugins/siem/public/pages/network/ip_details/network_top_countries_query_table.tsx b/x-pack/plugins/siem/public/network/pages/ip_details/network_top_countries_query_table.tsx similarity index 86% rename from x-pack/plugins/siem/public/pages/network/ip_details/network_top_countries_query_table.tsx rename to x-pack/plugins/siem/public/network/pages/ip_details/network_top_countries_query_table.tsx index 8f3505009b9a51..6bc80ef1a6aae4 100644 --- a/x-pack/plugins/siem/public/pages/network/ip_details/network_top_countries_query_table.tsx +++ b/x-pack/plugins/siem/public/network/pages/ip_details/network_top_countries_query_table.tsx @@ -6,10 +6,10 @@ import React from 'react'; import { getOr } from 'lodash/fp'; -import { manageQuery } from '../../../components/page/manage_query'; +import { manageQuery } from '../../../common/components/page/manage_query'; import { NetworkWithIndexComponentsQueryTableProps } from './types'; -import { NetworkTopCountriesQuery } from '../../../containers/network_top_countries'; -import { NetworkTopCountriesTable } from '../../../components/page/network/network_top_countries_table'; +import { NetworkTopCountriesQuery } from '../../containers/network_top_countries'; +import { NetworkTopCountriesTable } from '../../components/network_top_countries_table'; const NetworkTopCountriesTableManage = manageQuery(NetworkTopCountriesTable); diff --git a/x-pack/plugins/siem/public/pages/network/ip_details/network_top_n_flow_query_table.tsx b/x-pack/plugins/siem/public/network/pages/ip_details/network_top_n_flow_query_table.tsx similarity index 86% rename from x-pack/plugins/siem/public/pages/network/ip_details/network_top_n_flow_query_table.tsx rename to x-pack/plugins/siem/public/network/pages/ip_details/network_top_n_flow_query_table.tsx index 06ae3160415d96..158b4057a7d5e3 100644 --- a/x-pack/plugins/siem/public/pages/network/ip_details/network_top_n_flow_query_table.tsx +++ b/x-pack/plugins/siem/public/network/pages/ip_details/network_top_n_flow_query_table.tsx @@ -6,9 +6,9 @@ import { getOr } from 'lodash/fp'; import React from 'react'; -import { manageQuery } from '../../../components/page/manage_query'; -import { NetworkTopNFlowTable } from '../../../components/page/network/network_top_n_flow_table'; -import { NetworkTopNFlowQuery } from '../../../containers/network_top_n_flow'; +import { manageQuery } from '../../../common/components/page/manage_query'; +import { NetworkTopNFlowTable } from '../../components/network_top_n_flow_table'; +import { NetworkTopNFlowQuery } from '../../containers/network_top_n_flow'; import { NetworkWithIndexComponentsQueryTableProps } from './types'; const NetworkTopNFlowTableManage = manageQuery(NetworkTopNFlowTable); diff --git a/x-pack/plugins/siem/public/pages/network/ip_details/tls_query_table.tsx b/x-pack/plugins/siem/public/network/pages/ip_details/tls_query_table.tsx similarity index 87% rename from x-pack/plugins/siem/public/pages/network/ip_details/tls_query_table.tsx rename to x-pack/plugins/siem/public/network/pages/ip_details/tls_query_table.tsx index ad3ffb8cb0a578..f0c3628af78d85 100644 --- a/x-pack/plugins/siem/public/pages/network/ip_details/tls_query_table.tsx +++ b/x-pack/plugins/siem/public/network/pages/ip_details/tls_query_table.tsx @@ -6,9 +6,9 @@ import { getOr } from 'lodash/fp'; import React from 'react'; -import { manageQuery } from '../../../components/page/manage_query'; -import { TlsTable } from '../../../components/page/network/tls_table'; -import { TlsQuery } from '../../../containers/tls'; +import { manageQuery } from '../../../common/components/page/manage_query'; +import { TlsTable } from '../../components/tls_table'; +import { TlsQuery } from '../../containers/tls'; import { TlsQueryTableComponentProps } from './types'; const TlsTableManage = manageQuery(TlsTable); diff --git a/x-pack/plugins/siem/public/network/pages/ip_details/types.ts b/x-pack/plugins/siem/public/network/pages/ip_details/types.ts new file mode 100644 index 00000000000000..02d83208884b41 --- /dev/null +++ b/x-pack/plugins/siem/public/network/pages/ip_details/types.ts @@ -0,0 +1,53 @@ +/* + * 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 { IIndexPattern } from 'src/plugins/data/public'; + +import { ESTermQuery } from '../../../../common/typed_json'; +import { NetworkType } from '../../store/model'; +import { InspectQuery, Refetch } from '../../../common/store/inputs/model'; +import { FlowTarget, FlowTargetSourceDest } from '../../../graphql/types'; +import { GlobalTimeArgs } from '../../../common/containers/global_time'; + +export const type = NetworkType.details; + +export type IPDetailsComponentProps = GlobalTimeArgs & { + detailName: string; + flowTarget: FlowTarget; +}; + +export interface OwnProps { + type: NetworkType; + startDate: number; + endDate: number; + filterQuery: string | ESTermQuery; + ip: string; + skip: boolean; + setQuery: ({ + id, + inspect, + loading, + refetch, + }: { + id: string; + inspect: InspectQuery | null; + loading: boolean; + refetch: Refetch; + }) => void; +} + +export type NetworkComponentsQueryProps = OwnProps & { + flowTarget: FlowTarget; +}; + +export type TlsQueryTableComponentProps = OwnProps & { + flowTarget: FlowTargetSourceDest; +}; + +export type NetworkWithIndexComponentsQueryTableProps = OwnProps & { + flowTarget: FlowTargetSourceDest; + indexPattern: IIndexPattern; +}; diff --git a/x-pack/plugins/siem/public/pages/network/ip_details/users_query_table.tsx b/x-pack/plugins/siem/public/network/pages/ip_details/users_query_table.tsx similarity index 87% rename from x-pack/plugins/siem/public/pages/network/ip_details/users_query_table.tsx rename to x-pack/plugins/siem/public/network/pages/ip_details/users_query_table.tsx index d2f6102e86595b..4071790b4208ae 100644 --- a/x-pack/plugins/siem/public/pages/network/ip_details/users_query_table.tsx +++ b/x-pack/plugins/siem/public/network/pages/ip_details/users_query_table.tsx @@ -6,10 +6,10 @@ import React from 'react'; import { getOr } from 'lodash/fp'; -import { manageQuery } from '../../../components/page/manage_query'; -import { UsersQuery } from '../../../containers/users'; +import { manageQuery } from '../../../common/components/page/manage_query'; +import { UsersQuery } from '../../containers/users'; import { NetworkComponentsQueryProps } from './types'; -import { UsersTable } from '../../../components/page/network/users_table'; +import { UsersTable } from '../../components/users_table'; const UsersTableManage = manageQuery(UsersTable); diff --git a/x-pack/plugins/siem/public/network/pages/ip_details/utils.ts b/x-pack/plugins/siem/public/network/pages/ip_details/utils.ts new file mode 100644 index 00000000000000..b1f986f20778f2 --- /dev/null +++ b/x-pack/plugins/siem/public/network/pages/ip_details/utils.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get, isEmpty } from 'lodash/fp'; + +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { ChromeBreadcrumb } from '../../../../../../../src/core/public'; +import { decodeIpv6 } from '../../../common/lib/helpers'; +import { + getNetworkUrl, + getIPDetailsUrl, +} from '../../../common/components/link_to/redirect_to_network'; +import { networkModel } from '../../store'; +import * as i18n from '../translations'; +import { NetworkRouteType } from '../navigation/types'; +import { NetworkRouteSpyState } from '../../../common/utils/route/types'; + +export const type = networkModel.NetworkType.details; +const TabNameMappedToI18nKey: Record = { + [NetworkRouteType.alerts]: i18n.NAVIGATION_ALERTS_TITLE, + [NetworkRouteType.anomalies]: i18n.NAVIGATION_ANOMALIES_TITLE, + [NetworkRouteType.flows]: i18n.NAVIGATION_FLOWS_TITLE, + [NetworkRouteType.dns]: i18n.NAVIGATION_DNS_TITLE, + [NetworkRouteType.http]: i18n.NAVIGATION_HTTP_TITLE, + [NetworkRouteType.tls]: i18n.NAVIGATION_TLS_TITLE, +}; + +export const getBreadcrumbs = ( + params: NetworkRouteSpyState, + search: string[] +): ChromeBreadcrumb[] => { + let breadcrumb = [ + { + text: i18n.PAGE_TITLE, + href: `${getNetworkUrl()}${!isEmpty(search[0]) ? search[0] : ''}`, + }, + ]; + if (params.detailName != null) { + breadcrumb = [ + ...breadcrumb, + { + text: decodeIpv6(params.detailName), + href: `${getIPDetailsUrl(params.detailName, params.flowTarget)}${ + !isEmpty(search[1]) ? search[1] : '' + }`, + }, + ]; + } + + const tabName = get('tabName', params); + if (!tabName) return breadcrumb; + + breadcrumb = [ + ...breadcrumb, + { + text: TabNameMappedToI18nKey[tabName], + href: '', + }, + ]; + return breadcrumb; +}; diff --git a/x-pack/plugins/siem/public/network/pages/navigation/alerts_query_tab_body.tsx b/x-pack/plugins/siem/public/network/pages/navigation/alerts_query_tab_body.tsx new file mode 100644 index 00000000000000..c5f59e751ca9ae --- /dev/null +++ b/x-pack/plugins/siem/public/network/pages/navigation/alerts_query_tab_body.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { Filter } from '../../../../../../../src/plugins/data/common/es_query'; +import { AlertsView } from '../../../common/components/alerts_viewer'; +import { NetworkComponentQueryProps } from './types'; + +export const filterNetworkData: Filter[] = [ + { + query: { + bool: { + filter: [ + { + bool: { + should: [ + { + bool: { + should: [ + { + exists: { + field: 'source.ip', + }, + }, + ], + minimum_should_match: 1, + }, + }, + { + bool: { + should: [ + { + exists: { + field: 'destination.ip', + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + minimum_should_match: 1, + }, + }, + ], + }, + }, + meta: { + alias: '', + disabled: false, + key: 'bool', + negate: false, + type: 'custom', + value: + '{"bool":{"filter":[{"bool":{"should":[{"bool":{"should":[{"exists":{"field": "source.ip"}}],"minimum_should_match":1}},{"bool":{"should":[{"exists":{"field": "destination.ip"}}],"minimum_should_match":1}}],"minimum_should_match":1}}]}}', + }, + }, +]; + +export const NetworkAlertsQueryTabBody = React.memo((alertsProps: NetworkComponentQueryProps) => ( + +)); + +NetworkAlertsQueryTabBody.displayName = 'NetworkAlertsQueryTabBody'; diff --git a/x-pack/plugins/siem/public/pages/network/navigation/conditional_flex_group.tsx b/x-pack/plugins/siem/public/network/pages/navigation/conditional_flex_group.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/network/navigation/conditional_flex_group.tsx rename to x-pack/plugins/siem/public/network/pages/navigation/conditional_flex_group.tsx diff --git a/x-pack/plugins/siem/public/pages/network/navigation/countries_query_tab_body.tsx b/x-pack/plugins/siem/public/network/pages/navigation/countries_query_tab_body.tsx similarity index 85% rename from x-pack/plugins/siem/public/pages/network/navigation/countries_query_tab_body.tsx rename to x-pack/plugins/siem/public/network/pages/navigation/countries_query_tab_body.tsx index 6ddd3bbec3a322..0c569952458e47 100644 --- a/x-pack/plugins/siem/public/pages/network/navigation/countries_query_tab_body.tsx +++ b/x-pack/plugins/siem/public/network/pages/navigation/countries_query_tab_body.tsx @@ -7,10 +7,10 @@ import React from 'react'; import { getOr } from 'lodash/fp'; -import { NetworkTopCountriesTable } from '../../../components/page/network'; -import { NetworkTopCountriesQuery } from '../../../containers/network_top_countries'; -import { networkModel } from '../../../store'; -import { manageQuery } from '../../../components/page/manage_query'; +import { NetworkTopCountriesTable } from '../../components/network_top_countries_table'; +import { NetworkTopCountriesQuery } from '../../containers/network_top_countries'; +import { networkModel } from '../../store'; +import { manageQuery } from '../../../common/components/page/manage_query'; import { IPsQueryTabBodyProps as CountriesQueryTabBodyProps } from './types'; diff --git a/x-pack/plugins/siem/public/pages/network/navigation/dns_query_tab_body.tsx b/x-pack/plugins/siem/public/network/pages/navigation/dns_query_tab_body.tsx similarity index 88% rename from x-pack/plugins/siem/public/pages/network/navigation/dns_query_tab_body.tsx rename to x-pack/plugins/siem/public/network/pages/navigation/dns_query_tab_body.tsx index fe456afcc7189b..acabdd1d3608eb 100644 --- a/x-pack/plugins/siem/public/pages/network/navigation/dns_query_tab_body.tsx +++ b/x-pack/plugins/siem/public/network/pages/navigation/dns_query_tab_body.tsx @@ -7,19 +7,19 @@ import React, { useEffect, useCallback, useMemo } from 'react'; import { getOr } from 'lodash/fp'; -import { NetworkDnsTable } from '../../../components/page/network/network_dns_table'; -import { NetworkDnsQuery, HISTOGRAM_ID } from '../../../containers/network_dns'; -import { manageQuery } from '../../../components/page/manage_query'; +import { NetworkDnsTable } from '../../components/network_dns_table'; +import { NetworkDnsQuery, HISTOGRAM_ID } from '../../containers/network_dns'; +import { manageQuery } from '../../../common/components/page/manage_query'; import { NetworkComponentQueryProps } from './types'; -import { networkModel } from '../../../store'; +import { networkModel } from '../../store'; import { MatrixHistogramOption, MatrixHisrogramConfigs, -} from '../../../components/matrix_histogram/types'; +} from '../../../common/components/matrix_histogram/types'; import * as i18n from '../translations'; -import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; +import { MatrixHistogramContainer } from '../../../common/components/matrix_histogram'; import { HistogramType } from '../../../graphql/types'; const NetworkDnsTableManage = manageQuery(NetworkDnsTable); diff --git a/x-pack/plugins/siem/public/pages/network/navigation/http_query_tab_body.tsx b/x-pack/plugins/siem/public/network/pages/navigation/http_query_tab_body.tsx similarity index 84% rename from x-pack/plugins/siem/public/pages/network/navigation/http_query_tab_body.tsx rename to x-pack/plugins/siem/public/network/pages/navigation/http_query_tab_body.tsx index 639a14d354ced4..7e0c4025d6cac1 100644 --- a/x-pack/plugins/siem/public/pages/network/navigation/http_query_tab_body.tsx +++ b/x-pack/plugins/siem/public/network/pages/navigation/http_query_tab_body.tsx @@ -7,10 +7,10 @@ import React from 'react'; import { getOr } from 'lodash/fp'; -import { NetworkHttpTable } from '../../../components/page/network'; -import { NetworkHttpQuery } from '../../../containers/network_http'; -import { networkModel } from '../../../store'; -import { manageQuery } from '../../../components/page/manage_query'; +import { NetworkHttpTable } from '../../components/network_http_table'; +import { NetworkHttpQuery } from '../../containers/network_http'; +import { networkModel } from '../../store'; +import { manageQuery } from '../../../common/components/page/manage_query'; import { HttpQueryTabBodyProps } from './types'; diff --git a/x-pack/plugins/siem/public/pages/network/navigation/index.ts b/x-pack/plugins/siem/public/network/pages/navigation/index.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/network/navigation/index.ts rename to x-pack/plugins/siem/public/network/pages/navigation/index.ts diff --git a/x-pack/plugins/siem/public/pages/network/navigation/ips_query_tab_body.tsx b/x-pack/plugins/siem/public/network/pages/navigation/ips_query_tab_body.tsx similarity index 84% rename from x-pack/plugins/siem/public/pages/network/navigation/ips_query_tab_body.tsx rename to x-pack/plugins/siem/public/network/pages/navigation/ips_query_tab_body.tsx index c4391ba2ec90ac..a9f4d504847a07 100644 --- a/x-pack/plugins/siem/public/pages/network/navigation/ips_query_tab_body.tsx +++ b/x-pack/plugins/siem/public/network/pages/navigation/ips_query_tab_body.tsx @@ -7,10 +7,10 @@ import React from 'react'; import { getOr } from 'lodash/fp'; -import { NetworkTopNFlowTable } from '../../../components/page/network'; -import { NetworkTopNFlowQuery } from '../../../containers/network_top_n_flow'; -import { networkModel } from '../../../store'; -import { manageQuery } from '../../../components/page/manage_query'; +import { NetworkTopNFlowTable } from '../../components/network_top_n_flow_table'; +import { NetworkTopNFlowQuery } from '../../containers/network_top_n_flow'; +import { networkModel } from '../../store'; +import { manageQuery } from '../../../common/components/page/manage_query'; import { IPsQueryTabBodyProps } from './types'; diff --git a/x-pack/plugins/siem/public/pages/network/navigation/nav_tabs.tsx b/x-pack/plugins/siem/public/network/pages/navigation/nav_tabs.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/network/navigation/nav_tabs.tsx rename to x-pack/plugins/siem/public/network/pages/navigation/nav_tabs.tsx diff --git a/x-pack/plugins/siem/public/pages/network/navigation/network_routes.tsx b/x-pack/plugins/siem/public/network/pages/navigation/network_routes.tsx similarity index 90% rename from x-pack/plugins/siem/public/pages/network/navigation/network_routes.tsx rename to x-pack/plugins/siem/public/network/pages/navigation/network_routes.tsx index fc8b632f87c597..08ed0d9769be8a 100644 --- a/x-pack/plugins/siem/public/pages/network/navigation/network_routes.tsx +++ b/x-pack/plugins/siem/public/network/pages/navigation/network_routes.tsx @@ -9,20 +9,20 @@ import { Route, Switch } from 'react-router-dom'; import { EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { FlowTargetSourceDest } from '../../../graphql/types'; -import { scoreIntervalToDateTime } from '../../../components/ml/score/score_interval_to_datetime'; +import { scoreIntervalToDateTime } from '../../../common/components/ml/score/score_interval_to_datetime'; import { IPsQueryTabBody } from './ips_query_tab_body'; import { CountriesQueryTabBody } from './countries_query_tab_body'; import { HttpQueryTabBody } from './http_query_tab_body'; -import { AnomaliesQueryTabBody } from '../../../containers/anomalies/anomalies_query_tab_body'; -import { AnomaliesNetworkTable } from '../../../components/ml/tables/anomalies_network_table'; +import { AnomaliesQueryTabBody } from '../../../common/containers/anomalies/anomalies_query_tab_body'; +import { AnomaliesNetworkTable } from '../../../common/components/ml/tables/anomalies_network_table'; import { DnsQueryTabBody } from './dns_query_tab_body'; import { ConditionalFlexGroup } from './conditional_flex_group'; import { NetworkRoutesProps, NetworkRouteType } from './types'; import { TlsQueryTabBody } from './tls_query_tab_body'; -import { Anomaly } from '../../../components/ml/types'; +import { Anomaly } from '../../../common/components/ml/types'; import { NetworkAlertsQueryTabBody } from './alerts_query_tab_body'; -import { UpdateDateRange } from '../../../components/charts/common'; +import { UpdateDateRange } from '../../../common/components/charts/common'; export const NetworkRoutes = React.memo( ({ diff --git a/x-pack/plugins/siem/public/pages/network/navigation/network_routes_loading.tsx b/x-pack/plugins/siem/public/network/pages/navigation/network_routes_loading.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/network/navigation/network_routes_loading.tsx rename to x-pack/plugins/siem/public/network/pages/navigation/network_routes_loading.tsx diff --git a/x-pack/plugins/siem/public/pages/network/navigation/tls_query_tab_body.tsx b/x-pack/plugins/siem/public/network/pages/navigation/tls_query_tab_body.tsx similarity index 87% rename from x-pack/plugins/siem/public/pages/network/navigation/tls_query_tab_body.tsx rename to x-pack/plugins/siem/public/network/pages/navigation/tls_query_tab_body.tsx index 0adfec203e0a61..00da5496e54405 100644 --- a/x-pack/plugins/siem/public/pages/network/navigation/tls_query_tab_body.tsx +++ b/x-pack/plugins/siem/public/network/pages/navigation/tls_query_tab_body.tsx @@ -5,9 +5,9 @@ */ import React from 'react'; import { getOr } from 'lodash/fp'; -import { manageQuery } from '../../../components/page/manage_query'; -import { TlsQuery } from '../../../containers/tls'; -import { TlsTable } from '../../../components/page/network/tls_table'; +import { manageQuery } from '../../../common/components/page/manage_query'; +import { TlsQuery } from '../../../network/containers/tls'; +import { TlsTable } from '../../components/tls_table'; import { TlsQueryTabBodyProps } from './types'; const TlsTableManage = manageQuery(TlsTable); diff --git a/x-pack/plugins/siem/public/network/pages/navigation/types.ts b/x-pack/plugins/siem/public/network/pages/navigation/types.ts new file mode 100644 index 00000000000000..0f48aad57b3a84 --- /dev/null +++ b/x-pack/plugins/siem/public/network/pages/navigation/types.ts @@ -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 { ESTermQuery } from '../../../../common/typed_json'; +import { IIndexPattern } from '../../../../../../../src/plugins/data/common'; + +import { NavTab } from '../../../common/components/navigation/types'; +import { FlowTargetSourceDest } from '../../../graphql/types'; +import { networkModel } from '../../store'; +import { GlobalTimeArgs } from '../../../common/containers/global_time'; + +import { SetAbsoluteRangeDatePicker } from '../types'; +import { NarrowDateRange } from '../../../common/components/ml/types'; + +interface QueryTabBodyProps extends Pick { + skip: boolean; + type: networkModel.NetworkType; + startDate: number; + endDate: number; + filterQuery?: string | ESTermQuery; + narrowDateRange?: NarrowDateRange; +} + +export type NetworkComponentQueryProps = QueryTabBodyProps; + +export type IPsQueryTabBodyProps = QueryTabBodyProps & { + indexPattern: IIndexPattern; + flowTarget: FlowTargetSourceDest; +}; + +export type TlsQueryTabBodyProps = QueryTabBodyProps & { + flowTarget: FlowTargetSourceDest; + ip?: string; +}; + +export type HttpQueryTabBodyProps = QueryTabBodyProps & { + ip?: string; +}; + +export type NetworkRoutesProps = GlobalTimeArgs & { + networkPagePath: string; + type: networkModel.NetworkType; + filterQuery?: string | ESTermQuery; + indexPattern: IIndexPattern; + setAbsoluteRangeDatePicker: SetAbsoluteRangeDatePicker; +}; + +export type KeyNetworkNavTabWithoutMlPermission = NetworkRouteType.dns & + NetworkRouteType.flows & + NetworkRouteType.http & + NetworkRouteType.tls & + NetworkRouteType.alerts; + +type KeyNetworkNavTabWithMlPermission = KeyNetworkNavTabWithoutMlPermission & + NetworkRouteType.anomalies; + +type KeyNetworkNavTab = KeyNetworkNavTabWithoutMlPermission | KeyNetworkNavTabWithMlPermission; + +export type NetworkNavTab = Record; + +export enum NetworkRouteType { + flows = 'flows', + dns = 'dns', + anomalies = 'anomalies', + tls = 'tls', + http = 'http', + alerts = 'alerts', +} + +export type GetNetworkRoutePath = ( + pagePath: string, + capabilitiesFetched: boolean, + hasMlUserPermission: boolean +) => string; diff --git a/x-pack/plugins/siem/public/pages/network/navigation/utils.ts b/x-pack/plugins/siem/public/network/pages/navigation/utils.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/network/navigation/utils.ts rename to x-pack/plugins/siem/public/network/pages/navigation/utils.ts diff --git a/x-pack/plugins/siem/public/pages/network/network.test.tsx b/x-pack/plugins/siem/public/network/pages/network.test.tsx similarity index 89% rename from x-pack/plugins/siem/public/pages/network/network.test.tsx rename to x-pack/plugins/siem/public/network/pages/network.test.tsx index 300cb83c4ce753..1a8313db92b61d 100644 --- a/x-pack/plugins/siem/public/pages/network/network.test.tsx +++ b/x-pack/plugins/siem/public/network/pages/network.test.tsx @@ -10,21 +10,27 @@ import React from 'react'; import { Router } from 'react-router-dom'; import { MockedProvider } from 'react-apollo/test-utils'; -import '../../mock/match_media'; +import '../../common/mock/match_media'; import { Filter } from '../../../../../../src/plugins/data/common/es_query'; -import { mocksSource } from '../../containers/source/mock'; -import { TestProviders, mockGlobalState, apolloClientObservable } from '../../mock'; -import { State, createStore } from '../../store'; -import { inputsActions } from '../../store/inputs'; +import { mocksSource } from '../../common/containers/source/mock'; +import { + TestProviders, + mockGlobalState, + apolloClientObservable, + SUB_PLUGINS_REDUCER, +} from '../../common/mock'; +import { State, createStore } from '../../common/store'; +import { inputsActions } from '../../common/store/inputs'; + import { Network } from './network'; import { NetworkRoutes } from './navigation'; // Test will fail because we will to need to mock some core services to make the test work // For now let's forget about SiemSearchBar and QueryBar -jest.mock('../../components/search_bar', () => ({ +jest.mock('../../common/components/search_bar', () => ({ SiemSearchBar: () => null, })); -jest.mock('../../components/query_bar', () => ({ +jest.mock('../../common/components/query_bar', () => ({ QueryBar: () => null, })); @@ -149,7 +155,7 @@ describe('rendering - rendering', () => { ]; localSource[0].result.data.source.status.indicesExist = true; const myState: State = mockGlobalState; - const myStore = createStore(myState, apolloClientObservable); + const myStore = createStore(myState, SUB_PLUGINS_REDUCER, apolloClientObservable); const wrapper = mount( diff --git a/x-pack/plugins/siem/public/network/pages/network.tsx b/x-pack/plugins/siem/public/network/pages/network.tsx new file mode 100644 index 00000000000000..2f7a97ed3d19ef --- /dev/null +++ b/x-pack/plugins/siem/public/network/pages/network.tsx @@ -0,0 +1,203 @@ +/* + * 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 { EuiSpacer } from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import { useParams } from 'react-router-dom'; +import { StickyContainer } from 'react-sticky'; + +import { esQuery } from '../../../../../../src/plugins/data/public'; +import { UpdateDateRange } from '../../common/components/charts/common'; +import { EmbeddedMap } from '../components/embeddables/embedded_map'; +import { FiltersGlobal } from '../../common/components/filters_global'; +import { HeaderPage } from '../../common/components/header_page'; +import { LastEventTime } from '../../common/components/last_event_time'; +import { SiemNavigation } from '../../common/components/navigation'; +import { manageQuery } from '../../common/components/page/manage_query'; +import { KpiNetworkComponent } from '..//components/kpi_network'; +import { SiemSearchBar } from '../../common/components/search_bar'; +import { WrapperPage } from '../../common/components/wrapper_page'; +import { KpiNetworkQuery } from '../../network/containers/kpi_network'; +import { + indicesExistOrDataTemporarilyUnavailable, + WithSource, +} from '../../common/containers/source'; +import { LastEventIndexKey } from '../../graphql/types'; +import { useKibana } from '../../common/lib/kibana'; +import { convertToBuildEsQuery } from '../../common/lib/keury'; +import { State, inputsSelectors } from '../../common/store'; +import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../common/store/inputs/actions'; +import { SpyRoute } from '../../common/utils/route/spy_routes'; +import { networkModel } from '../store'; +import { navTabsNetwork, NetworkRoutes, NetworkRoutesLoading } from './navigation'; +import { filterNetworkData } from './navigation/alerts_query_tab_body'; +import { NetworkEmptyPage } from './network_empty_page'; +import * as i18n from './translations'; +import { NetworkComponentProps } from './types'; +import { NetworkRouteType } from './navigation/types'; + +const KpiNetworkComponentManage = manageQuery(KpiNetworkComponent); +const sourceId = 'default'; + +const NetworkComponent = React.memo( + ({ + filters, + query, + setAbsoluteRangeDatePicker, + networkPagePath, + to, + from, + setQuery, + isInitializing, + hasMlUserPermissions, + capabilitiesFetched, + }) => { + const kibana = useKibana(); + const { tabName } = useParams(); + + const tabsFilters = useMemo(() => { + if (tabName === NetworkRouteType.alerts) { + return filters.length > 0 ? [...filters, ...filterNetworkData] : filterNetworkData; + } + return filters; + }, [tabName, filters]); + + const narrowDateRange = useCallback( + ({ x }) => { + if (!x) { + return; + } + const [min, max] = x; + setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); + }, + [setAbsoluteRangeDatePicker] + ); + + return ( + <> + + {({ indicesExist, indexPattern }) => { + const filterQuery = convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + indexPattern, + queries: [query], + filters, + }); + const tabsFilterQuery = convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + indexPattern, + queries: [query], + filters: tabsFilters, + }); + + return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( + + + + + + + } + title={i18n.PAGE_TITLE} + /> + + + + + + + {({ kpiNetwork, loading, id, inspect, refetch }) => ( + + )} + + + {capabilitiesFetched && !isInitializing ? ( + <> + + + + + + + + + ) : ( + + )} + + + + + ) : ( + + + + + ); + }} + + + + + ); + } +); +NetworkComponent.displayName = 'NetworkComponent'; + +const makeMapStateToProps = () => { + const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); + const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); + const mapStateToProps = (state: State) => ({ + query: getGlobalQuerySelector(state), + filters: getGlobalFiltersQuerySelector(state), + }); + return mapStateToProps; +}; + +const mapDispatchToProps = { + setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker, +}; + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const Network = connector(NetworkComponent); diff --git a/x-pack/plugins/siem/public/pages/network/network_empty_page.tsx b/x-pack/plugins/siem/public/network/pages/network_empty_page.tsx similarity index 85% rename from x-pack/plugins/siem/public/pages/network/network_empty_page.tsx rename to x-pack/plugins/siem/public/network/pages/network_empty_page.tsx index 22db00400bf8a2..0dbcddd5d28721 100644 --- a/x-pack/plugins/siem/public/pages/network/network_empty_page.tsx +++ b/x-pack/plugins/siem/public/network/pages/network_empty_page.tsx @@ -6,9 +6,9 @@ import React from 'react'; -import { useKibana } from '../../lib/kibana'; -import { EmptyPage } from '../../components/empty_page'; -import * as i18n from '../common/translations'; +import { useKibana } from '../../common/lib/kibana'; +import { EmptyPage } from '../../common/components/empty_page'; +import * as i18n from '../../common/translations'; export const NetworkEmptyPage = React.memo(() => { const { http, docLinks } = useKibana().services; diff --git a/x-pack/plugins/siem/public/pages/network/translations.ts b/x-pack/plugins/siem/public/network/pages/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/network/translations.ts rename to x-pack/plugins/siem/public/network/pages/translations.ts diff --git a/x-pack/plugins/siem/public/network/pages/types.ts b/x-pack/plugins/siem/public/network/pages/types.ts new file mode 100644 index 00000000000000..e4170ee4b908b2 --- /dev/null +++ b/x-pack/plugins/siem/public/network/pages/types.ts @@ -0,0 +1,23 @@ +/* + * 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 { RouteComponentProps } from 'react-router-dom'; +import { ActionCreator } from 'typescript-fsa'; +import { InputsModelId } from '../../common/store/inputs/constants'; +import { GlobalTimeArgs } from '../../common/containers/global_time'; + +export type SetAbsoluteRangeDatePicker = ActionCreator<{ + id: InputsModelId; + from: number; + to: number; +}>; + +export type NetworkComponentProps = Partial> & + GlobalTimeArgs & { + networkPagePath: string; + hasMlUserPermissions: boolean; + capabilitiesFetched: boolean; + }; diff --git a/x-pack/plugins/siem/public/network/routes.tsx b/x-pack/plugins/siem/public/network/routes.tsx new file mode 100644 index 00000000000000..6f3fd28ec53b7b --- /dev/null +++ b/x-pack/plugins/siem/public/network/routes.tsx @@ -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. + */ + +import React from 'react'; +import { Route } from 'react-router-dom'; + +import { NetworkContainer } from './pages'; +import { SiemPageName } from '../app/types'; + +export const getNetworkRoutes = () => [ + } + />, +]; diff --git a/x-pack/plugins/siem/public/network/store/actions.ts b/x-pack/plugins/siem/public/network/store/actions.ts new file mode 100644 index 00000000000000..2a9766f9592224 --- /dev/null +++ b/x-pack/plugins/siem/public/network/store/actions.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import actionCreatorFactory from 'typescript-fsa'; +import { networkModel } from '.'; + +const actionCreator = actionCreatorFactory('x-pack/siem/local/network'); + +export const updateNetworkTable = actionCreator<{ + networkType: networkModel.NetworkType; + tableType: networkModel.NetworkTableType | networkModel.IpDetailsTableType; + updates: networkModel.TableUpdates; +}>('UPDATE_NETWORK_TABLE'); + +export const setIpDetailsTablesActivePageToZero = actionCreator( + 'SET_IP_DETAILS_TABLES_ACTIVE_PAGE_TO_ZERO' +); + +export const setNetworkTablesActivePageToZero = actionCreator( + 'SET_NETWORK_TABLES_ACTIVE_PAGE_TO_ZERO' +); diff --git a/x-pack/plugins/siem/public/network/store/helpers.test.ts b/x-pack/plugins/siem/public/network/store/helpers.test.ts new file mode 100644 index 00000000000000..a3a2a9b7f5393a --- /dev/null +++ b/x-pack/plugins/siem/public/network/store/helpers.test.ts @@ -0,0 +1,248 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + Direction, + FlowTarget, + NetworkDnsFields, + NetworkTopTablesFields, + TlsFields, + UsersFields, +} from '../../graphql/types'; +import { DEFAULT_TABLE_LIMIT } from '../../common/store/constants'; +import { NetworkModel, NetworkTableType, IpDetailsTableType, NetworkType } from './model'; +import { setNetworkQueriesActivePageToZero } from './helpers'; + +export const mockNetworkState: NetworkModel = { + page: { + queries: { + [NetworkTableType.topCountriesSource]: { + activePage: 7, + limit: DEFAULT_TABLE_LIMIT, + sort: { + field: NetworkTopTablesFields.bytes_out, + direction: Direction.desc, + }, + }, + [NetworkTableType.topCountriesDestination]: { + activePage: 3, + limit: DEFAULT_TABLE_LIMIT, + sort: { + field: NetworkTopTablesFields.bytes_out, + direction: Direction.desc, + }, + }, + [NetworkTableType.topNFlowSource]: { + activePage: 7, + limit: DEFAULT_TABLE_LIMIT, + sort: { + field: NetworkTopTablesFields.bytes_out, + direction: Direction.desc, + }, + }, + [NetworkTableType.topNFlowDestination]: { + activePage: 3, + limit: DEFAULT_TABLE_LIMIT, + sort: { + field: NetworkTopTablesFields.bytes_out, + direction: Direction.desc, + }, + }, + [NetworkTableType.dns]: { + activePage: 5, + limit: DEFAULT_TABLE_LIMIT, + sort: { + field: NetworkDnsFields.uniqueDomains, + direction: Direction.desc, + }, + isPtrIncluded: false, + }, + [NetworkTableType.tls]: { + activePage: 2, + limit: DEFAULT_TABLE_LIMIT, + sort: { + field: TlsFields._id, + direction: Direction.desc, + }, + }, + [NetworkTableType.http]: { + activePage: 0, + limit: DEFAULT_TABLE_LIMIT, + sort: { direction: Direction.desc }, + }, + [NetworkTableType.alerts]: { + activePage: 0, + limit: DEFAULT_TABLE_LIMIT, + }, + }, + }, + details: { + queries: { + [IpDetailsTableType.topCountriesSource]: { + activePage: 7, + limit: DEFAULT_TABLE_LIMIT, + sort: { + field: NetworkTopTablesFields.bytes_out, + direction: Direction.desc, + }, + }, + [IpDetailsTableType.topCountriesDestination]: { + activePage: 3, + limit: DEFAULT_TABLE_LIMIT, + sort: { + field: NetworkTopTablesFields.bytes_out, + direction: Direction.desc, + }, + }, + [IpDetailsTableType.topNFlowSource]: { + activePage: 7, + limit: DEFAULT_TABLE_LIMIT, + sort: { + field: NetworkTopTablesFields.bytes_out, + direction: Direction.desc, + }, + }, + [IpDetailsTableType.topNFlowDestination]: { + activePage: 3, + limit: DEFAULT_TABLE_LIMIT, + sort: { + field: NetworkTopTablesFields.bytes_out, + direction: Direction.desc, + }, + }, + [IpDetailsTableType.tls]: { + activePage: 2, + limit: DEFAULT_TABLE_LIMIT, + sort: { + field: TlsFields._id, + direction: Direction.desc, + }, + }, + [IpDetailsTableType.users]: { + activePage: 6, + limit: DEFAULT_TABLE_LIMIT, + sort: { + field: UsersFields.name, + direction: Direction.asc, + }, + }, + [IpDetailsTableType.http]: { + activePage: 0, + limit: DEFAULT_TABLE_LIMIT, + sort: { direction: Direction.desc }, + }, + }, + flowTarget: FlowTarget.source, + }, +}; + +describe('Network redux store', () => { + describe('#setNetworkQueriesActivePageToZero', () => { + test('set activePage to zero for all queries in network page', () => { + expect(setNetworkQueriesActivePageToZero(mockNetworkState, NetworkType.page)).toEqual({ + [NetworkTableType.topNFlowSource]: { + activePage: 0, + limit: 10, + sort: { field: 'bytes_out', direction: 'desc' }, + }, + [NetworkTableType.topNFlowDestination]: { + activePage: 0, + limit: 10, + sort: { field: 'bytes_out', direction: 'desc' }, + }, + [NetworkTableType.dns]: { + activePage: 0, + limit: 10, + sort: { field: 'uniqueDomains', direction: 'desc' }, + isPtrIncluded: false, + }, + [NetworkTableType.http]: { + activePage: 0, + limit: 10, + sort: { + direction: 'desc', + }, + }, + [NetworkTableType.tls]: { + activePage: 0, + limit: 10, + sort: { + direction: 'desc', + field: '_id', + }, + }, + [NetworkTableType.topCountriesDestination]: { + activePage: 0, + limit: 10, + sort: { + direction: 'desc', + field: 'bytes_out', + }, + }, + [NetworkTableType.topCountriesSource]: { + activePage: 0, + limit: 10, + sort: { + direction: 'desc', + field: 'bytes_out', + }, + }, + [NetworkTableType.alerts]: { + activePage: 0, + limit: 10, + }, + }); + }); + + test('set activePage to zero for all queries in ip details ', () => { + expect(setNetworkQueriesActivePageToZero(mockNetworkState, NetworkType.details)).toEqual({ + [IpDetailsTableType.topNFlowSource]: { + activePage: 0, + limit: 10, + sort: { field: 'bytes_out', direction: 'desc' }, + }, + [IpDetailsTableType.topNFlowDestination]: { + activePage: 0, + limit: 10, + sort: { field: 'bytes_out', direction: 'desc' }, + }, + [IpDetailsTableType.topCountriesDestination]: { + activePage: 0, + limit: 10, + sort: { + direction: 'desc', + field: 'bytes_out', + }, + }, + [IpDetailsTableType.topCountriesSource]: { + activePage: 0, + limit: 10, + sort: { + direction: 'desc', + field: 'bytes_out', + }, + }, + [IpDetailsTableType.http]: { + activePage: 0, + limit: 10, + sort: { + direction: 'desc', + }, + }, + [IpDetailsTableType.tls]: { + activePage: 0, + limit: 10, + sort: { field: '_id', direction: 'desc' }, + }, + [IpDetailsTableType.users]: { + activePage: 0, + limit: 10, + sort: { field: 'name', direction: 'asc' }, + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/network/store/helpers.ts b/x-pack/plugins/siem/public/network/store/helpers.ts new file mode 100644 index 00000000000000..938de1dedf0b70 --- /dev/null +++ b/x-pack/plugins/siem/public/network/store/helpers.ts @@ -0,0 +1,93 @@ +/* + * 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 { + NetworkModel, + NetworkType, + NetworkTableType, + IpDetailsTableType, + NetworkQueries, + IpOverviewQueries, +} from './model'; +import { DEFAULT_TABLE_ACTIVE_PAGE } from '../../common/store/constants'; + +export const setNetworkPageQueriesActivePageToZero = (state: NetworkModel): NetworkQueries => ({ + ...state.page.queries, + [NetworkTableType.topCountriesSource]: { + ...state.page.queries[NetworkTableType.topCountriesSource], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, + [NetworkTableType.topCountriesDestination]: { + ...state.page.queries[NetworkTableType.topCountriesDestination], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, + [NetworkTableType.topNFlowSource]: { + ...state.page.queries[NetworkTableType.topNFlowSource], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, + [NetworkTableType.topNFlowDestination]: { + ...state.page.queries[NetworkTableType.topNFlowDestination], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, + [NetworkTableType.dns]: { + ...state.page.queries[NetworkTableType.dns], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, + [NetworkTableType.tls]: { + ...state.page.queries[NetworkTableType.tls], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, + [NetworkTableType.http]: { + ...state.page.queries[NetworkTableType.http], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, +}); + +export const setNetworkDetailsQueriesActivePageToZero = ( + state: NetworkModel +): IpOverviewQueries => ({ + ...state.details.queries, + [IpDetailsTableType.topCountriesSource]: { + ...state.details.queries[IpDetailsTableType.topCountriesSource], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, + [IpDetailsTableType.topCountriesDestination]: { + ...state.details.queries[IpDetailsTableType.topCountriesDestination], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, + [IpDetailsTableType.topNFlowSource]: { + ...state.details.queries[IpDetailsTableType.topNFlowSource], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, + [IpDetailsTableType.topNFlowDestination]: { + ...state.details.queries[IpDetailsTableType.topNFlowDestination], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, + [IpDetailsTableType.tls]: { + ...state.details.queries[IpDetailsTableType.tls], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, + [IpDetailsTableType.users]: { + ...state.details.queries[IpDetailsTableType.users], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, + [IpDetailsTableType.http]: { + ...state.details.queries[IpDetailsTableType.http], + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + }, +}); + +export const setNetworkQueriesActivePageToZero = ( + state: NetworkModel, + type: NetworkType +): NetworkQueries | IpOverviewQueries => { + if (type === NetworkType.page) { + return setNetworkPageQueriesActivePageToZero(state); + } else if (type === NetworkType.details) { + return setNetworkDetailsQueriesActivePageToZero(state); + } + throw new Error(`NetworkType ${type} is unknown`); +}; diff --git a/x-pack/plugins/siem/public/network/store/index.ts b/x-pack/plugins/siem/public/network/store/index.ts new file mode 100644 index 00000000000000..85268509ae9c58 --- /dev/null +++ b/x-pack/plugins/siem/public/network/store/index.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 { Reducer, AnyAction } from 'redux'; +import * as networkActions from './actions'; +import * as networkModel from './model'; +import * as networkSelectors from './selectors'; +import { NetworkState } from './reducer'; + +export { networkActions, networkModel, networkSelectors }; +export * from './reducer'; + +export interface NetworkPluginState { + network: NetworkState; +} + +export interface NetworkPluginReducer { + network: Reducer; +} diff --git a/x-pack/plugins/siem/public/store/network/model.ts b/x-pack/plugins/siem/public/network/store/model.ts similarity index 100% rename from x-pack/plugins/siem/public/store/network/model.ts rename to x-pack/plugins/siem/public/network/store/model.ts diff --git a/x-pack/plugins/siem/public/network/store/reducer.ts b/x-pack/plugins/siem/public/network/store/reducer.ts new file mode 100644 index 00000000000000..26458229da2968 --- /dev/null +++ b/x-pack/plugins/siem/public/network/store/reducer.ts @@ -0,0 +1,191 @@ +/* + * 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 { reducerWithInitialState } from 'typescript-fsa-reducers'; +import { get } from 'lodash/fp'; +import { + Direction, + FlowTarget, + NetworkDnsFields, + NetworkTopTablesFields, + TlsFields, + UsersFields, +} from '../../graphql/types'; +import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from '../../common/store/constants'; + +import { + setIpDetailsTablesActivePageToZero, + setNetworkTablesActivePageToZero, + updateNetworkTable, +} from './actions'; +import { + setNetworkDetailsQueriesActivePageToZero, + setNetworkPageQueriesActivePageToZero, +} from './helpers'; +import { IpDetailsTableType, NetworkModel, NetworkTableType } from './model'; + +export type NetworkState = NetworkModel; + +export const initialNetworkState: NetworkState = { + page: { + queries: { + [NetworkTableType.topNFlowSource]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + sort: { + field: NetworkTopTablesFields.bytes_out, + direction: Direction.desc, + }, + }, + [NetworkTableType.topNFlowDestination]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + sort: { + field: NetworkTopTablesFields.bytes_in, + direction: Direction.desc, + }, + }, + [NetworkTableType.dns]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + sort: { + field: NetworkDnsFields.uniqueDomains, + direction: Direction.desc, + }, + isPtrIncluded: false, + }, + [NetworkTableType.http]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + sort: { + direction: Direction.desc, + }, + }, + [NetworkTableType.tls]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + sort: { + field: TlsFields._id, + direction: Direction.desc, + }, + }, + [NetworkTableType.topCountriesSource]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + sort: { + field: NetworkTopTablesFields.bytes_out, + direction: Direction.desc, + }, + }, + [NetworkTableType.topCountriesDestination]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + sort: { + field: NetworkTopTablesFields.bytes_in, + direction: Direction.desc, + }, + }, + [NetworkTableType.alerts]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + }, + }, + }, + details: { + queries: { + [IpDetailsTableType.http]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + sort: { + direction: Direction.desc, + }, + }, + [IpDetailsTableType.topCountriesSource]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + sort: { + field: NetworkTopTablesFields.bytes_out, + direction: Direction.desc, + }, + }, + [IpDetailsTableType.topCountriesDestination]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + sort: { + field: NetworkTopTablesFields.bytes_in, + direction: Direction.desc, + }, + }, + [IpDetailsTableType.topNFlowSource]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + sort: { + field: NetworkTopTablesFields.bytes_out, + direction: Direction.desc, + }, + }, + [IpDetailsTableType.topNFlowDestination]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + sort: { + field: NetworkTopTablesFields.bytes_in, + direction: Direction.desc, + }, + }, + [IpDetailsTableType.tls]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + sort: { + field: TlsFields._id, + direction: Direction.desc, + }, + }, + [IpDetailsTableType.users]: { + activePage: DEFAULT_TABLE_ACTIVE_PAGE, + limit: DEFAULT_TABLE_LIMIT, + sort: { + field: UsersFields.name, + direction: Direction.asc, + }, + }, + }, + flowTarget: FlowTarget.source, + }, +}; + +export const networkReducer = reducerWithInitialState(initialNetworkState) + .case(updateNetworkTable, (state, { networkType, tableType, updates }) => ({ + ...state, + [networkType]: { + ...state[networkType], + queries: { + ...state[networkType].queries, + [tableType]: { + ...get([networkType, 'queries', tableType], state), + ...updates, + }, + }, + }, + })) + .case(setNetworkTablesActivePageToZero, state => ({ + ...state, + page: { + ...state.page, + queries: setNetworkPageQueriesActivePageToZero(state), + }, + details: { + ...state.details, + queries: setNetworkDetailsQueriesActivePageToZero(state), + }, + })) + .case(setIpDetailsTablesActivePageToZero, state => ({ + ...state, + details: { + ...state.details, + queries: setNetworkDetailsQueriesActivePageToZero(state), + }, + })) + .build(); diff --git a/x-pack/plugins/siem/public/network/store/selectors.ts b/x-pack/plugins/siem/public/network/store/selectors.ts new file mode 100644 index 00000000000000..0b48fa2170535d --- /dev/null +++ b/x-pack/plugins/siem/public/network/store/selectors.ts @@ -0,0 +1,88 @@ +/* + * 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 { createSelector } from 'reselect'; +import { get } from 'lodash/fp'; + +import { FlowTargetSourceDest } from '../../graphql/types'; +import { State } from '../../common/store/reducer'; +import { initialNetworkState } from './reducer'; +import { + IpDetailsTableType, + NetworkDetailsModel, + NetworkPageModel, + NetworkTableType, + NetworkType, + TopCountriesQuery, + TlsQuery, + HttpQuery, +} from './model'; + +const selectNetworkPage = (state: State): NetworkPageModel => state.network.page; + +const selectNetworkDetails = (state: State): NetworkDetailsModel => state.network.details; + +// Network Page Selectors +export const dnsSelector = () => createSelector(selectNetworkPage, network => network.queries.dns); + +const selectTopNFlowByType = ( + state: State, + networkType: NetworkType, + flowTarget: FlowTargetSourceDest +) => { + const ft = flowTarget === FlowTargetSourceDest.source ? 'topNFlowSource' : 'topNFlowDestination'; + const nFlowType = + networkType === NetworkType.page ? NetworkTableType[ft] : IpDetailsTableType[ft]; + return ( + get([networkType, 'queries', nFlowType], state.network) || + get([networkType, 'queries', nFlowType], initialNetworkState) + ); +}; + +export const topNFlowSelector = () => + createSelector(selectTopNFlowByType, topNFlowQueries => topNFlowQueries); +const selectTlsByType = (state: State, networkType: NetworkType): TlsQuery => { + const tlsType = networkType === NetworkType.page ? NetworkTableType.tls : IpDetailsTableType.tls; + return ( + get([networkType, 'queries', tlsType], state.network) || + get([networkType, 'queries', tlsType], initialNetworkState) + ); +}; + +export const tlsSelector = () => createSelector(selectTlsByType, tlsQueries => tlsQueries); + +const selectTopCountriesByType = ( + state: State, + networkType: NetworkType, + flowTarget: FlowTargetSourceDest +): TopCountriesQuery => { + const ft = + flowTarget === FlowTargetSourceDest.source ? 'topCountriesSource' : 'topCountriesDestination'; + const nFlowType = + networkType === NetworkType.page ? NetworkTableType[ft] : IpDetailsTableType[ft]; + + return ( + get([networkType, 'queries', nFlowType], state.network) || + get([networkType, 'queries', nFlowType], initialNetworkState) + ); +}; + +export const topCountriesSelector = () => + createSelector(selectTopCountriesByType, topCountriesQueries => topCountriesQueries); + +const selectHttpByType = (state: State, networkType: NetworkType): HttpQuery => { + const httpType = + networkType === NetworkType.page ? NetworkTableType.http : IpDetailsTableType.http; + return ( + get([networkType, 'queries', httpType], state.network) || + get([networkType, 'queries', httpType], initialNetworkState) + ); +}; + +export const httpSelector = () => createSelector(selectHttpByType, httpQueries => httpQueries); + +export const usersSelector = () => + createSelector(selectNetworkDetails, network => network.queries.users); diff --git a/x-pack/plugins/siem/public/overview/components/alerts_by_category/index.test.tsx b/x-pack/plugins/siem/public/overview/components/alerts_by_category/index.test.tsx new file mode 100644 index 00000000000000..c032b21f73290a --- /dev/null +++ b/x-pack/plugins/siem/public/overview/components/alerts_by_category/index.test.tsx @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable react/display-name */ + +import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { mount, ReactWrapper } from 'enzyme'; +import React from 'react'; +import { ThemeProvider } from 'styled-components'; + +import { useQuery } from '../../../common/containers/matrix_histogram'; +import { wait } from '../../../common/lib/helpers'; +import { mockIndexPattern, TestProviders } from '../../../common/mock'; + +import { AlertsByCategory } from '.'; + +jest.mock('../../../common/lib/kibana'); + +jest.mock('../../../common/containers/matrix_histogram', () => { + return { + useQuery: jest.fn(), + }; +}); + +const theme = () => ({ eui: { ...euiDarkVars, euiSizeL: '24px' }, darkMode: true }); +const from = new Date('2020-03-31T06:00:00.000Z').valueOf(); +const to = new Date('2019-03-31T06:00:00.000Z').valueOf(); + +describe('Alerts by category', () => { + let wrapper: ReactWrapper; + + describe('before loading data', () => { + beforeAll(async () => { + (useQuery as jest.Mock).mockReturnValue({ + data: null, + loading: false, + inspect: false, + totalCount: null, + }); + + wrapper = mount( + + + + + + ); + + await wait(); + wrapper.update(); + }); + + test('it renders the expected title', () => { + expect(wrapper.find('[data-test-subj="header-section-title"]').text()).toEqual( + 'External alert count' + ); + }); + + test('it renders the subtitle (to prevent layout thrashing)', () => { + expect(wrapper.find('[data-test-subj="header-panel-subtitle"]').exists()).toBe(true); + }); + + test('it renders the expected filter fields', () => { + const expectedOptions = ['event.category', 'event.module']; + + expectedOptions.forEach(option => { + expect(wrapper.find(`option[value="${option}"]`).text()).toEqual(option); + }); + }); + + test('it renders the `View alerts` button', () => { + expect(wrapper.find('[data-test-subj="view-alerts"]').exists()).toBe(true); + }); + + test('it does NOT render the bar chart when data is not available', () => { + expect(wrapper.find(`.echChart`).exists()).toBe(false); + }); + }); + + describe('after loading data', () => { + beforeAll(async () => { + (useQuery as jest.Mock).mockReturnValue({ + data: [ + { x: 1, y: 2, g: 'g1' }, + { x: 2, y: 4, g: 'g1' }, + { x: 3, y: 6, g: 'g1' }, + { x: 1, y: 1, g: 'g2' }, + { x: 2, y: 3, g: 'g2' }, + { x: 3, y: 5, g: 'g2' }, + ], + loading: false, + inspect: false, + totalCount: 6, + }); + + wrapper = mount( + + + + + + ); + + await wait(); + wrapper.update(); + }); + + test('it renders the expected subtitle', () => { + expect(wrapper.find('[data-test-subj="header-panel-subtitle"]').text()).toEqual( + 'Showing: 6 external alerts' + ); + }); + + test('it renders the bar chart when data is available', () => { + expect(wrapper.find(`.echChart`).exists()).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/overview/components/alerts_by_category/index.tsx b/x-pack/plugins/siem/public/overview/components/alerts_by_category/index.tsx new file mode 100644 index 00000000000000..92f55aa1aa36d8 --- /dev/null +++ b/x-pack/plugins/siem/public/overview/components/alerts_by_category/index.tsx @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButton } from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import React, { useEffect, useMemo } from 'react'; +import { Position } from '@elastic/charts'; + +import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; +import { SHOWING, UNIT } from '../../../common/components/alerts_viewer/translations'; +import { getDetectionEngineAlertUrl } from '../../../common/components/link_to/redirect_to_detection_engine'; +import { MatrixHistogramContainer } from '../../../common/components/matrix_histogram'; +import { useKibana, useUiSetting$ } from '../../../common/lib/kibana'; +import { convertToBuildEsQuery } from '../../../common/lib/keury'; +import { + Filter, + esQuery, + IIndexPattern, + Query, +} from '../../../../../../../src/plugins/data/public'; +import { inputsModel } from '../../../common/store'; +import { HostsType } from '../../../hosts/store/model'; + +import * as i18n from '../../pages/translations'; +import { + alertsStackByOptions, + histogramConfigs, +} from '../../../common/components/alerts_viewer/histogram_configs'; +import { MatrixHisrogramConfigs } from '../../../common/components/matrix_histogram/types'; +import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search'; +import { navTabs } from '../../../app/home/home_navigations'; + +const ID = 'alertsByCategoryOverview'; + +const NO_FILTERS: Filter[] = []; +const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; +const DEFAULT_STACK_BY = 'event.module'; + +interface Props { + deleteQuery?: ({ id }: { id: string }) => void; + filters?: Filter[]; + from: number; + hideHeaderChildren?: boolean; + indexPattern: IIndexPattern; + query?: Query; + setQuery: (params: { + id: string; + inspect: inputsModel.InspectQuery | null; + loading: boolean; + refetch: inputsModel.Refetch; + }) => void; + to: number; +} + +const AlertsByCategoryComponent: React.FC = ({ + deleteQuery, + filters = NO_FILTERS, + from, + hideHeaderChildren = false, + indexPattern, + query = DEFAULT_QUERY, + setQuery, + to, +}) => { + useEffect(() => { + return () => { + if (deleteQuery) { + deleteQuery({ id: ID }); + } + }; + }, []); + + const kibana = useKibana(); + const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); + const urlSearch = useGetUrlSearch(navTabs.detections); + + const alertsCountViewAlertsButton = useMemo( + () => ( + + {i18n.VIEW_ALERTS} + + ), + [urlSearch] + ); + + const alertsByCategoryHistogramConfigs: MatrixHisrogramConfigs = useMemo( + () => ({ + ...histogramConfigs, + defaultStackByOption: + alertsStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? alertsStackByOptions[0], + subtitle: (totalCount: number) => + `${SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${UNIT(totalCount)}`, + legendPosition: Position.Right, + }), + [] + ); + + return ( + + ); +}; + +AlertsByCategoryComponent.displayName = 'AlertsByCategoryComponent'; + +export const AlertsByCategory = React.memo(AlertsByCategoryComponent); diff --git a/x-pack/plugins/siem/public/overview/components/event_counts/index.test.tsx b/x-pack/plugins/siem/public/overview/components/event_counts/index.test.tsx new file mode 100644 index 00000000000000..628cd28979083e --- /dev/null +++ b/x-pack/plugins/siem/public/overview/components/event_counts/index.test.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mount } from 'enzyme'; +import React from 'react'; + +import { OverviewHostProps } from '../overview_host'; +import { OverviewNetworkProps } from '../overview_network'; +import { mockIndexPattern, TestProviders } from '../../../common/mock'; + +import { EventCounts } from '.'; + +describe('EventCounts', () => { + const from = 1579553397080; + const to = 1579639797080; + + test('it filters the `Host events` widget with a `host.name` `exists` filter', () => { + const wrapper = mount( + + + + ); + + expect( + (wrapper + .find('[data-test-subj="overview-host-query"]') + .first() + .props() as OverviewHostProps).filterQuery + ).toContain('[{"bool":{"should":[{"exists":{"field":"host.name"}}]'); + }); + + test('it filters the `Network events` widget with a `source.ip` or `destination.ip` `exists` filter', () => { + const wrapper = mount( + + + + ); + + expect( + (wrapper + .find('[data-test-subj="overview-network-query"]') + .first() + .props() as OverviewNetworkProps).filterQuery + ).toContain( + '{"bool":{"filter":[{"bool":{"should":[{"bool":{"should":[{"exists":{"field":"source.ip"}}],"minimum_should_match":1}},{"bool":{"should":[{"exists":{"field":"destination.ip"}}],"minimum_should_match":1}}],"minimum_should_match":1}}]}}]' + ); + }); +}); diff --git a/x-pack/plugins/siem/public/overview/components/event_counts/index.tsx b/x-pack/plugins/siem/public/overview/components/event_counts/index.tsx new file mode 100644 index 00000000000000..1773af86a382f6 --- /dev/null +++ b/x-pack/plugins/siem/public/overview/components/event_counts/index.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +import { OverviewHost } from '../overview_host'; +import { OverviewNetwork } from '../overview_network'; +import { filterHostData } from '../../../hosts/pages/navigation/alerts_query_tab_body'; +import { useKibana } from '../../../common/lib/kibana'; +import { convertToBuildEsQuery } from '../../../common/lib/keury'; +import { filterNetworkData } from '../../../network/pages/navigation/alerts_query_tab_body'; +import { + Filter, + esQuery, + IIndexPattern, + Query, +} from '../../../../../../../src/plugins/data/public'; +import { inputsModel } from '../../../common/store'; + +const HorizontalSpacer = styled(EuiFlexItem)` + width: 24px; +`; + +const NO_FILTERS: Filter[] = []; +const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; + +interface Props { + filters?: Filter[]; + from: number; + indexPattern: IIndexPattern; + query?: Query; + setQuery: (params: { + id: string; + inspect: inputsModel.InspectQuery | null; + loading: boolean; + refetch: inputsModel.Refetch; + }) => void; + to: number; +} + +const EventCountsComponent: React.FC = ({ + filters = NO_FILTERS, + from, + indexPattern, + query = DEFAULT_QUERY, + setQuery, + to, +}) => { + const kibana = useKibana(); + + return ( + + + + + + + + + + + + ); +}; + +export const EventCounts = React.memo(EventCountsComponent); diff --git a/x-pack/plugins/siem/public/pages/overview/events_by_dataset/__mocks__/index.tsx b/x-pack/plugins/siem/public/overview/components/events_by_dataset/__mocks__/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/overview/events_by_dataset/__mocks__/index.tsx rename to x-pack/plugins/siem/public/overview/components/events_by_dataset/__mocks__/index.tsx diff --git a/x-pack/plugins/siem/public/overview/components/events_by_dataset/index.tsx b/x-pack/plugins/siem/public/overview/components/events_by_dataset/index.tsx new file mode 100644 index 00000000000000..ebd005e7cb0b32 --- /dev/null +++ b/x-pack/plugins/siem/public/overview/components/events_by_dataset/index.tsx @@ -0,0 +1,174 @@ +/* + * 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 { Position } from '@elastic/charts'; +import { EuiButton } from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import React, { useEffect, useMemo } from 'react'; +import uuid from 'uuid'; + +import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; +import { SHOWING, UNIT } from '../../../common/components/events_viewer/translations'; +import { getTabsOnHostsUrl } from '../../../common/components/link_to/redirect_to_hosts'; +import { MatrixHistogramContainer } from '../../../common/components/matrix_histogram'; +import { + MatrixHisrogramConfigs, + MatrixHistogramOption, +} from '../../../common/components/matrix_histogram/types'; +import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search'; +import { navTabs } from '../../../app/home/home_navigations'; +import { eventsStackByOptions } from '../../../hosts/pages/navigation'; +import { convertToBuildEsQuery } from '../../../common/lib/keury'; +import { useKibana, useUiSetting$ } from '../../../common/lib/kibana'; +import { histogramConfigs } from '../../../hosts/pages/navigation/events_query_tab_body'; +import { + Filter, + esQuery, + IIndexPattern, + Query, +} from '../../../../../../../src/plugins/data/public'; +import { inputsModel } from '../../../common/store'; +import { HostsTableType, HostsType } from '../../../hosts/store/model'; +import { InputsModelId } from '../../../common/store/inputs/constants'; + +import * as i18n from '../../pages/translations'; + +const NO_FILTERS: Filter[] = []; +const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; +const DEFAULT_STACK_BY = 'event.dataset'; + +const ID = 'eventsByDatasetOverview'; + +interface Props { + combinedQueries?: string; + deleteQuery?: ({ id }: { id: string }) => void; + filters?: Filter[]; + from: number; + headerChildren?: React.ReactNode; + indexPattern: IIndexPattern; + indexToAdd?: string[] | null; + onlyField?: string; + query?: Query; + setAbsoluteRangeDatePickerTarget?: InputsModelId; + setQuery: (params: { + id: string; + inspect: inputsModel.InspectQuery | null; + loading: boolean; + refetch: inputsModel.Refetch; + }) => void; + showSpacer?: boolean; + to: number; +} + +const getHistogramOption = (fieldName: string): MatrixHistogramOption => ({ + text: fieldName, + value: fieldName, +}); + +const EventsByDatasetComponent: React.FC = ({ + combinedQueries, + deleteQuery, + filters = NO_FILTERS, + from, + headerChildren, + indexPattern, + indexToAdd, + onlyField, + query = DEFAULT_QUERY, + setAbsoluteRangeDatePickerTarget, + setQuery, + showSpacer = true, + to, +}) => { + // create a unique, but stable (across re-renders) query id + const uniqueQueryId = useMemo(() => `${ID}-${uuid.v4()}`, []); + + useEffect(() => { + return () => { + if (deleteQuery) { + deleteQuery({ id: uniqueQueryId }); + } + }; + }, [deleteQuery, uniqueQueryId]); + + const kibana = useKibana(); + const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); + const urlSearch = useGetUrlSearch(navTabs.hosts); + + const eventsCountViewEventsButton = useMemo( + () => ( + + {i18n.VIEW_EVENTS} + + ), + [urlSearch] + ); + + const filterQuery = useMemo( + () => + combinedQueries == null + ? convertToBuildEsQuery({ + config: esQuery.getEsQueryConfig(kibana.services.uiSettings), + indexPattern, + queries: [query], + filters, + }) + : combinedQueries, + [combinedQueries, kibana, indexPattern, query, filters] + ); + + const eventsByDatasetHistogramConfigs: MatrixHisrogramConfigs = useMemo( + () => ({ + ...histogramConfigs, + stackByOptions: + onlyField != null ? [getHistogramOption(onlyField)] : histogramConfigs.stackByOptions, + defaultStackByOption: + onlyField != null + ? getHistogramOption(onlyField) + : eventsStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? eventsStackByOptions[0], + legendPosition: Position.Right, + subtitle: (totalCount: number) => + `${SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${UNIT(totalCount)}`, + titleSize: onlyField == null ? 'm' : 's', + }), + [onlyField, defaultNumberFormat] + ); + + const headerContent = useMemo(() => { + if (onlyField == null || headerChildren != null) { + return ( + <> + {headerChildren} + {onlyField == null && eventsCountViewEventsButton} + + ); + } else { + return null; + } + }, [onlyField, headerChildren, eventsCountViewEventsButton]); + + return ( + + ); +}; + +EventsByDatasetComponent.displayName = 'EventsByDatasetComponent'; + +export const EventsByDataset = React.memo(EventsByDatasetComponent); diff --git a/x-pack/plugins/siem/public/components/page/hosts/host_overview/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/overview/components/host_overview/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/page/hosts/host_overview/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/overview/components/host_overview/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/overview/components/host_overview/index.test.tsx b/x-pack/plugins/siem/public/overview/components/host_overview/index.test.tsx new file mode 100644 index 00000000000000..56c232158ac02c --- /dev/null +++ b/x-pack/plugins/siem/public/overview/components/host_overview/index.test.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { shallow } from 'enzyme'; +import React from 'react'; +import { TestProviders } from '../../../common/mock'; + +import { HostOverview } from './index'; +import { mockData } from './mock'; +import { mockAnomalies } from '../../../common/components/ml/mock'; + +describe('Host Summary Component', () => { + describe('rendering', () => { + test('it renders the default Host Summary', () => { + const wrapper = shallow( + + + + ); + + expect(wrapper.find('HostOverview')).toMatchSnapshot(); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/overview/components/host_overview/index.tsx b/x-pack/plugins/siem/public/overview/components/host_overview/index.tsx new file mode 100644 index 00000000000000..4440147c35f2f1 --- /dev/null +++ b/x-pack/plugins/siem/public/overview/components/host_overview/index.tsx @@ -0,0 +1,197 @@ +/* + * 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 { EuiFlexItem } from '@elastic/eui'; +import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; +import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; +import { getOr } from 'lodash/fp'; +import React from 'react'; + +import { DEFAULT_DARK_MODE } from '../../../../common/constants'; +import { DescriptionList } from '../../../../common/utility_types'; +import { useUiSetting$ } from '../../../common/lib/kibana'; +import { getEmptyTagValue } from '../../../common/components/empty_value'; +import { + DefaultFieldRenderer, + hostIdRenderer, +} from '../../../timelines/components/field_renderers/field_renderers'; +import { InspectButton, InspectButtonContainer } from '../../../common/components/inspect'; +import { HostItem } from '../../../graphql/types'; +import { Loader } from '../../../common/components/loader'; +import { IPDetailsLink } from '../../../common/components/links'; +import { hasMlUserPermissions } from '../../../../common/machine_learning/has_ml_user_permissions'; +import { useMlCapabilities } from '../../../common/components/ml_popover/hooks/use_ml_capabilities'; +import { AnomalyScores } from '../../../common/components/ml/score/anomaly_scores'; +import { Anomalies, NarrowDateRange } from '../../../common/components/ml/types'; +import { DescriptionListStyled, OverviewWrapper } from '../../../common/components/page'; +import { + FirstLastSeenHost, + FirstLastSeenHostType, +} from '../../../hosts/components/first_last_seen_host'; + +import * as i18n from './translations'; + +interface HostSummaryProps { + data: HostItem; + id: string; + loading: boolean; + isLoadingAnomaliesData: boolean; + anomaliesData: Anomalies | null; + startDate: number; + endDate: number; + narrowDateRange: NarrowDateRange; +} + +const getDescriptionList = (descriptionList: DescriptionList[], key: number) => ( + + + +); + +export const HostOverview = React.memo( + ({ + data, + loading, + id, + startDate, + endDate, + isLoadingAnomaliesData, + anomaliesData, + narrowDateRange, + }) => { + const capabilities = useMlCapabilities(); + const userPermissions = hasMlUserPermissions(capabilities); + const [darkMode] = useUiSetting$(DEFAULT_DARK_MODE); + + const getDefaultRenderer = (fieldName: string, fieldData: HostItem) => ( + + ); + + const column: DescriptionList[] = [ + { + title: i18n.HOST_ID, + description: data.host + ? hostIdRenderer({ host: data.host, noLink: true }) + : getEmptyTagValue(), + }, + { + title: i18n.FIRST_SEEN, + description: + data.host != null && data.host.name && data.host.name.length ? ( + + ) : ( + getEmptyTagValue() + ), + }, + { + title: i18n.LAST_SEEN, + description: + data.host != null && data.host.name && data.host.name.length ? ( + + ) : ( + getEmptyTagValue() + ), + }, + ]; + const firstColumn = userPermissions + ? [ + ...column, + { + title: i18n.MAX_ANOMALY_SCORE_BY_JOB, + description: ( + + ), + }, + ] + : column; + + const descriptionLists: Readonly = [ + firstColumn, + [ + { + title: i18n.IP_ADDRESSES, + description: ( + (ip != null ? : getEmptyTagValue())} + /> + ), + }, + { + title: i18n.MAC_ADDRESSES, + description: getDefaultRenderer('host.mac', data), + }, + { title: i18n.PLATFORM, description: getDefaultRenderer('host.os.platform', data) }, + ], + [ + { title: i18n.OS, description: getDefaultRenderer('host.os.name', data) }, + { title: i18n.FAMILY, description: getDefaultRenderer('host.os.family', data) }, + { title: i18n.VERSION, description: getDefaultRenderer('host.os.version', data) }, + { title: i18n.ARCHITECTURE, description: getDefaultRenderer('host.architecture', data) }, + ], + [ + { + title: i18n.CLOUD_PROVIDER, + description: getDefaultRenderer('cloud.provider', data), + }, + { + title: i18n.REGION, + description: getDefaultRenderer('cloud.region', data), + }, + { + title: i18n.INSTANCE_ID, + description: getDefaultRenderer('cloud.instance.id', data), + }, + { + title: i18n.MACHINE_TYPE, + description: getDefaultRenderer('cloud.machine.type', data), + }, + ], + ]; + + return ( + + + + + {descriptionLists.map((descriptionList, index) => + getDescriptionList(descriptionList, index) + )} + + {loading && ( + + )} + + + ); + } +); + +HostOverview.displayName = 'HostOverview'; diff --git a/x-pack/plugins/siem/public/overview/components/host_overview/mock.ts b/x-pack/plugins/siem/public/overview/components/host_overview/mock.ts new file mode 100644 index 00000000000000..c24cb20e9087ca --- /dev/null +++ b/x-pack/plugins/siem/public/overview/components/host_overview/mock.ts @@ -0,0 +1,52 @@ +/* + * 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 { HostsData } from '../../../graphql/types'; + +export const mockData: { Hosts: HostsData; DateFields: string[] } = { + Hosts: { + totalCount: 1, + edges: [ + { + node: { + _id: 'yneHlmgBjVl2VqDlAjPR', + host: { + architecture: ['x86_64'], + id: ['aa7ca589f1b8220002f2fc61c64cfbf1'], + ip: ['10.142.0.7', 'fe80::4001:aff:fe8e:7'], + mac: ['42:01:0a:8e:00:07'], + name: ['siem-kibana'], + os: { + family: ['debian'], + name: ['Debian GNU/Linux'], + platform: ['debian'], + version: ['9 (stretch)'], + }, + }, + cloud: { + instance: { + id: ['423232333829362673777'], + }, + machine: { + type: ['custom-4-16384'], + }, + provider: ['gce'], + region: ['us-east-1'], + }, + }, + cursor: { + value: 'aa7ca589f1b8220002f2fc61c64cfbf1', + }, + }, + ], + pageInfo: { + activePage: 1, + fakeTotalCount: 50, + showMorePagesIndicator: true, + }, + }, + DateFields: ['lastBeat'], +}; diff --git a/x-pack/plugins/siem/public/components/page/hosts/host_overview/translations.ts b/x-pack/plugins/siem/public/overview/components/host_overview/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/page/hosts/host_overview/translations.ts rename to x-pack/plugins/siem/public/overview/components/host_overview/translations.ts diff --git a/x-pack/plugins/siem/public/components/page/overview/loading_placeholders/index.tsx b/x-pack/plugins/siem/public/overview/components/loading_placeholders/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/page/overview/loading_placeholders/index.tsx rename to x-pack/plugins/siem/public/overview/components/loading_placeholders/index.tsx diff --git a/x-pack/plugins/siem/public/overview/components/overview_empty/index.tsx b/x-pack/plugins/siem/public/overview/components/overview_empty/index.tsx new file mode 100644 index 00000000000000..85a89882027749 --- /dev/null +++ b/x-pack/plugins/siem/public/overview/components/overview_empty/index.tsx @@ -0,0 +1,35 @@ +/* + * 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 i18nCommon from '../../../common/translations'; +import { EmptyPage } from '../../../common/components/empty_page'; +import { useKibana } from '../../../common/lib/kibana'; + +const OverviewEmptyComponent: React.FC = () => { + const { http, docLinks } = useKibana().services; + const basePath = http.basePath.get(); + + return ( + + ); +}; + +OverviewEmptyComponent.displayName = 'OverviewEmptyComponent'; + +export const OverviewEmpty = React.memo(OverviewEmptyComponent); diff --git a/x-pack/plugins/siem/public/overview/components/overview_host/index.test.tsx b/x-pack/plugins/siem/public/overview/components/overview_host/index.test.tsx new file mode 100644 index 00000000000000..137f5d1dc245de --- /dev/null +++ b/x-pack/plugins/siem/public/overview/components/overview_host/index.test.tsx @@ -0,0 +1,150 @@ +/* + * 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 { cloneDeep } from 'lodash/fp'; +import { mount } from 'enzyme'; +import React from 'react'; +import { MockedProvider } from 'react-apollo/test-utils'; + +import { + apolloClientObservable, + mockGlobalState, + TestProviders, + SUB_PLUGINS_REDUCER, +} from '../../../common/mock'; + +import { OverviewHost } from '.'; +import { createStore, State } from '../../../common/store'; +import { overviewHostQuery } from '../../containers/overview_host/index.gql_query'; +import { GetOverviewHostQuery } from '../../../graphql/types'; + +import { wait } from '../../../common/lib/helpers'; + +jest.mock('../../../common/lib/kibana'); + +const startDate = 1579553397080; +const endDate = 1579639797080; + +interface MockedProvidedQuery { + request: { + query: GetOverviewHostQuery.Query; + fetchPolicy: string; + variables: GetOverviewHostQuery.Variables; + }; + result: { + data: { + source: unknown; + }; + }; +} + +const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ + { + request: { + query: overviewHostQuery, + fetchPolicy: 'cache-and-network', + variables: { + sourceId: 'default', + timerange: { interval: '12h', from: startDate, to: endDate }, + filterQuery: undefined, + defaultIndex: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + inspect: false, + }, + }, + result: { + data: { + source: { + id: 'default', + OverviewHost: { + auditbeatAuditd: 1, + auditbeatFIM: 1, + auditbeatLogin: 1, + auditbeatPackage: 1, + auditbeatProcess: 1, + auditbeatUser: 1, + endgameDns: 1, + endgameFile: 1, + endgameImageLoad: 1, + endgameNetwork: 1, + endgameProcess: 1, + endgameRegistry: 1, + endgameSecurity: 1, + filebeatSystemModule: 1, + winlogbeatSecurity: 1, + winlogbeatMWSysmonOperational: 1, + }, + }, + }, + }, + }, +]; + +describe('OverviewHost', () => { + const state: State = mockGlobalState; + + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + + beforeEach(() => { + const myState = cloneDeep(state); + store = createStore(myState, SUB_PLUGINS_REDUCER, apolloClientObservable); + }); + + test('it renders the expected widget title', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="header-section-title"]') + .first() + .text() + ).toEqual('Host events'); + }); + + test('it renders an empty subtitle while loading', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="header-panel-subtitle"]') + .first() + .text() + ).toEqual(''); + }); + + test('it renders the expected event count in the subtitle after loading events', async () => { + const wrapper = mount( + + + + + + ); + await wait(); + wrapper.update(); + + expect( + wrapper + .find('[data-test-subj="header-panel-subtitle"]') + .first() + .text() + ).toEqual('Showing: 16 events'); + }); +}); diff --git a/x-pack/plugins/siem/public/overview/components/overview_host/index.tsx b/x-pack/plugins/siem/public/overview/components/overview_host/index.tsx new file mode 100644 index 00000000000000..111c1282931949 --- /dev/null +++ b/x-pack/plugins/siem/public/overview/components/overview_host/index.tsx @@ -0,0 +1,126 @@ +/* + * 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 { isEmpty } from 'lodash/fp'; +import { EuiButton, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useMemo } from 'react'; + +import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; +import { ESQuery } from '../../../../common/typed_json'; +import { ID as OverviewHostQueryId, OverviewHostQuery } from '../../containers/overview_host'; +import { HeaderSection } from '../../../common/components/header_section'; +import { useUiSetting$ } from '../../../common/lib/kibana'; +import { getHostsUrl } from '../../../common/components/link_to'; +import { getOverviewHostStats, OverviewHostStats } from '../overview_host_stats'; +import { manageQuery } from '../../../common/components/page/manage_query'; +import { inputsModel } from '../../../common/store/inputs'; +import { InspectButtonContainer } from '../../../common/components/inspect'; +import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search'; +import { navTabs } from '../../../app/home/home_navigations'; + +export interface OwnProps { + startDate: number; + endDate: number; + filterQuery?: ESQuery | string; + setQuery: ({ + id, + inspect, + loading, + refetch, + }: { + id: string; + inspect: inputsModel.InspectQuery | null; + loading: boolean; + refetch: inputsModel.Refetch; + }) => void; +} + +const OverviewHostStatsManage = manageQuery(OverviewHostStats); +export type OverviewHostProps = OwnProps; + +const OverviewHostComponent: React.FC = ({ + endDate, + filterQuery, + startDate, + setQuery, +}) => { + const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); + const urlSearch = useGetUrlSearch(navTabs.hosts); + const hostPageButton = useMemo( + () => ( + + + + ), + [urlSearch] + ); + return ( + + + + + {({ overviewHost, loading, id, inspect, refetch }) => { + const hostEventsCount = getOverviewHostStats(overviewHost).reduce( + (total, stat) => total + stat.count, + 0 + ); + const formattedHostEventsCount = numeral(hostEventsCount).format(defaultNumberFormat); + + return ( + <> + + ) : ( + <>{''} + ) + } + title={ + + } + > + {hostPageButton} + + + + + ); + }} + + + + + ); +}; + +export const OverviewHost = React.memo(OverviewHostComponent); diff --git a/x-pack/plugins/siem/public/components/page/overview/overview_host_stats/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/overview/components/overview_host_stats/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/page/overview/overview_host_stats/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/overview/components/overview_host_stats/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/overview/components/overview_host_stats/index.test.tsx b/x-pack/plugins/siem/public/overview/components/overview_host_stats/index.test.tsx new file mode 100644 index 00000000000000..fcbe0c5272dae5 --- /dev/null +++ b/x-pack/plugins/siem/public/overview/components/overview_host_stats/index.test.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mount, shallow } from 'enzyme'; +import React from 'react'; + +import { OverviewHostStats } from '.'; +import { mockData } from './mock'; +import { TestProviders } from '../../../common/mock/test_providers'; + +describe('Overview Host Stat Data', () => { + describe('rendering', () => { + test('it renders the default OverviewHostStats', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + }); + describe('loading', () => { + test('it does NOT show loading indicator when loading is false', () => { + const wrapper = mount( + + + + ); + + // click the accordion to expand it + wrapper + .find('button') + .first() + .simulate('click'); + wrapper.update(); + + expect( + wrapper + .find('[data-test-subj="host-stat-auditbeatAuditd"]') + .first() + .find('[data-test-subj="stat-value-loading-spinner"]') + .first() + .exists() + ).toBe(false); + }); + test('it shows loading indicator when loading is true', () => { + const wrapper = mount( + + + + ); + + // click the accordion to expand it + wrapper + .find('button') + .first() + .simulate('click'); + wrapper.update(); + + expect( + wrapper + .find('[data-test-subj="host-stat-auditbeatAuditd"]') + .first() + .find('[data-test-subj="stat-value-loading-spinner"]') + .first() + .exists() + ).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/overview/components/overview_host_stats/index.tsx b/x-pack/plugins/siem/public/overview/components/overview_host_stats/index.tsx new file mode 100644 index 00000000000000..ab2e12f2110b8b --- /dev/null +++ b/x-pack/plugins/siem/public/overview/components/overview_host_stats/index.tsx @@ -0,0 +1,269 @@ +/* + * 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 { EuiAccordion, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; +import styled from 'styled-components'; + +import { OverviewHostData } from '../../../graphql/types'; +import { FormattedStat, StatGroup } from '../types'; +import { StatValue } from '../stat_value'; + +interface OverviewHostProps { + data: OverviewHostData; + loading: boolean; +} + +export const getOverviewHostStats = (data: OverviewHostData): FormattedStat[] => [ + { + count: data.auditbeatAuditd ?? 0, + title: , + id: 'auditbeatAuditd', + }, + { + count: data.auditbeatFIM ?? 0, + title: ( + + ), + id: 'auditbeatFIM', + }, + { + count: data.auditbeatLogin ?? 0, + title: , + id: 'auditbeatLogin', + }, + { + count: data.auditbeatPackage ?? 0, + title: ( + + ), + id: 'auditbeatPackage', + }, + { + count: data.auditbeatProcess ?? 0, + title: ( + + ), + id: 'auditbeatProcess', + }, + { + count: data.auditbeatUser ?? 0, + title: , + id: 'auditbeatUser', + }, + { + count: data.endgameDns ?? 0, + title: , + id: 'endgameDns', + }, + { + count: data.endgameFile ?? 0, + title: , + id: 'endgameFile', + }, + { + count: data.endgameImageLoad ?? 0, + title: ( + + ), + id: 'endgameImageLoad', + }, + { + count: data.endgameNetwork ?? 0, + title: ( + + ), + id: 'endgameNetwork', + }, + { + count: data.endgameProcess ?? 0, + title: ( + + ), + id: 'endgameProcess', + }, + { + count: data.endgameRegistry ?? 0, + title: ( + + ), + id: 'endgameRegistry', + }, + { + count: data.endgameSecurity ?? 0, + title: ( + + ), + id: 'endgameSecurity', + }, + { + count: data.filebeatSystemModule ?? 0, + title: ( + + ), + id: 'filebeatSystemModule', + }, + { + count: data.winlogbeatSecurity ?? 0, + title: ( + + ), + id: 'winlogbeatSecurity', + }, + { + count: data.winlogbeatMWSysmonOperational ?? 0, + title: ( + + ), + id: 'winlogbeatMWSysmonOperational', + }, +]; + +const HostStatsContainer = styled.div` + .accordion-button { + width: 100%; + } +`; + +const hostStatGroups: StatGroup[] = [ + { + groupId: 'auditbeat', + name: ( + + ), + statIds: [ + 'auditbeatAuditd', + 'auditbeatFIM', + 'auditbeatLogin', + 'auditbeatPackage', + 'auditbeatProcess', + 'auditbeatUser', + ], + }, + { + groupId: 'endgame', + name: ( + + ), + statIds: [ + 'endgameDns', + 'endgameFile', + 'endgameImageLoad', + 'endgameNetwork', + 'endgameProcess', + 'endgameRegistry', + 'endgameSecurity', + ], + }, + { + groupId: 'filebeat', + name: ( + + ), + statIds: ['filebeatSystemModule'], + }, + { + groupId: 'winlogbeat', + name: ( + + ), + statIds: ['winlogbeatSecurity', 'winlogbeatMWSysmonOperational'], + }, +]; + +const Title = styled.div` + margin-left: 24px; +`; + +const AccordionContent = styled.div` + margin-top: 8px; +`; + +const OverviewHostStatsComponent: React.FC = ({ data, loading }) => { + const allHostStats = getOverviewHostStats(data); + const allHostStatsCount = allHostStats.reduce((total, stat) => total + stat.count, 0); + + return ( + + {hostStatGroups.map((statGroup, i) => { + const statsForGroup = allHostStats.filter(s => statGroup.statIds.includes(s.id)); + const statsForGroupCount = statsForGroup.reduce((total, stat) => total + stat.count, 0); + + return ( + + + + + {statGroup.name} + + + + + + } + buttonContentClassName="accordion-button" + > + + {statsForGroup.map(stat => ( + + + + {stat.title} + + + + + + + ))} + + + + ); + })} + + ); +}; + +export const OverviewHostStats = React.memo(OverviewHostStatsComponent); diff --git a/x-pack/plugins/siem/public/overview/components/overview_host_stats/mock.ts b/x-pack/plugins/siem/public/overview/components/overview_host_stats/mock.ts new file mode 100644 index 00000000000000..63b3a484c1eaa9 --- /dev/null +++ b/x-pack/plugins/siem/public/overview/components/overview_host_stats/mock.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { OverviewHostData } from '../../../graphql/types'; + +export const mockData: { OverviewHost: OverviewHostData } = { + OverviewHost: { + auditbeatAuditd: 73847, + auditbeatFIM: 107307, + auditbeatLogin: 60015, + auditbeatPackage: 2003, + auditbeatProcess: 1200, + auditbeatUser: 1979, + endgameDns: 39123, + endgameFile: 39456, + endgameImageLoad: 39789, + endgameNetwork: 39101112, + endgameProcess: 39131415, + endgameRegistry: 39161718, + endgameSecurity: 39202122, + filebeatSystemModule: 568, + winlogbeatSecurity: 195929, + winlogbeatMWSysmonOperational: 101070, + }, +}; diff --git a/x-pack/plugins/siem/public/overview/components/overview_network/index.test.tsx b/x-pack/plugins/siem/public/overview/components/overview_network/index.test.tsx new file mode 100644 index 00000000000000..e28681a3320f5e --- /dev/null +++ b/x-pack/plugins/siem/public/overview/components/overview_network/index.test.tsx @@ -0,0 +1,141 @@ +/* + * 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 { cloneDeep } from 'lodash/fp'; +import { mount } from 'enzyme'; +import React from 'react'; +import { MockedProvider } from 'react-apollo/test-utils'; +import { + apolloClientObservable, + mockGlobalState, + TestProviders, + SUB_PLUGINS_REDUCER, +} from '../../../common/mock'; + +import { OverviewNetwork } from '.'; +import { createStore, State } from '../../../common/store'; +import { overviewNetworkQuery } from '../../containers/overview_network/index.gql_query'; +import { GetOverviewHostQuery } from '../../../graphql/types'; +import { wait } from '../../../common/lib/helpers'; + +jest.mock('../../../common/lib/kibana'); + +const startDate = 1579553397080; +const endDate = 1579639797080; + +interface MockedProvidedQuery { + request: { + query: GetOverviewHostQuery.Query; + fetchPolicy: string; + variables: GetOverviewHostQuery.Variables; + }; + result: { + data: { + source: unknown; + }; + }; +} + +const mockOpenTimelineQueryResults: MockedProvidedQuery[] = [ + { + request: { + query: overviewNetworkQuery, + fetchPolicy: 'cache-and-network', + variables: { + sourceId: 'default', + timerange: { interval: '12h', from: startDate, to: endDate }, + filterQuery: undefined, + defaultIndex: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + inspect: false, + }, + }, + result: { + data: { + source: { + id: 'default', + OverviewNetwork: { + auditbeatSocket: 1, + filebeatCisco: 1, + filebeatNetflow: 1, + filebeatPanw: 1, + filebeatSuricata: 1, + filebeatZeek: 1, + packetbeatDNS: 1, + packetbeatFlow: 1, + packetbeatTLS: 1, + }, + }, + }, + }, + }, +]; + +describe('OverviewNetwork', () => { + const state: State = mockGlobalState; + + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + + beforeEach(() => { + const myState = cloneDeep(state); + store = createStore(myState, SUB_PLUGINS_REDUCER, apolloClientObservable); + }); + + test('it renders the expected widget title', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="header-section-title"]') + .first() + .text() + ).toEqual('Network events'); + }); + + test('it renders an empty subtitle while loading', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="header-panel-subtitle"]') + .first() + .text() + ).toEqual(''); + }); + + test('it renders the expected event count in the subtitle after loading events', async () => { + const wrapper = mount( + + + + + + ); + await wait(); + wrapper.update(); + + expect( + wrapper + .find('[data-test-subj="header-panel-subtitle"]') + .first() + .text() + ).toEqual('Showing: 9 events'); + }); +}); diff --git a/x-pack/plugins/siem/public/overview/components/overview_network/index.tsx b/x-pack/plugins/siem/public/overview/components/overview_network/index.tsx new file mode 100644 index 00000000000000..cd70831fddfbab --- /dev/null +++ b/x-pack/plugins/siem/public/overview/components/overview_network/index.tsx @@ -0,0 +1,132 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty } from 'lodash/fp'; +import { EuiButton, EuiFlexItem, EuiPanel } from '@elastic/eui'; +import numeral from '@elastic/numeral'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useMemo } from 'react'; + +import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; +import { ESQuery } from '../../../../common/typed_json'; +import { HeaderSection } from '../../../common/components/header_section'; +import { useUiSetting$ } from '../../../common/lib/kibana'; +import { manageQuery } from '../../../common/components/page/manage_query'; +import { + ID as OverviewNetworkQueryId, + OverviewNetworkQuery, +} from '../../containers/overview_network'; +import { inputsModel } from '../../../common/store/inputs'; +import { getOverviewNetworkStats, OverviewNetworkStats } from '../overview_network_stats'; +import { getNetworkUrl } from '../../../common/components/link_to'; +import { InspectButtonContainer } from '../../../common/components/inspect'; +import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search'; +import { navTabs } from '../../../app/home/home_navigations'; + +export interface OverviewNetworkProps { + startDate: number; + endDate: number; + filterQuery?: ESQuery | string; + setQuery: ({ + id, + inspect, + loading, + refetch, + }: { + id: string; + inspect: inputsModel.InspectQuery | null; + loading: boolean; + refetch: inputsModel.Refetch; + }) => void; +} + +const OverviewNetworkStatsManage = manageQuery(OverviewNetworkStats); + +const OverviewNetworkComponent: React.FC = ({ + endDate, + filterQuery, + startDate, + setQuery, +}) => { + const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); + const urlSearch = useGetUrlSearch(navTabs.network); + const networkPageButton = useMemo( + () => ( + + + + ), + [urlSearch] + ); + return ( + + + + + {({ overviewNetwork, loading, id, inspect, refetch }) => { + const networkEventsCount = getOverviewNetworkStats(overviewNetwork).reduce( + (total, stat) => total + stat.count, + 0 + ); + const formattedNetworkEventsCount = numeral(networkEventsCount).format( + defaultNumberFormat + ); + + return ( + <> + + ) : ( + <>{''} + ) + } + title={ + + } + > + {networkPageButton} + + + + + ); + }} + + + + + ); +}; + +OverviewNetworkComponent.displayName = 'OverviewNetworkComponent'; + +export const OverviewNetwork = React.memo(OverviewNetworkComponent); diff --git a/x-pack/plugins/siem/public/components/page/overview/overview_network_stats/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/overview/components/overview_network_stats/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/page/overview/overview_network_stats/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/overview/components/overview_network_stats/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/overview/components/overview_network_stats/index.test.tsx b/x-pack/plugins/siem/public/overview/components/overview_network_stats/index.test.tsx new file mode 100644 index 00000000000000..bff6ee7d7469d6 --- /dev/null +++ b/x-pack/plugins/siem/public/overview/components/overview_network_stats/index.test.tsx @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mount, shallow } from 'enzyme'; +import React from 'react'; + +import { OverviewNetworkStats } from '.'; +import { mockData } from './mock'; +import { TestProviders } from '../../../common/mock/test_providers'; + +describe('Overview Network Stat Data', () => { + describe('rendering', () => { + test('it renders the default OverviewNetworkStats', () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); + }); + describe('loading', () => { + test('it does NOT show loading indicator when loading is false', () => { + const wrapper = mount( + + + + ); + + // click the accordion to expand it + wrapper + .find('button') + .first() + .simulate('click'); + wrapper.update(); + + expect( + wrapper + .find('[data-test-subj="network-stat-auditbeatSocket"]') + .first() + .find('[data-test-subj="stat-value-loading-spinner"]') + .first() + .exists() + ).toBe(false); + }); + + test('it shows the loading indicator when loading is true', () => { + const wrapper = mount( + + + + ); + + // click the accordion to expand it + wrapper + .find('button') + .first() + .simulate('click'); + wrapper.update(); + + expect( + wrapper + .find('[data-test-subj="network-stat-auditbeatSocket"]') + .first() + .find('[data-test-subj="stat-value-loading-spinner"]') + .first() + .exists() + ).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/overview/components/overview_network_stats/index.tsx b/x-pack/plugins/siem/public/overview/components/overview_network_stats/index.tsx new file mode 100644 index 00000000000000..709f1ffbe5cae5 --- /dev/null +++ b/x-pack/plugins/siem/public/overview/components/overview_network_stats/index.tsx @@ -0,0 +1,195 @@ +/* + * 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 { EuiAccordion, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; +import styled from 'styled-components'; + +import { OverviewNetworkData } from '../../../graphql/types'; +import { FormattedStat, StatGroup } from '../types'; +import { StatValue } from '../stat_value'; + +interface OverviewNetworkProps { + data: OverviewNetworkData; + loading: boolean; +} + +export const getOverviewNetworkStats = (data: OverviewNetworkData): FormattedStat[] => [ + { + count: data.auditbeatSocket ?? 0, + title: ( + + ), + id: 'auditbeatSocket', + }, + { + count: data.filebeatCisco ?? 0, + title: , + id: 'filebeatCisco', + }, + { + count: data.filebeatNetflow ?? 0, + title: ( + + ), + id: 'filebeatNetflow', + }, + { + count: data.filebeatPanw ?? 0, + title: ( + + ), + id: 'filebeatPanw', + }, + { + count: data.filebeatSuricata ?? 0, + title: ( + + ), + id: 'filebeatSuricata', + }, + { + count: data.filebeatZeek ?? 0, + title: , + id: 'filebeatZeek', + }, + { + count: data.packetbeatDNS ?? 0, + title: , + id: 'packetbeatDNS', + }, + { + count: data.packetbeatFlow ?? 0, + title: , + id: 'packetbeatFlow', + }, + { + count: data.packetbeatTLS ?? 0, + title: , + id: 'packetbeatTLS', + }, +]; + +const networkStatGroups: StatGroup[] = [ + { + groupId: 'auditbeat', + name: ( + + ), + statIds: ['auditbeatSocket'], + }, + { + groupId: 'filebeat', + name: ( + + ), + statIds: [ + 'filebeatCisco', + 'filebeatNetflow', + 'filebeatPanw', + 'filebeatSuricata', + 'filebeatZeek', + ], + }, + { + groupId: 'packetbeat', + name: ( + + ), + statIds: ['packetbeatDNS', 'packetbeatFlow', 'packetbeatTLS'], + }, +]; + +const NetworkStatsContainer = styled.div` + .accordion-button { + width: 100%; + } +`; + +const Title = styled.div` + margin-left: 24px; +`; + +const AccordionContent = styled.div` + margin-top: 8px; +`; + +const OverviewNetworkStatsComponent: React.FC = ({ data, loading }) => { + const allNetworkStats = getOverviewNetworkStats(data); + const allNetworkStatsCount = allNetworkStats.reduce((total, stat) => total + stat.count, 0); + + return ( + + {networkStatGroups.map((statGroup, i) => { + const statsForGroup = allNetworkStats.filter(s => statGroup.statIds.includes(s.id)); + const statsForGroupCount = statsForGroup.reduce((total, stat) => total + stat.count, 0); + + return ( + + + + + {statGroup.name} + + + + + + } + buttonContentClassName="accordion-button" + > + + {statsForGroup.map(stat => ( + + + + {stat.title} + + + + + + + ))} + + + + ); + })} + + ); +}; + +export const OverviewNetworkStats = React.memo(OverviewNetworkStatsComponent); diff --git a/x-pack/plugins/siem/public/overview/components/overview_network_stats/mock.ts b/x-pack/plugins/siem/public/overview/components/overview_network_stats/mock.ts new file mode 100644 index 00000000000000..f55d6a1577ccd8 --- /dev/null +++ b/x-pack/plugins/siem/public/overview/components/overview_network_stats/mock.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 { OverviewNetworkData } from '../../../graphql/types'; + +export const mockData: { OverviewNetwork: OverviewNetworkData } = { + OverviewNetwork: { + auditbeatSocket: 12, + filebeatCisco: 999, + filebeatNetflow: 7777, + filebeatPanw: 66, + filebeatSuricata: 60015, + filebeatZeek: 2003, + packetbeatDNS: 10277307, + packetbeatFlow: 16, + packetbeatTLS: 3400000, + }, +}; diff --git a/x-pack/plugins/siem/public/components/recent_cases/filters/index.tsx b/x-pack/plugins/siem/public/overview/components/recent_cases/filters/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/recent_cases/filters/index.tsx rename to x-pack/plugins/siem/public/overview/components/recent_cases/filters/index.tsx diff --git a/x-pack/plugins/siem/public/overview/components/recent_cases/index.tsx b/x-pack/plugins/siem/public/overview/components/recent_cases/index.tsx new file mode 100644 index 00000000000000..03c1754f1b8d5a --- /dev/null +++ b/x-pack/plugins/siem/public/overview/components/recent_cases/index.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiHorizontalRule, EuiLink, EuiText } from '@elastic/eui'; +import React, { useEffect, useMemo, useRef } from 'react'; + +import { FilterOptions, QueryParams } from '../../../cases/containers/types'; +import { DEFAULT_QUERY_PARAMS, useGetCases } from '../../../cases/containers/use_get_cases'; +import { getCaseUrl } from '../../../common/components/link_to/redirect_to_case'; +import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search'; +import { navTabs } from '../../../app/home/home_navigations'; +import { LoadingPlaceholders } from '../loading_placeholders'; +import { NoCases } from './no_cases'; +import { RecentCases } from './recent_cases'; +import * as i18n from './translations'; + +const usePrevious = (value: FilterOptions) => { + const ref = useRef(); + useEffect(() => { + (ref.current as unknown) = value; + }); + return ref.current; +}; + +const MAX_CASES_TO_SHOW = 3; + +const queryParams: QueryParams = { + ...DEFAULT_QUERY_PARAMS, + perPage: MAX_CASES_TO_SHOW, +}; + +const StatefulRecentCasesComponent = React.memo( + ({ filterOptions }: { filterOptions: FilterOptions }) => { + const previousFilterOptions = usePrevious(filterOptions); + const { data, loading, setFilters } = useGetCases(queryParams); + const isLoadingCases = useMemo( + () => loading.indexOf('cases') > -1 || loading.indexOf('caseUpdate') > -1, + [loading] + ); + const search = useGetUrlSearch(navTabs.case); + const allCasesLink = useMemo( + () => {i18n.VIEW_ALL_CASES}, + [search] + ); + + useEffect(() => { + if (previousFilterOptions !== undefined && previousFilterOptions !== filterOptions) { + setFilters(filterOptions); + } + }, [previousFilterOptions, filterOptions, setFilters]); + + const content = useMemo( + () => + isLoadingCases ? ( + + ) : !isLoadingCases && data.cases.length === 0 ? ( + + ) : ( + + ), + [isLoadingCases, data] + ); + + return ( + + {content} + + {allCasesLink} + + ); + } +); + +StatefulRecentCasesComponent.displayName = 'StatefulRecentCasesComponent'; + +export const StatefulRecentCases = React.memo(StatefulRecentCasesComponent); diff --git a/x-pack/plugins/siem/public/overview/components/recent_cases/no_cases/index.tsx b/x-pack/plugins/siem/public/overview/components/recent_cases/no_cases/index.tsx new file mode 100644 index 00000000000000..e29223ca07e65e --- /dev/null +++ b/x-pack/plugins/siem/public/overview/components/recent_cases/no_cases/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 { EuiLink } from '@elastic/eui'; +import React, { useMemo } from 'react'; + +import { getCreateCaseUrl } from '../../../../common/components/link_to/redirect_to_case'; +import { useGetUrlSearch } from '../../../../common/components/navigation/use_get_url_search'; +import { navTabs } from '../../../../app/home/home_navigations'; + +import * as i18n from '../translations'; + +const NoCasesComponent = () => { + const urlSearch = useGetUrlSearch(navTabs.case); + const newCaseLink = useMemo( + () => {` ${i18n.START_A_NEW_CASE}`}, + [urlSearch] + ); + + return ( + <> + {i18n.NO_CASES} + {newCaseLink} + {'!'} + + ); +}; + +NoCasesComponent.displayName = 'NoCasesComponent'; + +export const NoCases = React.memo(NoCasesComponent); diff --git a/x-pack/plugins/siem/public/components/recent_cases/recent_cases.tsx b/x-pack/plugins/siem/public/overview/components/recent_cases/recent_cases.tsx similarity index 82% rename from x-pack/plugins/siem/public/components/recent_cases/recent_cases.tsx rename to x-pack/plugins/siem/public/overview/components/recent_cases/recent_cases.tsx index eb17c75f4111be..9618ddb05716d0 100644 --- a/x-pack/plugins/siem/public/components/recent_cases/recent_cases.tsx +++ b/x-pack/plugins/siem/public/overview/components/recent_cases/recent_cases.tsx @@ -8,11 +8,11 @@ import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiSpacer, EuiText } from '@elastic import React from 'react'; import styled from 'styled-components'; -import { Case } from '../../containers/case/types'; -import { getCaseDetailsUrl } from '../link_to/redirect_to_case'; -import { Markdown } from '../markdown'; -import { useGetUrlSearch } from '../navigation/use_get_url_search'; -import { navTabs } from '../../pages/home/home_navigations'; +import { Case } from '../../../cases/containers/types'; +import { getCaseDetailsUrl } from '../../../common/components/link_to/redirect_to_case'; +import { Markdown } from '../../../common/components/markdown'; +import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search'; +import { navTabs } from '../../../app/home/home_navigations'; import { IconWithCount } from '../recent_timelines/counts'; import * as i18n from './translations'; diff --git a/x-pack/plugins/siem/public/components/recent_cases/translations.ts b/x-pack/plugins/siem/public/overview/components/recent_cases/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/recent_cases/translations.ts rename to x-pack/plugins/siem/public/overview/components/recent_cases/translations.ts diff --git a/x-pack/plugins/siem/public/components/recent_cases/types.ts b/x-pack/plugins/siem/public/overview/components/recent_cases/types.ts similarity index 100% rename from x-pack/plugins/siem/public/components/recent_cases/types.ts rename to x-pack/plugins/siem/public/overview/components/recent_cases/types.ts diff --git a/x-pack/plugins/siem/public/overview/components/recent_timelines/counts/index.tsx b/x-pack/plugins/siem/public/overview/components/recent_timelines/counts/index.tsx new file mode 100644 index 00000000000000..bdb75f8800647b --- /dev/null +++ b/x-pack/plugins/siem/public/overview/components/recent_timelines/counts/index.tsx @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiText, EuiToolTip } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +import { + getPinnedEventCount, + getNotesCount, +} from '../../../../timelines/components/open_timeline/helpers'; +import { OpenTimelineResult } from '../../../../timelines/components/open_timeline/types'; + +import * as i18n from '../translations'; + +const Icon = styled(EuiIcon)` + margin-right: 8px; +`; + +const FlexGroup = styled(EuiFlexGroup)` + margin-right: 16px; +`; + +export const IconWithCount = React.memo<{ count: number; icon: string; tooltip: string }>( + ({ count, icon, tooltip }) => ( + + + + + + + + + {count} + + + + + ) +); + +IconWithCount.displayName = 'IconWithCount'; + +export const RecentTimelineCounts = React.memo<{ + timeline: OpenTimelineResult; +}>(({ timeline }) => { + return ( +
+ + +
+ ); +}); + +RecentTimelineCounts.displayName = 'RecentTimelineCounts'; diff --git a/x-pack/plugins/siem/public/components/recent_timelines/filters/index.tsx b/x-pack/plugins/siem/public/overview/components/recent_timelines/filters/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/recent_timelines/filters/index.tsx rename to x-pack/plugins/siem/public/overview/components/recent_timelines/filters/index.tsx diff --git a/x-pack/plugins/siem/public/overview/components/recent_timelines/header/index.tsx b/x-pack/plugins/siem/public/overview/components/recent_timelines/header/index.tsx new file mode 100644 index 00000000000000..07144840dae11c --- /dev/null +++ b/x-pack/plugins/siem/public/overview/components/recent_timelines/header/index.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiText, EuiLink } from '@elastic/eui'; +import React, { useCallback } from 'react'; + +import { isUntitled } from '../../../../timelines/components/open_timeline/helpers'; +import { + OnOpenTimeline, + OpenTimelineResult, +} from '../../../../timelines/components/open_timeline/types'; +import * as i18n from '../translations'; + +export const RecentTimelineHeader = React.memo<{ + onOpenTimeline: OnOpenTimeline; + timeline: OpenTimelineResult; +}>(({ onOpenTimeline, timeline, timeline: { title, savedObjectId } }) => { + const onClick = useCallback( + () => onOpenTimeline({ duplicate: false, timelineId: `${savedObjectId}` }), + [onOpenTimeline, savedObjectId] + ); + + return ( + + {isUntitled(timeline) ? i18n.UNTITLED_TIMELINE : title} + + ); +}); + +RecentTimelineHeader.displayName = 'RecentTimelineHeader'; diff --git a/x-pack/plugins/siem/public/overview/components/recent_timelines/index.tsx b/x-pack/plugins/siem/public/overview/components/recent_timelines/index.tsx new file mode 100644 index 00000000000000..75b157a282eeb9 --- /dev/null +++ b/x-pack/plugins/siem/public/overview/components/recent_timelines/index.tsx @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import ApolloClient from 'apollo-client'; +import { EuiHorizontalRule, EuiLink, EuiText } from '@elastic/eui'; +import React, { useCallback, useMemo, useEffect } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import { Dispatch } from 'redux'; + +import { TimelineType } from '../../../../common/types/timeline'; +import { useGetAllTimeline } from '../../../timelines/containers/all'; +import { SortFieldTimeline, Direction } from '../../../graphql/types'; +import { + queryTimelineById, + dispatchUpdateTimeline, +} from '../../../timelines/components/open_timeline/helpers'; +import { OnOpenTimeline } from '../../../timelines/components/open_timeline/types'; +import { updateIsLoading as dispatchUpdateIsLoading } from '../../../timelines/store/timeline/actions'; + +import { RecentTimelines } from './recent_timelines'; +import * as i18n from './translations'; +import { FilterMode } from './types'; +import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search'; +import { navTabs } from '../../../app/home/home_navigations'; +import { getTimelinesUrl } from '../../../common/components/link_to/redirect_to_timelines'; +import { LoadingPlaceholders } from '../loading_placeholders'; + +interface OwnProps { + apolloClient: ApolloClient<{}>; + filterBy: FilterMode; +} + +export type Props = OwnProps & PropsFromRedux; + +const PAGE_SIZE = 3; + +const StatefulRecentTimelinesComponent = React.memo( + ({ apolloClient, filterBy, updateIsLoading, updateTimeline }) => { + const onOpenTimeline: OnOpenTimeline = useCallback( + ({ duplicate, timelineId }: { duplicate: boolean; timelineId: string }) => { + queryTimelineById({ + apolloClient, + duplicate, + timelineId, + updateIsLoading, + updateTimeline, + }); + }, + [apolloClient, updateIsLoading, updateTimeline] + ); + + const noTimelinesMessage = + filterBy === 'favorites' ? i18n.NO_FAVORITE_TIMELINES : i18n.NO_TIMELINES; + const urlSearch = useGetUrlSearch(navTabs.timelines); + const linkAllTimelines = useMemo( + () => {i18n.VIEW_ALL_TIMELINES}, + [urlSearch] + ); + const loadingPlaceholders = useMemo( + () => ( + + ), + [filterBy] + ); + + const { fetchAllTimeline, timelines, loading } = useGetAllTimeline(); + + useEffect(() => { + fetchAllTimeline({ + pageInfo: { + pageIndex: 1, + pageSize: PAGE_SIZE, + }, + search: '', + sort: { + sortField: SortFieldTimeline.updated, + sortOrder: Direction.desc, + }, + onlyUserFavorite: filterBy === 'favorites', + timelineType: TimelineType.default, + }); + }, [filterBy]); + + return ( + <> + {loading ? ( + loadingPlaceholders + ) : ( + + )} + + {linkAllTimelines} + + ); + } +); + +StatefulRecentTimelinesComponent.displayName = 'StatefulRecentTimelinesComponent'; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + updateIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => + dispatch(dispatchUpdateIsLoading({ id, isLoading })), + updateTimeline: dispatchUpdateTimeline(dispatch), +}); + +const connector = connect(null, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const StatefulRecentTimelines = connector(StatefulRecentTimelinesComponent); diff --git a/x-pack/plugins/siem/public/components/recent_timelines/recent_timelines.tsx b/x-pack/plugins/siem/public/overview/components/recent_timelines/recent_timelines.tsx similarity index 93% rename from x-pack/plugins/siem/public/components/recent_timelines/recent_timelines.tsx rename to x-pack/plugins/siem/public/overview/components/recent_timelines/recent_timelines.tsx index dbcd3fe721ea36..ba6fcad2a03e9b 100644 --- a/x-pack/plugins/siem/public/components/recent_timelines/recent_timelines.tsx +++ b/x-pack/plugins/siem/public/overview/components/recent_timelines/recent_timelines.tsx @@ -15,8 +15,11 @@ import { import React from 'react'; import { RecentTimelineHeader } from './header'; -import { OnOpenTimeline, OpenTimelineResult } from '../open_timeline/types'; -import { WithHoverActions } from '../with_hover_actions'; +import { + OnOpenTimeline, + OpenTimelineResult, +} from '../../../timelines/components/open_timeline/types'; +import { WithHoverActions } from '../../../common/components/with_hover_actions'; import { RecentTimelineCounts } from './counts'; import * as i18n from './translations'; diff --git a/x-pack/plugins/siem/public/components/recent_timelines/translations.ts b/x-pack/plugins/siem/public/overview/components/recent_timelines/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/recent_timelines/translations.ts rename to x-pack/plugins/siem/public/overview/components/recent_timelines/translations.ts diff --git a/x-pack/plugins/siem/public/components/recent_timelines/types.ts b/x-pack/plugins/siem/public/overview/components/recent_timelines/types.ts similarity index 100% rename from x-pack/plugins/siem/public/components/recent_timelines/types.ts rename to x-pack/plugins/siem/public/overview/components/recent_timelines/types.ts diff --git a/x-pack/plugins/siem/public/overview/components/sidebar/index.tsx b/x-pack/plugins/siem/public/overview/components/sidebar/index.tsx new file mode 100644 index 00000000000000..773750f3d0cc57 --- /dev/null +++ b/x-pack/plugins/siem/public/overview/components/sidebar/index.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState } from 'react'; + +import { FilterMode as RecentTimelinesFilterMode } from '../recent_timelines/types'; +import { FilterMode as RecentCasesFilterMode } from '../recent_cases/types'; + +import { Sidebar } from './sidebar'; + +export const StatefulSidebar = React.memo(() => { + const [recentTimelinesFilterBy, setRecentTimelinesFilterBy] = useState( + 'favorites' + ); + const [recentCasesFilterBy, setRecentCasesFilterBy] = useState( + 'recentlyCreated' + ); + + return ( + + ); +}); + +StatefulSidebar.displayName = 'StatefulSidebar'; diff --git a/x-pack/plugins/siem/public/pages/overview/sidebar/sidebar.tsx b/x-pack/plugins/siem/public/overview/components/sidebar/sidebar.tsx similarity index 78% rename from x-pack/plugins/siem/public/pages/overview/sidebar/sidebar.tsx rename to x-pack/plugins/siem/public/overview/components/sidebar/sidebar.tsx index c972fd83cc88f8..81de0ed5035956 100644 --- a/x-pack/plugins/siem/public/pages/overview/sidebar/sidebar.tsx +++ b/x-pack/plugins/siem/public/overview/components/sidebar/sidebar.tsx @@ -9,19 +9,19 @@ import React, { useMemo } from 'react'; import styled from 'styled-components'; import { ENABLE_NEWS_FEED_SETTING, NEWS_FEED_URL_SETTING } from '../../../../common/constants'; -import { Filters as RecentCasesFilters } from '../../../components/recent_cases/filters'; -import { Filters as RecentTimelinesFilters } from '../../../components/recent_timelines/filters'; -import { StatefulRecentCases } from '../../../components/recent_cases'; -import { StatefulRecentTimelines } from '../../../components/recent_timelines'; -import { StatefulNewsFeed } from '../../../components/news_feed'; -import { FilterMode as RecentTimelinesFilterMode } from '../../../components/recent_timelines/types'; -import { FilterMode as RecentCasesFilterMode } from '../../../components/recent_cases/types'; -import { DEFAULT_FILTER_OPTIONS } from '../../../containers/case/use_get_cases'; -import { SidebarHeader } from '../../../components/sidebar_header'; -import { useCurrentUser } from '../../../lib/kibana'; -import { useApolloClient } from '../../../utils/apollo_context'; +import { Filters as RecentCasesFilters } from '../recent_cases/filters'; +import { Filters as RecentTimelinesFilters } from '../recent_timelines/filters'; +import { StatefulRecentCases } from '../recent_cases'; +import { StatefulRecentTimelines } from '../recent_timelines'; +import { StatefulNewsFeed } from '../../../common/components/news_feed'; +import { FilterMode as RecentTimelinesFilterMode } from '../recent_timelines/types'; +import { FilterMode as RecentCasesFilterMode } from '../recent_cases/types'; +import { DEFAULT_FILTER_OPTIONS } from '../../../cases/containers/use_get_cases'; +import { SidebarHeader } from '../../../common/components/sidebar_header'; +import { useCurrentUser } from '../../../common/lib/kibana'; +import { useApolloClient } from '../../../common/utils/apollo_context'; -import * as i18n from '../translations'; +import * as i18n from '../../pages/translations'; const SidebarFlexGroup = styled(EuiFlexGroup)` width: 305px; diff --git a/x-pack/plugins/siem/public/overview/components/signals_by_category/index.tsx b/x-pack/plugins/siem/public/overview/components/signals_by_category/index.tsx new file mode 100644 index 00000000000000..def7342ff76b2f --- /dev/null +++ b/x-pack/plugins/siem/public/overview/components/signals_by_category/index.tsx @@ -0,0 +1,94 @@ +/* + * 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, { useCallback } from 'react'; + +import { SignalsHistogramPanel } from '../../../alerts/components/signals_histogram_panel'; +import { signalsHistogramOptions } from '../../../alerts/components/signals_histogram_panel/config'; +import { useSignalIndex } from '../../../alerts/containers/detection_engine/signals/use_signal_index'; +import { SetAbsoluteRangeDatePicker } from '../../../network/pages/types'; +import { Filter, IIndexPattern, Query } from '../../../../../../../src/plugins/data/public'; +import { inputsModel } from '../../../common/store'; +import { InputsModelId } from '../../../common/store/inputs/constants'; +import * as i18n from '../../pages/translations'; +import { UpdateDateRange } from '../../../common/components/charts/common'; + +const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; +const DEFAULT_STACK_BY = 'signal.rule.threat.tactic.name'; +const NO_FILTERS: Filter[] = []; + +interface Props { + deleteQuery?: ({ id }: { id: string }) => void; + filters?: Filter[]; + from: number; + headerChildren?: React.ReactNode; + indexPattern: IIndexPattern; + /** Override all defaults, and only display this field */ + onlyField?: string; + query?: Query; + setAbsoluteRangeDatePicker: SetAbsoluteRangeDatePicker; + setAbsoluteRangeDatePickerTarget?: InputsModelId; + setQuery: (params: { + id: string; + inspect: inputsModel.InspectQuery | null; + loading: boolean; + refetch: inputsModel.Refetch; + }) => void; + to: number; +} + +const SignalsByCategoryComponent: React.FC = ({ + deleteQuery, + filters = NO_FILTERS, + from, + headerChildren, + onlyField, + query = DEFAULT_QUERY, + setAbsoluteRangeDatePicker, + setAbsoluteRangeDatePickerTarget = 'global', + setQuery, + to, +}) => { + const { signalIndexName } = useSignalIndex(); + const updateDateRangeCallback = useCallback( + ({ x }) => { + if (!x) { + return; + } + const [min, max] = x; + setAbsoluteRangeDatePicker({ id: setAbsoluteRangeDatePickerTarget, from: min, to: max }); + }, + [setAbsoluteRangeDatePicker] + ); + + const defaultStackByOption = + signalsHistogramOptions.find(o => o.text === DEFAULT_STACK_BY) ?? signalsHistogramOptions[0]; + + return ( + + ); +}; + +SignalsByCategoryComponent.displayName = 'SignalsByCategoryComponent'; + +export const SignalsByCategory = React.memo(SignalsByCategoryComponent); diff --git a/x-pack/plugins/siem/public/components/page/overview/stat_value.tsx b/x-pack/plugins/siem/public/overview/components/stat_value.tsx similarity index 94% rename from x-pack/plugins/siem/public/components/page/overview/stat_value.tsx rename to x-pack/plugins/siem/public/overview/components/stat_value.tsx index 7615001eec9da3..dd50a9599e142f 100644 --- a/x-pack/plugins/siem/public/components/page/overview/stat_value.tsx +++ b/x-pack/plugins/siem/public/overview/components/stat_value.tsx @@ -9,8 +9,8 @@ import numeral from '@elastic/numeral'; import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; -import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; -import { useUiSetting$ } from '../../../lib/kibana'; +import { DEFAULT_NUMBER_FORMAT } from '../../../common/constants'; +import { useUiSetting$ } from '../../common/lib/kibana'; const ProgressContainer = styled.div` margin-left: 8px; diff --git a/x-pack/plugins/siem/public/components/page/overview/types.ts b/x-pack/plugins/siem/public/overview/components/types.ts similarity index 100% rename from x-pack/plugins/siem/public/components/page/overview/types.ts rename to x-pack/plugins/siem/public/overview/components/types.ts diff --git a/x-pack/plugins/siem/public/containers/overview/overview_host/index.gql_query.ts b/x-pack/plugins/siem/public/overview/containers/overview_host/index.gql_query.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/overview/overview_host/index.gql_query.ts rename to x-pack/plugins/siem/public/overview/containers/overview_host/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/overview/containers/overview_host/index.tsx b/x-pack/plugins/siem/public/overview/containers/overview_host/index.tsx new file mode 100644 index 00000000000000..89761e104d70f2 --- /dev/null +++ b/x-pack/plugins/siem/public/overview/containers/overview_host/index.tsx @@ -0,0 +1,89 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect, ConnectedProps } from 'react-redux'; + +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { GetOverviewHostQuery, OverviewHostData } from '../../../graphql/types'; +import { useUiSetting } from '../../../common/lib/kibana'; +import { inputsModel, inputsSelectors } from '../../../common/store/inputs'; +import { State } from '../../../common/store'; +import { createFilter, getDefaultFetchPolicy } from '../../../common/containers/helpers'; +import { QueryTemplateProps } from '../../../common/containers/query_template'; + +import { overviewHostQuery } from './index.gql_query'; + +export const ID = 'overviewHostQuery'; + +export interface OverviewHostArgs { + id: string; + inspect: inputsModel.InspectQuery; + loading: boolean; + overviewHost: OverviewHostData; + refetch: inputsModel.Refetch; +} + +export interface OverviewHostProps extends QueryTemplateProps { + children: (args: OverviewHostArgs) => React.ReactNode; + sourceId: string; + endDate: number; + startDate: number; +} + +const OverviewHostComponentQuery = React.memo( + ({ id = ID, children, filterQuery, isInspected, sourceId, startDate, endDate }) => { + return ( + + query={overviewHostQuery} + fetchPolicy={getDefaultFetchPolicy()} + variables={{ + sourceId, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + filterQuery: createFilter(filterQuery), + defaultIndex: useUiSetting(DEFAULT_INDEX_KEY), + inspect: isInspected, + }} + > + {({ data, loading, refetch }) => { + const overviewHost = getOr({}, `source.OverviewHost`, data); + return children({ + id, + inspect: getOr(null, 'source.OverviewHost.inspect', data), + overviewHost, + loading, + refetch, + }); + }} + + ); + } +); + +OverviewHostComponentQuery.displayName = 'OverviewHostComponentQuery'; + +const makeMapStateToProps = () => { + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = ID }: OverviewHostProps) => { + const { isInspected } = getQuery(state, id); + return { + isInspected, + }; + }; + return mapStateToProps; +}; + +const connector = connect(makeMapStateToProps); + +type PropsFromRedux = ConnectedProps; + +export const OverviewHostQuery = connector(OverviewHostComponentQuery); diff --git a/x-pack/plugins/siem/public/containers/overview/overview_network/index.gql_query.ts b/x-pack/plugins/siem/public/overview/containers/overview_network/index.gql_query.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/overview/overview_network/index.gql_query.ts rename to x-pack/plugins/siem/public/overview/containers/overview_network/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/overview/containers/overview_network/index.tsx b/x-pack/plugins/siem/public/overview/containers/overview_network/index.tsx new file mode 100644 index 00000000000000..86242adf3f47fa --- /dev/null +++ b/x-pack/plugins/siem/public/overview/containers/overview_network/index.tsx @@ -0,0 +1,88 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { connect, ConnectedProps } from 'react-redux'; + +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { GetOverviewNetworkQuery, OverviewNetworkData } from '../../../graphql/types'; +import { useUiSetting } from '../../../common/lib/kibana'; +import { State } from '../../../common/store'; +import { inputsModel, inputsSelectors } from '../../../common/store/inputs'; +import { createFilter, getDefaultFetchPolicy } from '../../../common/containers/helpers'; +import { QueryTemplateProps } from '../../../common/containers/query_template'; + +import { overviewNetworkQuery } from './index.gql_query'; + +export const ID = 'overviewNetworkQuery'; + +export interface OverviewNetworkArgs { + id: string; + inspect: inputsModel.InspectQuery; + overviewNetwork: OverviewNetworkData; + loading: boolean; + refetch: inputsModel.Refetch; +} + +export interface OverviewNetworkProps extends QueryTemplateProps { + children: (args: OverviewNetworkArgs) => React.ReactNode; + sourceId: string; + endDate: number; + startDate: number; +} + +export const OverviewNetworkComponentQuery = React.memo( + ({ id = ID, children, filterQuery, isInspected, sourceId, startDate, endDate }) => ( + + query={overviewNetworkQuery} + fetchPolicy={getDefaultFetchPolicy()} + notifyOnNetworkStatusChange + variables={{ + sourceId, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + filterQuery: createFilter(filterQuery), + defaultIndex: useUiSetting(DEFAULT_INDEX_KEY), + inspect: isInspected, + }} + > + {({ data, loading, refetch }) => { + const overviewNetwork = getOr({}, `source.OverviewNetwork`, data); + return children({ + id, + inspect: getOr(null, 'source.OverviewNetwork.inspect', data), + overviewNetwork, + loading, + refetch, + }); + }} + + ) +); + +OverviewNetworkComponentQuery.displayName = 'OverviewNetworkComponentQuery'; + +const makeMapStateToProps = () => { + const getQuery = inputsSelectors.globalQueryByIdSelector(); + const mapStateToProps = (state: State, { id = ID }: OverviewNetworkProps) => { + const { isInspected } = getQuery(state, id); + return { + isInspected, + }; + }; + return mapStateToProps; +}; + +const connector = connect(makeMapStateToProps); + +type PropsFromRedux = ConnectedProps; + +export const OverviewNetworkQuery = connector(OverviewNetworkComponentQuery); diff --git a/x-pack/plugins/siem/public/overview/index.ts b/x-pack/plugins/siem/public/overview/index.ts new file mode 100644 index 00000000000000..bdf855b3851c82 --- /dev/null +++ b/x-pack/plugins/siem/public/overview/index.ts @@ -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. + */ + +import { SecuritySubPlugin } from '../app/types'; +import { getOverviewRoutes } from './routes'; + +export class Overview { + public setup() {} + + public start(): SecuritySubPlugin { + return { + routes: getOverviewRoutes(), + }; + } +} diff --git a/x-pack/plugins/siem/public/pages/overview/index.tsx b/x-pack/plugins/siem/public/overview/pages/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/pages/overview/index.tsx rename to x-pack/plugins/siem/public/overview/pages/index.tsx diff --git a/x-pack/plugins/siem/public/pages/overview/overview.test.tsx b/x-pack/plugins/siem/public/overview/pages/overview.test.tsx similarity index 88% rename from x-pack/plugins/siem/public/pages/overview/overview.test.tsx rename to x-pack/plugins/siem/public/overview/pages/overview.test.tsx index c129258fa2e87b..36174bdb94a3f1 100644 --- a/x-pack/plugins/siem/public/pages/overview/overview.test.tsx +++ b/x-pack/plugins/siem/public/overview/pages/overview.test.tsx @@ -10,19 +10,19 @@ import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; import { MemoryRouter } from 'react-router-dom'; -import '../../mock/match_media'; -import { TestProviders } from '../../mock'; -import { mocksSource } from '../../containers/source/mock'; +import '../../common/mock/match_media'; +import { TestProviders } from '../../common/mock'; +import { mocksSource } from '../../common/containers/source/mock'; import { Overview } from './index'; -jest.mock('../../lib/kibana'); +jest.mock('../../common/lib/kibana'); // Test will fail because we will to need to mock some core services to make the test work // For now let's forget about SiemSearchBar and QueryBar -jest.mock('../../components/search_bar', () => ({ +jest.mock('../../common/components/search_bar', () => ({ SiemSearchBar: () => null, })); -jest.mock('../../components/query_bar', () => ({ +jest.mock('../../common/components/query_bar', () => ({ QueryBar: () => null, })); diff --git a/x-pack/plugins/siem/public/pages/overview/overview.tsx b/x-pack/plugins/siem/public/overview/pages/overview.tsx similarity index 82% rename from x-pack/plugins/siem/public/pages/overview/overview.tsx rename to x-pack/plugins/siem/public/overview/pages/overview.tsx index 82f44447289022..57a82f6f254f27 100644 --- a/x-pack/plugins/siem/public/pages/overview/overview.tsx +++ b/x-pack/plugins/siem/public/overview/pages/overview.tsx @@ -11,20 +11,23 @@ import { StickyContainer } from 'react-sticky'; import { Query, Filter } from 'src/plugins/data/public'; import styled from 'styled-components'; -import { AlertsByCategory } from './alerts_by_category'; -import { FiltersGlobal } from '../../components/filters_global'; -import { SiemSearchBar } from '../../components/search_bar'; -import { WrapperPage } from '../../components/wrapper_page'; -import { GlobalTime } from '../../containers/global_time'; -import { WithSource, indicesExistOrDataTemporarilyUnavailable } from '../../containers/source'; -import { EventsByDataset } from './events_by_dataset'; -import { EventCounts } from './event_counts'; -import { OverviewEmpty } from './overview_empty'; -import { StatefulSidebar } from './sidebar'; -import { SignalsByCategory } from './signals_by_category'; -import { inputsSelectors, State } from '../../store'; -import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions'; -import { SpyRoute } from '../../utils/route/spy_routes'; +import { AlertsByCategory } from '../components/alerts_by_category'; +import { FiltersGlobal } from '../../common/components/filters_global'; +import { SiemSearchBar } from '../../common/components/search_bar'; +import { WrapperPage } from '../../common/components/wrapper_page'; +import { GlobalTime } from '../../common/containers/global_time'; +import { + WithSource, + indicesExistOrDataTemporarilyUnavailable, +} from '../../common/containers/source'; +import { EventsByDataset } from '../components/events_by_dataset'; +import { EventCounts } from '../components/event_counts'; +import { OverviewEmpty } from '../components/overview_empty'; +import { StatefulSidebar } from '../components/sidebar'; +import { SignalsByCategory } from '../components/signals_by_category'; +import { inputsSelectors, State } from '../../common/store'; +import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../common/store/inputs/actions'; +import { SpyRoute } from '../../common/utils/route/spy_routes'; const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; const NO_FILTERS: Filter[] = []; diff --git a/x-pack/plugins/siem/public/pages/overview/summary.tsx b/x-pack/plugins/siem/public/overview/pages/summary.tsx similarity index 98% rename from x-pack/plugins/siem/public/pages/overview/summary.tsx rename to x-pack/plugins/siem/public/overview/pages/summary.tsx index da16cb28c61711..1e08a2cdca8e75 100644 --- a/x-pack/plugins/siem/public/pages/overview/summary.tsx +++ b/x-pack/plugins/siem/public/overview/pages/summary.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { EuiFlexItem, EuiLink, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useKibana } from '../../lib/kibana'; +import { useKibana } from '../../common/lib/kibana'; export const Summary = React.memo(() => { const docLinks = useKibana().services.docLinks; diff --git a/x-pack/plugins/siem/public/pages/overview/translations.ts b/x-pack/plugins/siem/public/overview/pages/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/overview/translations.ts rename to x-pack/plugins/siem/public/overview/pages/translations.ts diff --git a/x-pack/plugins/siem/public/overview/routes.tsx b/x-pack/plugins/siem/public/overview/routes.tsx new file mode 100644 index 00000000000000..fc41227b27c04f --- /dev/null +++ b/x-pack/plugins/siem/public/overview/routes.tsx @@ -0,0 +1,15 @@ +/* + * 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 { Route } from 'react-router-dom'; + +import { Overview } from './pages'; +import { SiemPageName } from '../app/types'; + +export const getOverviewRoutes = () => [ + } />, +]; diff --git a/x-pack/plugins/siem/public/pages/case/case.tsx b/x-pack/plugins/siem/public/pages/case/case.tsx deleted file mode 100644 index 2b613f6692df12..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/case.tsx +++ /dev/null @@ -1,38 +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 { WrapperPage } from '../../components/wrapper_page'; -import { useGetUserSavedObjectPermissions } from '../../lib/kibana'; -import { SpyRoute } from '../../utils/route/spy_routes'; -import { AllCases } from './components/all_cases'; - -import { savedObjectReadOnly, CaseCallOut } from './components/callout'; -import { CaseSavedObjectNoPermissions } from './saved_object_no_permissions'; - -export const CasesPage = React.memo(() => { - const userPermissions = useGetUserSavedObjectPermissions(); - - return userPermissions == null || userPermissions?.read ? ( - <> - - {userPermissions != null && !userPermissions?.crud && userPermissions?.read && ( - - )} - - - - - ) : ( - - ); -}); - -CasesPage.displayName = 'CasesPage'; diff --git a/x-pack/plugins/siem/public/pages/case/case_details.tsx b/x-pack/plugins/siem/public/pages/case/case_details.tsx deleted file mode 100644 index 4bb8afa7f8d42a..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/case_details.tsx +++ /dev/null @@ -1,38 +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 { useParams, Redirect } from 'react-router-dom'; - -import { useGetUrlSearch } from '../../components/navigation/use_get_url_search'; -import { useGetUserSavedObjectPermissions } from '../../lib/kibana'; -import { SpyRoute } from '../../utils/route/spy_routes'; -import { getCaseUrl } from '../../components/link_to'; -import { navTabs } from '../home/home_navigations'; -import { CaseView } from './components/case_view'; -import { savedObjectReadOnly, CaseCallOut } from './components/callout'; - -export const CaseDetailsPage = React.memo(() => { - const userPermissions = useGetUserSavedObjectPermissions(); - const { detailName: caseId } = useParams(); - const search = useGetUrlSearch(navTabs.case); - - if (userPermissions != null && !userPermissions.read) { - return ; - } - - return caseId != null ? ( - <> - {userPermissions != null && !userPermissions?.crud && userPermissions?.read && ( - - )} - - - - ) : null; -}); - -CaseDetailsPage.displayName = 'CaseDetailsPage'; diff --git a/x-pack/plugins/siem/public/pages/case/components/add_comment/index.test.tsx b/x-pack/plugins/siem/public/pages/case/components/add_comment/index.test.tsx deleted file mode 100644 index 7ba8ec96662532..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/add_comment/index.test.tsx +++ /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. - */ - -import React from 'react'; -import { mount } from 'enzyme'; - -import { AddComment } from './'; -import { TestProviders } from '../../../../mock'; -import { getFormMock } from '../__mock__/form'; -import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; - -import { useInsertTimeline } from '../../../../components/timeline/insert_timeline_popover/use_insert_timeline'; -import { usePostComment } from '../../../../containers/case/use_post_comment'; -import { useForm } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form'; -import { wait } from '../../../../lib/helpers'; -jest.mock( - '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' -); -jest.mock('../../../../components/timeline/insert_timeline_popover/use_insert_timeline'); -jest.mock('../../../../containers/case/use_post_comment'); - -export const useFormMock = useForm as jest.Mock; - -const useInsertTimelineMock = useInsertTimeline as jest.Mock; -const usePostCommentMock = usePostComment as jest.Mock; - -const onCommentSaving = jest.fn(); -const onCommentPosted = jest.fn(); -const postComment = jest.fn(); -const handleCursorChange = jest.fn(); -const handleOnTimelineChange = jest.fn(); - -const addCommentProps = { - caseId: '1234', - disabled: false, - insertQuote: null, - onCommentSaving, - onCommentPosted, - showLoading: false, -}; - -const defaultInsertTimeline = { - cursorPosition: { - start: 0, - end: 0, - }, - handleCursorChange, - handleOnTimelineChange, -}; - -const defaultPostCommment = { - isLoading: false, - isError: false, - postComment, -}; -const sampleData = { - comment: 'what a cool comment', -}; -describe('AddComment ', () => { - const formHookMock = getFormMock(sampleData); - - beforeEach(() => { - jest.resetAllMocks(); - useInsertTimelineMock.mockImplementation(() => defaultInsertTimeline); - usePostCommentMock.mockImplementation(() => defaultPostCommment); - useFormMock.mockImplementation(() => ({ form: formHookMock })); - jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); - }); - - it('should post comment on submit click', async () => { - const wrapper = mount( - - - - - - ); - expect(wrapper.find(`[data-test-subj="add-comment"]`).exists()).toBeTruthy(); - expect(wrapper.find(`[data-test-subj="loading-spinner"]`).exists()).toBeFalsy(); - - wrapper - .find(`[data-test-subj="submit-comment"]`) - .first() - .simulate('click'); - await wait(); - expect(onCommentSaving).toBeCalled(); - expect(postComment).toBeCalledWith(sampleData, onCommentPosted); - expect(formHookMock.reset).toBeCalled(); - }); - - it('should render spinner and disable submit when loading', () => { - usePostCommentMock.mockImplementation(() => ({ ...defaultPostCommment, isLoading: true })); - const wrapper = mount( - - - - - - ); - expect(wrapper.find(`[data-test-subj="loading-spinner"]`).exists()).toBeTruthy(); - expect( - wrapper - .find(`[data-test-subj="submit-comment"]`) - .first() - .prop('isDisabled') - ).toBeTruthy(); - }); - - it('should disable submit button when disabled prop passed', () => { - usePostCommentMock.mockImplementation(() => ({ ...defaultPostCommment, isLoading: true })); - const wrapper = mount( - - - - - - ); - expect( - wrapper - .find(`[data-test-subj="submit-comment"]`) - .first() - .prop('isDisabled') - ).toBeTruthy(); - }); - - it('should insert a quote if one is available', () => { - const sampleQuote = 'what a cool quote'; - mount( - - - - - - ); - - expect(formHookMock.setFieldValue).toBeCalledWith( - 'comment', - `${sampleData.comment}\n\n${sampleQuote}` - ); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/case/components/add_comment/index.tsx b/x-pack/plugins/siem/public/pages/case/components/add_comment/index.tsx deleted file mode 100644 index aa987b277da06a..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/add_comment/index.tsx +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiButton, EuiLoadingSpinner } from '@elastic/eui'; -import React, { useCallback, useEffect } from 'react'; -import styled from 'styled-components'; - -import { CommentRequest } from '../../../../../../case/common/api'; -import { usePostComment } from '../../../../containers/case/use_post_comment'; -import { Case } from '../../../../containers/case/types'; -import { MarkdownEditorForm } from '../../../../components/markdown_editor/form'; -import { InsertTimelinePopover } from '../../../../components/timeline/insert_timeline_popover'; -import { useInsertTimeline } from '../../../../components/timeline/insert_timeline_popover/use_insert_timeline'; -import { Form, useForm, UseField } from '../../../../shared_imports'; - -import * as i18n from '../../translations'; -import { schema } from './schema'; - -const MySpinner = styled(EuiLoadingSpinner)` - position: absolute; - top: 50%; - left: 50%; -`; - -const initialCommentValue: CommentRequest = { - comment: '', -}; - -interface AddCommentProps { - caseId: string; - disabled?: boolean; - insertQuote: string | null; - onCommentSaving?: () => void; - onCommentPosted: (newCase: Case) => void; - showLoading?: boolean; -} - -export const AddComment = React.memo( - ({ caseId, disabled, insertQuote, showLoading = true, onCommentPosted, onCommentSaving }) => { - const { isLoading, postComment } = usePostComment(caseId); - const { form } = useForm({ - defaultValue: initialCommentValue, - options: { stripEmptyFields: false }, - schema, - }); - const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline( - form, - 'comment' - ); - - useEffect(() => { - if (insertQuote !== null) { - const { comment } = form.getFormData(); - form.setFieldValue( - 'comment', - `${comment}${comment.length > 0 ? '\n\n' : ''}${insertQuote}` - ); - } - }, [insertQuote]); - - const onSubmit = useCallback(async () => { - const { isValid, data } = await form.submit(); - if (isValid) { - if (onCommentSaving != null) { - onCommentSaving(); - } - await postComment(data, onCommentPosted); - form.reset(); - } - }, [form, onCommentPosted, onCommentSaving]); - return ( - - {isLoading && showLoading && } -
- - {i18n.ADD_COMMENT} - - ), - topRightContent: ( - - ), - }} - /> - -
- ); - } -); - -AddComment.displayName = 'AddComment'; diff --git a/x-pack/plugins/siem/public/pages/case/components/add_comment/schema.tsx b/x-pack/plugins/siem/public/pages/case/components/add_comment/schema.tsx deleted file mode 100644 index ad73fd71b8e115..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/add_comment/schema.tsx +++ /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 { CommentRequest } from '../../../../../../case/common/api'; -import { FIELD_TYPES, fieldValidators, FormSchema } from '../../../../shared_imports'; -import * as i18n from '../../translations'; - -const { emptyField } = fieldValidators; - -export const schema: FormSchema = { - comment: { - type: FIELD_TYPES.TEXTAREA, - validations: [ - { - validator: emptyField(i18n.COMMENT_REQUIRED), - }, - ], - }, -}; diff --git a/x-pack/plugins/siem/public/pages/case/components/all_cases/actions.tsx b/x-pack/plugins/siem/public/pages/case/components/all_cases/actions.tsx deleted file mode 100644 index 01b501bf6cf078..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/all_cases/actions.tsx +++ /dev/null @@ -1,61 +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 { DefaultItemIconButtonAction } from '@elastic/eui/src/components/basic_table/action_types'; -import { Dispatch } from 'react'; -import { Case } from '../../../../containers/case/types'; - -import * as i18n from './translations'; -import { UpdateCase } from '../../../../containers/case/use_get_cases'; - -interface GetActions { - caseStatus: string; - dispatchUpdate: Dispatch>; - deleteCaseOnClick: (deleteCase: Case) => void; -} - -export const getActions = ({ - caseStatus, - dispatchUpdate, - deleteCaseOnClick, -}: GetActions): Array> => [ - { - description: i18n.DELETE_CASE, - icon: 'trash', - name: i18n.DELETE_CASE, - onClick: deleteCaseOnClick, - type: 'icon', - 'data-test-subj': 'action-delete', - }, - caseStatus === 'open' - ? { - description: i18n.CLOSE_CASE, - icon: 'folderCheck', - name: i18n.CLOSE_CASE, - onClick: (theCase: Case) => - dispatchUpdate({ - updateKey: 'status', - updateValue: 'closed', - caseId: theCase.id, - version: theCase.version, - }), - type: 'icon', - 'data-test-subj': 'action-close', - } - : { - description: i18n.REOPEN_CASE, - icon: 'folderExclamation', - name: i18n.REOPEN_CASE, - onClick: (theCase: Case) => - dispatchUpdate({ - updateKey: 'status', - updateValue: 'open', - caseId: theCase.id, - version: theCase.version, - }), - type: 'icon', - 'data-test-subj': 'action-open', - }, -]; diff --git a/x-pack/plugins/siem/public/pages/case/components/all_cases/columns.tsx b/x-pack/plugins/siem/public/pages/case/components/all_cases/columns.tsx deleted file mode 100644 index 9a0460009ffacc..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/all_cases/columns.tsx +++ /dev/null @@ -1,215 +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, { useCallback } from 'react'; -import { - EuiBadge, - EuiTableFieldDataColumnType, - EuiTableComputedColumnType, - EuiTableActionsColumnType, - EuiAvatar, - EuiLink, -} from '@elastic/eui'; -import styled from 'styled-components'; -import { DefaultItemIconButtonAction } from '@elastic/eui/src/components/basic_table/action_types'; -import { getEmptyTagValue } from '../../../../components/empty_value'; -import { Case } from '../../../../containers/case/types'; -import { FormattedRelativePreferenceDate } from '../../../../components/formatted_date'; -import { CaseDetailsLink } from '../../../../components/links'; -import { TruncatableText } from '../../../../components/truncatable_text'; -import * as i18n from './translations'; - -export type CasesColumns = - | EuiTableFieldDataColumnType - | EuiTableComputedColumnType - | EuiTableActionsColumnType; - -const MediumShadeText = styled.p` - color: ${({ theme }) => theme.eui.euiColorMediumShade}; -`; - -const Spacer = styled.span` - margin-left: ${({ theme }) => theme.eui.paddingSizes.s}; -`; - -const renderStringField = (field: string, dataTestSubj: string) => - field != null ? {field} : getEmptyTagValue(); - -export const getCasesColumns = ( - actions: Array>, - filterStatus: string -): CasesColumns[] => [ - { - name: i18n.NAME, - render: (theCase: Case) => { - if (theCase.id != null && theCase.title != null) { - const caseDetailsLinkComponent = ( - - {theCase.title} - - ); - return theCase.status === 'open' ? ( - caseDetailsLinkComponent - ) : ( - <> - - {caseDetailsLinkComponent} - {i18n.CLOSED} - - - ); - } - return getEmptyTagValue(); - }, - }, - { - field: 'createdBy', - name: i18n.REPORTER, - render: (createdBy: Case['createdBy']) => { - if (createdBy != null) { - return ( - <> - - - {createdBy.fullName ? createdBy.fullName : createdBy.username ?? ''} - - - ); - } - return getEmptyTagValue(); - }, - }, - { - field: 'tags', - name: i18n.TAGS, - render: (tags: Case['tags']) => { - if (tags != null && tags.length > 0) { - return ( - - {tags.map((tag: string, i: number) => ( - - {tag} - - ))} - - ); - } - return getEmptyTagValue(); - }, - truncateText: true, - }, - { - align: 'right', - field: 'totalComment', - name: i18n.COMMENTS, - sortable: true, - render: (totalComment: Case['totalComment']) => - totalComment != null - ? renderStringField(`${totalComment}`, `case-table-column-commentCount`) - : getEmptyTagValue(), - }, - filterStatus === 'open' - ? { - field: 'createdAt', - name: i18n.OPENED_ON, - sortable: true, - render: (createdAt: Case['createdAt']) => { - if (createdAt != null) { - return ( - - - - ); - } - return getEmptyTagValue(); - }, - } - : { - field: 'closedAt', - name: i18n.CLOSED_ON, - sortable: true, - render: (closedAt: Case['closedAt']) => { - if (closedAt != null) { - return ( - - - - ); - } - return getEmptyTagValue(); - }, - }, - { - name: i18n.EXTERNAL_INCIDENT, - render: (theCase: Case) => { - if (theCase.id != null) { - return ; - } - return getEmptyTagValue(); - }, - }, - { - name: i18n.INCIDENT_MANAGEMENT_SYSTEM, - render: (theCase: Case) => { - if (theCase.externalService != null) { - return renderStringField( - `${theCase.externalService.connectorName}`, - `case-table-column-connector` - ); - } - return getEmptyTagValue(); - }, - }, - { - name: i18n.ACTIONS, - actions, - }, -]; - -interface Props { - theCase: Case; -} - -export const ExternalServiceColumn: React.FC = ({ theCase }) => { - const handleRenderDataToPush = useCallback(() => { - const lastCaseUpdate = theCase.updatedAt != null ? new Date(theCase.updatedAt) : null; - const lastCasePush = - theCase.externalService?.pushedAt != null - ? new Date(theCase.externalService?.pushedAt) - : null; - const hasDataToPush = - lastCasePush === null || - (lastCasePush != null && - lastCaseUpdate != null && - lastCasePush.getTime() < lastCaseUpdate?.getTime()); - return ( -

- - {theCase.externalService?.externalTitle} - - {hasDataToPush - ? renderStringField(i18n.REQUIRES_UPDATE, `case-table-column-external-requiresUpdate`) - : renderStringField(i18n.UP_TO_DATE, `case-table-column-external-upToDate`)} -

- ); - }, [theCase]); - if (theCase.externalService !== null) { - return handleRenderDataToPush(); - } - return renderStringField(i18n.NOT_PUSHED, `case-table-column-external-notPushed`); -}; diff --git a/x-pack/plugins/siem/public/pages/case/components/all_cases/index.test.tsx b/x-pack/plugins/siem/public/pages/case/components/all_cases/index.test.tsx deleted file mode 100644 index eb5bca6cc57fff..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/all_cases/index.test.tsx +++ /dev/null @@ -1,347 +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 { mount } from 'enzyme'; -import moment from 'moment-timezone'; -import { AllCases } from './'; -import { TestProviders } from '../../../../mock'; -import { useGetCasesMockState } from '../../../../containers/case/mock'; -import * as i18n from './translations'; - -import { getEmptyTagValue } from '../../../../components/empty_value'; -import { useDeleteCases } from '../../../../containers/case/use_delete_cases'; -import { useGetCases } from '../../../../containers/case/use_get_cases'; -import { useGetCasesStatus } from '../../../../containers/case/use_get_cases_status'; -import { useUpdateCases } from '../../../../containers/case/use_bulk_update_case'; -import { getCasesColumns } from './columns'; -jest.mock('../../../../containers/case/use_bulk_update_case'); -jest.mock('../../../../containers/case/use_delete_cases'); -jest.mock('../../../../containers/case/use_get_cases'); -jest.mock('../../../../containers/case/use_get_cases_status'); -const useDeleteCasesMock = useDeleteCases as jest.Mock; -const useGetCasesMock = useGetCases as jest.Mock; -const useGetCasesStatusMock = useGetCasesStatus as jest.Mock; -const useUpdateCasesMock = useUpdateCases as jest.Mock; - -describe('AllCases', () => { - const dispatchResetIsDeleted = jest.fn(); - const dispatchResetIsUpdated = jest.fn(); - const dispatchUpdateCaseProperty = jest.fn(); - const handleOnDeleteConfirm = jest.fn(); - const handleToggleModal = jest.fn(); - const refetchCases = jest.fn(); - const setFilters = jest.fn(); - const setQueryParams = jest.fn(); - const setSelectedCases = jest.fn(); - const updateBulkStatus = jest.fn(); - const fetchCasesStatus = jest.fn(); - const emptyTag = getEmptyTagValue().props.children; - - const defaultGetCases = { - ...useGetCasesMockState, - dispatchUpdateCaseProperty, - refetchCases, - setFilters, - setQueryParams, - setSelectedCases, - }; - const defaultDeleteCases = { - dispatchResetIsDeleted, - handleOnDeleteConfirm, - handleToggleModal, - isDeleted: false, - isDisplayConfirmDeleteModal: false, - isLoading: false, - }; - const defaultCasesStatus = { - countClosedCases: 0, - countOpenCases: 5, - fetchCasesStatus, - isError: false, - isLoading: true, - }; - const defaultUpdateCases = { - isUpdated: false, - isLoading: false, - isError: false, - dispatchResetIsUpdated, - updateBulkStatus, - }; - /* eslint-disable no-console */ - // Silence until enzyme fixed to use ReactTestUtils.act() - const originalError = console.error; - beforeAll(() => { - console.error = jest.fn(); - }); - afterAll(() => { - console.error = originalError; - }); - /* eslint-enable no-console */ - beforeEach(() => { - jest.resetAllMocks(); - useUpdateCasesMock.mockImplementation(() => defaultUpdateCases); - useGetCasesMock.mockImplementation(() => defaultGetCases); - useDeleteCasesMock.mockImplementation(() => defaultDeleteCases); - useGetCasesStatusMock.mockImplementation(() => defaultCasesStatus); - 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].id}?timerange=(global:(linkTo:!(timeline),timerange:(from:0,fromStr:now-24h,kind:relative,to:1,toStr:now)),timeline:(linkTo:!(global),timerange:(from:0,fromStr:now-24h,kind:relative,to:1,toStr:now)))` - ); - expect( - wrapper - .find(`a[data-test-subj="case-details-link"]`) - .first() - .text() - ).toEqual(useGetCasesMockState.data.cases[0].title); - 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-createdBy"]`) - .first() - .text() - ).toEqual(useGetCasesMockState.data.cases[0].createdBy.fullName); - expect( - wrapper - .find(`[data-test-subj="case-table-column-createdAt"]`) - .first() - .childAt(0) - .prop('value') - ).toBe(useGetCasesMockState.data.cases[0].createdAt); - expect( - wrapper - .find(`[data-test-subj="case-table-case-count"]`) - .first() - .text() - ).toEqual('Showing 10 cases'); - }); - it('should render empty fields', () => { - useGetCasesMock.mockImplementation(() => ({ - ...defaultGetCases, - data: { - ...defaultGetCases.data, - cases: [ - { - ...defaultGetCases.data.cases[0], - id: null, - createdAt: null, - createdBy: null, - tags: null, - title: null, - totalComment: null, - }, - ], - }, - })); - const wrapper = mount( - - - - ); - const checkIt = (columnName: string, key: number) => { - const column = wrapper.find('[data-test-subj="cases-table"] tbody .euiTableRowCell').at(key); - if (columnName === i18n.ACTIONS) { - return; - } - expect(column.find('.euiTableRowCell--hideForDesktop').text()).toEqual(columnName); - expect(column.find('span').text()).toEqual(emptyTag); - }; - getCasesColumns([], 'open').map((i, key) => i.name != null && checkIt(`${i.name}`, key)); - }); - it('should tableHeaderSortButton AllCases', () => { - const wrapper = mount( - - - - ); - wrapper - .find('[data-test-subj="tableHeaderSortButton"]') - .first() - .simulate('click'); - expect(setQueryParams).toBeCalledWith({ - page: 1, - perPage: 5, - sortField: 'createdAt', - sortOrder: 'asc', - }); - }); - it('closes case when row action icon clicked', () => { - const wrapper = mount( - - - - ); - wrapper - .find('[data-test-subj="action-close"]') - .first() - .simulate('click'); - const firstCase = useGetCasesMockState.data.cases[0]; - expect(dispatchUpdateCaseProperty).toBeCalledWith({ - caseId: firstCase.id, - updateKey: 'status', - updateValue: 'closed', - refetchCasesStatus: fetchCasesStatus, - version: firstCase.version, - }); - }); - it('opens case when row action icon clicked', () => { - useGetCasesMock.mockImplementation(() => ({ - ...defaultGetCases, - filterOptions: { ...defaultGetCases.filterOptions, status: 'closed' }, - })); - - const wrapper = mount( - - - - ); - wrapper - .find('[data-test-subj="action-open"]') - .first() - .simulate('click'); - const firstCase = useGetCasesMockState.data.cases[0]; - expect(dispatchUpdateCaseProperty).toBeCalledWith({ - caseId: firstCase.id, - updateKey: 'status', - updateValue: 'open', - refetchCasesStatus: fetchCasesStatus, - version: firstCase.version, - }); - }); - it('Bulk delete', () => { - useGetCasesMock.mockImplementation(() => ({ - ...defaultGetCases, - selectedCases: useGetCasesMockState.data.cases, - })); - useDeleteCasesMock - .mockReturnValueOnce({ - ...defaultDeleteCases, - isDisplayConfirmDeleteModal: false, - }) - .mockReturnValue({ - ...defaultDeleteCases, - isDisplayConfirmDeleteModal: true, - }); - - const wrapper = mount( - - - - ); - wrapper - .find('[data-test-subj="case-table-bulk-actions"] button') - .first() - .simulate('click'); - wrapper - .find('[data-test-subj="cases-bulk-delete-button"]') - .first() - .simulate('click'); - expect(handleToggleModal).toBeCalled(); - - wrapper - .find( - '[data-test-subj="confirm-delete-case-modal"] [data-test-subj="confirmModalConfirmButton"]' - ) - .last() - .simulate('click'); - expect(handleOnDeleteConfirm.mock.calls[0][0]).toStrictEqual( - useGetCasesMockState.data.cases.map(({ id }) => ({ id })) - ); - }); - it('Bulk close status update', () => { - useGetCasesMock.mockImplementation(() => ({ - ...defaultGetCases, - selectedCases: useGetCasesMockState.data.cases, - })); - - const wrapper = mount( - - - - ); - wrapper - .find('[data-test-subj="case-table-bulk-actions"] button') - .first() - .simulate('click'); - wrapper - .find('[data-test-subj="cases-bulk-close-button"]') - .first() - .simulate('click'); - expect(updateBulkStatus).toBeCalledWith(useGetCasesMockState.data.cases, 'closed'); - }); - it('Bulk open status update', () => { - useGetCasesMock.mockImplementation(() => ({ - ...defaultGetCases, - selectedCases: useGetCasesMockState.data.cases, - filterOptions: { - ...defaultGetCases.filterOptions, - status: 'closed', - }, - })); - - const wrapper = mount( - - - - ); - wrapper - .find('[data-test-subj="case-table-bulk-actions"] button') - .first() - .simulate('click'); - wrapper - .find('[data-test-subj="cases-bulk-open-button"]') - .first() - .simulate('click'); - expect(updateBulkStatus).toBeCalledWith(useGetCasesMockState.data.cases, 'open'); - }); - it('isDeleted is true, refetch', () => { - useDeleteCasesMock.mockImplementation(() => ({ - ...defaultDeleteCases, - isDeleted: true, - })); - - mount( - - - - ); - expect(refetchCases).toBeCalled(); - expect(fetchCasesStatus).toBeCalled(); - expect(dispatchResetIsDeleted).toBeCalled(); - }); - it('isUpdated is true, refetch', () => { - useUpdateCasesMock.mockImplementation(() => ({ - ...defaultUpdateCases, - isUpdated: true, - })); - - mount( - - - - ); - expect(refetchCases).toBeCalled(); - expect(fetchCasesStatus).toBeCalled(); - expect(dispatchResetIsUpdated).toBeCalled(); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/case/components/all_cases/index.tsx b/x-pack/plugins/siem/public/pages/case/components/all_cases/index.tsx deleted file mode 100644 index 9dd90074a2e7b9..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/all_cases/index.tsx +++ /dev/null @@ -1,439 +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, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { - EuiBasicTable, - EuiButton, - EuiContextMenuPanel, - EuiEmptyPrompt, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingContent, - EuiProgress, - EuiTableSortingType, -} from '@elastic/eui'; -import { EuiTableSelectionType } from '@elastic/eui/src/components/basic_table/table_types'; -import { isEmpty } from 'lodash/fp'; -import styled, { css } from 'styled-components'; -import * as i18n from './translations'; - -import { getCasesColumns } from './columns'; -import { Case, DeleteCase, FilterOptions, SortFieldCase } from '../../../../containers/case/types'; -import { useGetCases, UpdateCase } from '../../../../containers/case/use_get_cases'; -import { useGetCasesStatus } from '../../../../containers/case/use_get_cases_status'; -import { useDeleteCases } from '../../../../containers/case/use_delete_cases'; -import { EuiBasicTableOnChange } from '../../../detection_engine/rules/types'; -import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; -import { Panel } from '../../../../components/panel'; -import { - UtilityBar, - UtilityBarAction, - UtilityBarGroup, - UtilityBarSection, - UtilityBarText, -} from '../../../../components/utility_bar'; -import { getCreateCaseUrl } from '../../../../components/link_to'; -import { getBulkItems } from '../bulk_actions'; -import { CaseHeaderPage } from '../case_header_page'; -import { ConfirmDeleteCaseModal } from '../confirm_delete_case'; -import { OpenClosedStats } from '../open_closed_stats'; -import { navTabs } from '../../../home/home_navigations'; - -import { getActions } from './actions'; -import { CasesTableFilters } from './table_filters'; -import { useUpdateCases } from '../../../../containers/case/use_bulk_update_case'; -import { useGetActionLicense } from '../../../../containers/case/use_get_action_license'; -import { getActionLicenseError } from '../use_push_to_service/helpers'; -import { CaseCallOut } from '../callout'; -import { ConfigureCaseButton } from '../configure_cases/button'; -import { ERROR_PUSH_SERVICE_CALLOUT_TITLE } from '../use_push_to_service/translations'; - -const Div = styled.div` - margin-top: ${({ theme }) => theme.eui.paddingSizes.m}; -`; -const FlexItemDivider = styled(EuiFlexItem)` - ${({ theme }) => css` - .euiFlexGroup--gutterMedium > &.euiFlexItem { - border-right: ${theme.eui.euiBorderThin}; - padding-right: ${theme.eui.euiSize}; - margin-right: ${theme.eui.euiSize}; - } - `} -`; - -const ProgressLoader = styled(EuiProgress)` - ${({ theme }) => css` - top: 2px; - border-radius: ${theme.eui.euiBorderRadius}; - z-index: ${theme.eui.euiZHeader}; - `} -`; - -const getSortField = (field: string): SortFieldCase => { - if (field === SortFieldCase.createdAt) { - return SortFieldCase.createdAt; - } else if (field === SortFieldCase.closedAt) { - return SortFieldCase.closedAt; - } - return SortFieldCase.createdAt; -}; - -interface AllCasesProps { - userCanCrud: boolean; -} -export const AllCases = React.memo(({ userCanCrud }) => { - const urlSearch = useGetUrlSearch(navTabs.case); - const { actionLicense } = useGetActionLicense(); - const { - countClosedCases, - countOpenCases, - isLoading: isCasesStatusLoading, - fetchCasesStatus, - } = useGetCasesStatus(); - const { - data, - dispatchUpdateCaseProperty, - filterOptions, - loading, - queryParams, - selectedCases, - refetchCases, - setFilters, - setQueryParams, - setSelectedCases, - } = useGetCases(); - - // Delete case - const { - dispatchResetIsDeleted, - handleOnDeleteConfirm, - handleToggleModal, - isLoading: isDeleting, - isDeleted, - isDisplayConfirmDeleteModal, - } = useDeleteCases(); - - // Update case - const { - dispatchResetIsUpdated, - isLoading: isUpdating, - isUpdated, - updateBulkStatus, - } = useUpdateCases(); - const [deleteThisCase, setDeleteThisCase] = useState({ - title: '', - id: '', - }); - const [deleteBulk, setDeleteBulk] = useState([]); - const filterRefetch = useRef<() => void>(); - const setFilterRefetch = useCallback( - (refetchFilter: () => void) => { - filterRefetch.current = refetchFilter; - }, - [filterRefetch.current] - ); - const refreshCases = useCallback( - (dataRefresh = true) => { - if (dataRefresh) refetchCases(); - fetchCasesStatus(); - setSelectedCases([]); - setDeleteBulk([]); - if (filterRefetch.current != null) { - filterRefetch.current(); - } - }, - [filterOptions, queryParams, filterRefetch.current] - ); - - useEffect(() => { - if (isDeleted) { - refreshCases(); - dispatchResetIsDeleted(); - } - if (isUpdated) { - refreshCases(); - dispatchResetIsUpdated(); - } - }, [isDeleted, isUpdated]); - const confirmDeleteModal = useMemo( - () => ( - 0} - onCancel={handleToggleModal} - onConfirm={handleOnDeleteConfirm.bind( - null, - deleteBulk.length > 0 ? deleteBulk : [deleteThisCase] - )} - /> - ), - [deleteBulk, deleteThisCase, isDisplayConfirmDeleteModal] - ); - - const toggleDeleteModal = useCallback((deleteCase: Case) => { - handleToggleModal(); - setDeleteThisCase(deleteCase); - }, []); - - const toggleBulkDeleteModal = useCallback( - (caseIds: string[]) => { - handleToggleModal(); - if (caseIds.length === 1) { - const singleCase = selectedCases.find(theCase => theCase.id === caseIds[0]); - if (singleCase) { - return setDeleteThisCase({ id: singleCase.id, title: singleCase.title }); - } - } - const convertToDeleteCases: DeleteCase[] = caseIds.map(id => ({ id })); - setDeleteBulk(convertToDeleteCases); - }, - [selectedCases] - ); - - const handleUpdateCaseStatus = useCallback( - (status: string) => { - updateBulkStatus(selectedCases, status); - }, - [selectedCases] - ); - - const selectedCaseIds = useMemo( - (): string[] => selectedCases.map((caseObj: Case) => caseObj.id), - [selectedCases] - ); - - const getBulkItemsPopoverContent = useCallback( - (closePopover: () => void) => ( - - ), - [selectedCaseIds, filterOptions.status, toggleBulkDeleteModal] - ); - const handleDispatchUpdate = useCallback( - (args: Omit) => { - dispatchUpdateCaseProperty({ ...args, refetchCasesStatus: fetchCasesStatus }); - }, - [dispatchUpdateCaseProperty, fetchCasesStatus] - ); - - const actions = useMemo( - () => - getActions({ - caseStatus: filterOptions.status, - deleteCaseOnClick: toggleDeleteModal, - dispatchUpdate: handleDispatchUpdate, - }), - [filterOptions.status, toggleDeleteModal, handleDispatchUpdate] - ); - - const actionsErrors = useMemo(() => getActionLicenseError(actionLicense), [actionLicense]); - - const tableOnChangeCallback = useCallback( - ({ page, sort }: EuiBasicTableOnChange) => { - let newQueryParams = queryParams; - if (sort) { - newQueryParams = { - ...newQueryParams, - sortField: getSortField(sort.field), - sortOrder: sort.direction, - }; - } - if (page) { - newQueryParams = { - ...newQueryParams, - page: page.index + 1, - perPage: page.size, - }; - } - setQueryParams(newQueryParams); - refreshCases(false); - }, - [queryParams] - ); - - const onFilterChangedCallback = useCallback( - (newFilterOptions: Partial) => { - if (newFilterOptions.status && newFilterOptions.status === 'closed') { - setQueryParams({ sortField: SortFieldCase.closedAt }); - } else if (newFilterOptions.status && newFilterOptions.status === 'open') { - setQueryParams({ sortField: SortFieldCase.createdAt }); - } - setFilters(newFilterOptions); - refreshCases(false); - }, - [filterOptions, queryParams] - ); - - const memoizedGetCasesColumns = useMemo( - () => getCasesColumns(userCanCrud ? actions : [], filterOptions.status), - [actions, filterOptions.status, userCanCrud] - ); - const memoizedPagination = useMemo( - () => ({ - pageIndex: queryParams.page - 1, - pageSize: queryParams.perPage, - totalItemCount: data.total, - pageSizeOptions: [5, 10, 15, 20, 25], - }), - [data, queryParams] - ); - - const sorting: EuiTableSortingType = { - sort: { field: queryParams.sortField, direction: queryParams.sortOrder }, - }; - const euiBasicTableSelectionProps = useMemo>( - () => ({ onSelectionChange: setSelectedCases }), - [selectedCases] - ); - const isCasesLoading = useMemo( - () => loading.indexOf('cases') > -1 || loading.indexOf('caseUpdate') > -1, - [loading] - ); - const isDataEmpty = useMemo(() => data.total === 0, [data]); - - return ( - <> - {!isEmpty(actionsErrors) && ( - - )} - - - - - - - - - - } - titleTooltip={!isEmpty(actionsErrors) ? actionsErrors[0].title : ''} - urlSearch={urlSearch} - /> - - - - {i18n.CREATE_TITLE} - - - - - {(isCasesLoading || isDeleting || isUpdating) && !isDataEmpty && ( - - )} - - - {isCasesLoading && isDataEmpty ? ( -
- -
- ) : ( -
- - - - - {i18n.SHOWING_CASES(data.total ?? 0)} - - - - - {i18n.SHOWING_SELECTED_CASES(selectedCases.length)} - - {userCanCrud && ( - - {i18n.BULK_ACTIONS} - - )} - - {i18n.REFRESH} - - - - - {i18n.NO_CASES}} - titleSize="xs" - body={i18n.NO_CASES_BODY} - actions={ - - {i18n.ADD_NEW_CASE} - - } - /> - } - onChange={tableOnChangeCallback} - pagination={memoizedPagination} - selection={userCanCrud ? euiBasicTableSelectionProps : {}} - sorting={sorting} - /> -
- )} -
- {confirmDeleteModal} - - ); -}); - -AllCases.displayName = 'AllCases'; diff --git a/x-pack/plugins/siem/public/pages/case/components/callout/index.test.tsx b/x-pack/plugins/siem/public/pages/case/components/callout/index.test.tsx deleted file mode 100644 index 126ea13e96af63..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/callout/index.test.tsx +++ /dev/null @@ -1,71 +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 { mount } from 'enzyme'; - -import { CaseCallOut } from './'; - -const defaultProps = { - title: 'hey title', -}; - -describe('CaseCallOut ', () => { - it('Renders single message callout', () => { - const props = { - ...defaultProps, - message: 'we have one message', - }; - const wrapper = mount(); - expect( - wrapper - .find(`[data-test-subj="callout-message"]`) - .last() - .exists() - ).toBeTruthy(); - expect( - wrapper - .find(`[data-test-subj="callout-messages"]`) - .last() - .exists() - ).toBeFalsy(); - }); - it('Renders multi message callout', () => { - const props = { - ...defaultProps, - messages: [ - { ...defaultProps, description:

{'we have two messages'}

}, - { ...defaultProps, description:

{'for real'}

}, - ], - }; - const wrapper = mount(); - expect( - wrapper - .find(`[data-test-subj="callout-message"]`) - .last() - .exists() - ).toBeFalsy(); - expect( - wrapper - .find(`[data-test-subj="callout-messages"]`) - .last() - .exists() - ).toBeTruthy(); - }); - it('Dismisses callout', () => { - const props = { - ...defaultProps, - message: 'we have one message', - }; - const wrapper = mount(); - expect(wrapper.find(`[data-test-subj="case-call-out"]`).exists()).toBeTruthy(); - wrapper - .find(`[data-test-subj="callout-dismiss"]`) - .last() - .simulate('click'); - expect(wrapper.find(`[data-test-subj="case-call-out"]`).exists()).toBeFalsy(); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/case/components/case_header_page/index.tsx b/x-pack/plugins/siem/public/pages/case/components/case_header_page/index.tsx deleted file mode 100644 index ae2664ca6e839d..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/case_header_page/index.tsx +++ /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 React from 'react'; - -import { HeaderPage, HeaderPageProps } from '../../../../components/header_page'; -import * as i18n from './translations'; - -const CaseHeaderPageComponent: React.FC = props => ; - -CaseHeaderPageComponent.defaultProps = { - badgeOptions: { - beta: true, - text: i18n.PAGE_BADGE_LABEL, - tooltip: i18n.PAGE_BADGE_TOOLTIP, - }, -}; - -export const CaseHeaderPage = React.memo(CaseHeaderPageComponent); diff --git a/x-pack/plugins/siem/public/pages/case/components/case_status/index.tsx b/x-pack/plugins/siem/public/pages/case/components/case_status/index.tsx deleted file mode 100644 index f48d9a68ffaf0a..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/case_status/index.tsx +++ /dev/null @@ -1,117 +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 styled, { css } from 'styled-components'; -import { - EuiBadge, - EuiButtonEmpty, - EuiButtonToggle, - EuiDescriptionList, - EuiDescriptionListDescription, - EuiDescriptionListTitle, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; -import * as i18n from '../case_view/translations'; -import { FormattedRelativePreferenceDate } from '../../../../components/formatted_date'; -import { CaseViewActions } from '../case_view/actions'; -import { Case } from '../../../../containers/case/types'; -import { CaseService } from '../../../../containers/case/use_get_case_user_actions'; - -const MyDescriptionList = styled(EuiDescriptionList)` - ${({ theme }) => css` - & { - padding-right: ${theme.eui.euiSizeL}; - border-right: ${theme.eui.euiBorderThin}; - } - `} -`; - -interface CaseStatusProps { - 'data-test-subj': string; - badgeColor: string; - buttonLabel: string; - caseData: Case; - currentExternalIncident: CaseService | null; - disabled?: boolean; - icon: string; - isLoading: boolean; - isSelected: boolean; - onRefresh: () => void; - status: string; - title: string; - toggleStatusCase: (evt: unknown) => void; - value: string | null; -} -const CaseStatusComp: React.FC = ({ - 'data-test-subj': dataTestSubj, - badgeColor, - buttonLabel, - caseData, - currentExternalIncident, - disabled = false, - icon, - isLoading, - isSelected, - onRefresh, - status, - title, - toggleStatusCase, - value, -}) => ( - - - - - - {i18n.STATUS} - - - {status} - - - - - {title} - - - - - - - - - - - - {i18n.CASE_REFRESH} - - - - - - - - - - - -); - -export const CaseStatus = React.memo(CaseStatusComp); diff --git a/x-pack/plugins/siem/public/pages/case/components/case_view/actions.test.tsx b/x-pack/plugins/siem/public/pages/case/components/case_view/actions.test.tsx deleted file mode 100644 index 24fbd59b3282b2..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/case_view/actions.test.tsx +++ /dev/null @@ -1,97 +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 { mount } from 'enzyme'; - -import { useDeleteCases } from '../../../../containers/case/use_delete_cases'; -import { TestProviders } from '../../../../mock'; -import { basicCase, basicPush } from '../../../../containers/case/mock'; -import { CaseViewActions } from './actions'; -import * as i18n from './translations'; -jest.mock('../../../../containers/case/use_delete_cases'); -const useDeleteCasesMock = useDeleteCases as jest.Mock; - -describe('CaseView actions', () => { - const handleOnDeleteConfirm = jest.fn(); - const handleToggleModal = jest.fn(); - const dispatchResetIsDeleted = jest.fn(); - const defaultDeleteState = { - dispatchResetIsDeleted, - handleToggleModal, - handleOnDeleteConfirm, - isLoading: false, - isError: false, - isDeleted: false, - isDisplayConfirmDeleteModal: false, - }; - beforeEach(() => { - jest.resetAllMocks(); - useDeleteCasesMock.mockImplementation(() => defaultDeleteState); - }); - it('clicking trash toggles modal', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="confirm-delete-case-modal"]').exists()).toBeFalsy(); - - wrapper - .find('button[data-test-subj="property-actions-ellipses"]') - .first() - .simulate('click'); - wrapper.find('button[data-test-subj="property-actions-trash"]').simulate('click'); - expect(handleToggleModal).toHaveBeenCalled(); - }); - it('toggle delete modal and confirm', () => { - useDeleteCasesMock.mockImplementation(() => ({ - ...defaultDeleteState, - isDisplayConfirmDeleteModal: true, - })); - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="confirm-delete-case-modal"]').exists()).toBeTruthy(); - wrapper.find('button[data-test-subj="confirmModalConfirmButton"]').simulate('click'); - expect(handleOnDeleteConfirm.mock.calls[0][0]).toEqual([ - { id: basicCase.id, title: basicCase.title }, - ]); - }); - it('displays active incident link', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('[data-test-subj="confirm-delete-case-modal"]').exists()).toBeFalsy(); - - wrapper - .find('button[data-test-subj="property-actions-ellipses"]') - .first() - .simulate('click'); - expect( - wrapper - .find('[data-test-subj="property-actions-popout"]') - .first() - .prop('aria-label') - ).toEqual(i18n.VIEW_INCIDENT(basicPush.externalTitle)); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/case/components/case_view/actions.tsx b/x-pack/plugins/siem/public/pages/case/components/case_view/actions.tsx deleted file mode 100644 index 4acdaef6ca51f8..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/case_view/actions.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { isEmpty } from 'lodash/fp'; -import React, { useMemo } from 'react'; -import { Redirect } from 'react-router-dom'; -import * as i18n from './translations'; -import { useDeleteCases } from '../../../../containers/case/use_delete_cases'; -import { ConfirmDeleteCaseModal } from '../confirm_delete_case'; -import { SiemPageName } from '../../../home/types'; -import { PropertyActions } from '../property_actions'; -import { Case } from '../../../../containers/case/types'; -import { CaseService } from '../../../../containers/case/use_get_case_user_actions'; - -interface CaseViewActions { - caseData: Case; - currentExternalIncident: CaseService | null; - disabled?: boolean; -} - -const CaseViewActionsComponent: React.FC = ({ - caseData, - currentExternalIncident, - disabled = false, -}) => { - // Delete case - const { - handleToggleModal, - handleOnDeleteConfirm, - isDeleted, - isDisplayConfirmDeleteModal, - } = useDeleteCases(); - - const confirmDeleteModal = useMemo( - () => ( - - ), - [isDisplayConfirmDeleteModal, caseData] - ); - const propertyActions = useMemo( - () => [ - { - disabled, - iconType: 'trash', - label: i18n.DELETE_CASE, - onClick: handleToggleModal, - }, - ...(currentExternalIncident != null && !isEmpty(currentExternalIncident?.externalUrl) - ? [ - { - iconType: 'popout', - label: i18n.VIEW_INCIDENT(currentExternalIncident?.externalTitle ?? ''), - onClick: () => window.open(currentExternalIncident?.externalUrl, '_blank'), - }, - ] - : []), - ], - [disabled, handleToggleModal, currentExternalIncident] - ); - - if (isDeleted) { - return ; - } - return ( - <> - - {confirmDeleteModal} - - ); -}; - -export const CaseViewActions = React.memo(CaseViewActionsComponent); diff --git a/x-pack/plugins/siem/public/pages/case/components/case_view/index.test.tsx b/x-pack/plugins/siem/public/pages/case/components/case_view/index.test.tsx deleted file mode 100644 index a6e6b19a071ce5..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/case_view/index.test.tsx +++ /dev/null @@ -1,432 +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 { mount } from 'enzyme'; - -import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; -import { CaseComponent, CaseProps, CaseView } from './'; -import { basicCase, basicCaseClosed, caseUserActions } from '../../../../containers/case/mock'; -import { TestProviders } from '../../../../mock'; -import { useUpdateCase } from '../../../../containers/case/use_update_case'; -import { useGetCase } from '../../../../containers/case/use_get_case'; -import { useGetCaseUserActions } from '../../../../containers/case/use_get_case_user_actions'; -import { wait } from '../../../../lib/helpers'; -import { usePushToService } from '../use_push_to_service'; -jest.mock('../../../../containers/case/use_update_case'); -jest.mock('../../../../containers/case/use_get_case_user_actions'); -jest.mock('../../../../containers/case/use_get_case'); -jest.mock('../use_push_to_service'); -const useUpdateCaseMock = useUpdateCase as jest.Mock; -const useGetCaseUserActionsMock = useGetCaseUserActions as jest.Mock; -const usePushToServiceMock = usePushToService as jest.Mock; - -export const caseProps: CaseProps = { - caseId: basicCase.id, - userCanCrud: true, - caseData: basicCase, - fetchCase: jest.fn(), - updateCase: jest.fn(), -}; - -export const caseClosedProps: CaseProps = { - ...caseProps, - caseData: basicCaseClosed, -}; - -describe('CaseView ', () => { - const updateCaseProperty = jest.fn(); - const fetchCaseUserActions = jest.fn(); - const fetchCase = jest.fn(); - const updateCase = jest.fn(); - const data = caseProps.caseData; - const defaultGetCase = { - isLoading: false, - isError: false, - data, - updateCase, - fetchCase, - }; - /* eslint-disable no-console */ - // Silence until enzyme fixed to use ReactTestUtils.act() - const originalError = console.error; - beforeAll(() => { - console.error = jest.fn(); - }); - afterAll(() => { - console.error = originalError; - }); - /* eslint-enable no-console */ - - const defaultUpdateCaseState = { - isLoading: false, - isError: false, - updateKey: null, - updateCaseProperty, - }; - - const defaultUseGetCaseUserActions = { - caseUserActions, - caseServices: {}, - fetchCaseUserActions, - firstIndexPushToService: -1, - hasDataToPush: false, - isLoading: false, - isError: false, - lastIndexPushToService: -1, - participants: [data.createdBy], - }; - - beforeEach(() => { - jest.resetAllMocks(); - useUpdateCaseMock.mockImplementation(() => defaultUpdateCaseState); - jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); - useGetCaseUserActionsMock.mockImplementation(() => defaultUseGetCaseUserActions); - usePushToServiceMock.mockImplementation(({ updateCase: updateCaseMockCall }) => ({ - pushButton: ( - - ), - pushCallouts: null, - })); - }); - - it('should render CaseComponent', async () => { - const wrapper = mount( - - - - - - ); - await wait(); - expect( - wrapper - .find(`[data-test-subj="case-view-title"]`) - .first() - .prop('title') - ).toEqual(data.title); - expect( - wrapper - .find(`[data-test-subj="case-view-status"]`) - .first() - .text() - ).toEqual(data.status); - expect( - wrapper - .find(`[data-test-subj="case-view-tag-list"] [data-test-subj="case-tag"]`) - .first() - .text() - ).toEqual(data.tags[0]); - expect( - wrapper - .find(`[data-test-subj="case-view-username"]`) - .first() - .text() - ).toEqual(data.createdBy.username); - expect(wrapper.contains(`[data-test-subj="case-view-closedAt"]`)).toBe(false); - expect( - wrapper - .find(`[data-test-subj="case-view-createdAt"]`) - .first() - .prop('value') - ).toEqual(data.createdAt); - expect( - wrapper - .find(`[data-test-subj="description-action"] [data-test-subj="user-action-markdown"]`) - .first() - .prop('raw') - ).toEqual(data.description); - }); - - it('should show closed indicators in header when case is closed', async () => { - useUpdateCaseMock.mockImplementation(() => ({ - ...defaultUpdateCaseState, - caseData: basicCaseClosed, - })); - const wrapper = mount( - - - - - - ); - await wait(); - expect(wrapper.contains(`[data-test-subj="case-view-createdAt"]`)).toBe(false); - expect( - wrapper - .find(`[data-test-subj="case-view-closedAt"]`) - .first() - .prop('value') - ).toEqual(basicCaseClosed.closedAt); - expect( - wrapper - .find(`[data-test-subj="case-view-status"]`) - .first() - .text() - ).toEqual(basicCaseClosed.status); - }); - - it('should dispatch update state when button is toggled', async () => { - const wrapper = mount( - - - - - - ); - await wait(); - wrapper - .find('input[data-test-subj="toggle-case-status"]') - .simulate('change', { target: { checked: true } }); - expect(updateCaseProperty).toHaveBeenCalled(); - }); - - it('should display EditableTitle isLoading', () => { - useUpdateCaseMock.mockImplementation(() => ({ - ...defaultUpdateCaseState, - isLoading: true, - updateKey: 'title', - })); - const wrapper = mount( - - - - - - ); - expect( - wrapper - .find('[data-test-subj="editable-title-loading"]') - .first() - .exists() - ).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="editable-title-edit-icon"]') - .first() - .exists() - ).toBeFalsy(); - }); - - it('should display Toggle Status isLoading', () => { - useUpdateCaseMock.mockImplementation(() => ({ - ...defaultUpdateCaseState, - isLoading: true, - updateKey: 'status', - })); - const wrapper = mount( - - - - - - ); - expect( - wrapper - .find('[data-test-subj="toggle-case-status"]') - .first() - .prop('isLoading') - ).toBeTruthy(); - }); - - it('should display description isLoading', () => { - useUpdateCaseMock.mockImplementation(() => ({ - ...defaultUpdateCaseState, - isLoading: true, - updateKey: 'description', - })); - const wrapper = mount( - - - - - - ); - expect( - wrapper - .find('[data-test-subj="description-action"] [data-test-subj="user-action-title-loading"]') - .first() - .exists() - ).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="description-action"] [data-test-subj="property-actions"]') - .first() - .exists() - ).toBeFalsy(); - }); - - it('should display tags isLoading', () => { - useUpdateCaseMock.mockImplementation(() => ({ - ...defaultUpdateCaseState, - isLoading: true, - updateKey: 'tags', - })); - const wrapper = mount( - - - - - - ); - expect( - wrapper - .find('[data-test-subj="case-view-tag-list"] [data-test-subj="tag-list-loading"]') - .first() - .exists() - ).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="tag-list-edit"]') - .first() - .exists() - ).toBeFalsy(); - }); - - it('should update title', () => { - const wrapper = mount( - - - - - - ); - const newTitle = 'The new title'; - wrapper - .find(`[data-test-subj="editable-title-edit-icon"]`) - .first() - .simulate('click'); - wrapper.update(); - wrapper - .find(`[data-test-subj="editable-title-input-field"]`) - .last() - .simulate('change', { target: { value: newTitle } }); - - wrapper.update(); - wrapper - .find(`[data-test-subj="editable-title-submit-btn"]`) - .first() - .simulate('click'); - - wrapper.update(); - const updateObject = updateCaseProperty.mock.calls[0][0]; - expect(updateObject.updateKey).toEqual('title'); - expect(updateObject.updateValue).toEqual(newTitle); - }); - - it('should push updates on button click', async () => { - useGetCaseUserActionsMock.mockImplementation(() => ({ - ...defaultUseGetCaseUserActions, - hasDataToPush: true, - })); - const wrapper = mount( - - - - - - ); - expect( - wrapper - .find('[data-test-subj="has-data-to-push-button"]') - .first() - .exists() - ).toBeTruthy(); - wrapper - .find('[data-test-subj="mock-button"]') - .first() - .simulate('click'); - wrapper.update(); - await wait(); - expect(updateCase).toBeCalledWith(caseProps.caseData); - expect(fetchCaseUserActions).toBeCalledWith(caseProps.caseData.id); - }); - - it('should return null if error', () => { - (useGetCase as jest.Mock).mockImplementation(() => ({ - ...defaultGetCase, - isError: true, - })); - const wrapper = mount( - - - - - - ); - expect(wrapper).toEqual({}); - }); - - it('should return spinner if loading', () => { - (useGetCase as jest.Mock).mockImplementation(() => ({ - ...defaultGetCase, - isLoading: true, - })); - const wrapper = mount( - - - - - - ); - expect(wrapper.find('[data-test-subj="case-view-loading"]').exists()).toBeTruthy(); - }); - - it('should return case view when data is there', () => { - (useGetCase as jest.Mock).mockImplementation(() => defaultGetCase); - const wrapper = mount( - - - - - - ); - expect(wrapper.find('[data-test-subj="case-view-title"]').exists()).toBeTruthy(); - }); - - it('should refresh data on refresh', () => { - (useGetCase as jest.Mock).mockImplementation(() => defaultGetCase); - const wrapper = mount( - - - - - - ); - wrapper - .find('[data-test-subj="case-refresh"]') - .first() - .simulate('click'); - expect(fetchCaseUserActions).toBeCalledWith(caseProps.caseData.id); - expect(fetchCase).toBeCalled(); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/case/components/case_view/index.tsx b/x-pack/plugins/siem/public/pages/case/components/case_view/index.tsx deleted file mode 100644 index fed8ec8edbe8b5..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/case_view/index.tsx +++ /dev/null @@ -1,382 +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 { - EuiButtonToggle, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingContent, - EuiLoadingSpinner, - EuiHorizontalRule, -} from '@elastic/eui'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import styled from 'styled-components'; - -import * as i18n from './translations'; -import { Case } from '../../../../containers/case/types'; -import { getCaseUrl } from '../../../../components/link_to'; -import { HeaderPage } from '../../../../components/header_page'; -import { EditableTitle } from '../../../../components/header_page/editable_title'; -import { TagList } from '../tag_list'; -import { useGetCase } from '../../../../containers/case/use_get_case'; -import { UserActionTree } from '../user_action_tree'; -import { UserList } from '../user_list'; -import { useUpdateCase } from '../../../../containers/case/use_update_case'; -import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; -import { WrapperPage } from '../../../../components/wrapper_page'; -import { getTypedPayload } from '../../../../containers/case/utils'; -import { WhitePageWrapper } from '../wrappers'; -import { useBasePath } from '../../../../lib/kibana'; -import { CaseStatus } from '../case_status'; -import { navTabs } from '../../../home/home_navigations'; -import { SpyRoute } from '../../../../utils/route/spy_routes'; -import { useGetCaseUserActions } from '../../../../containers/case/use_get_case_user_actions'; -import { usePushToService } from '../use_push_to_service'; -import { EditConnector } from '../edit_connector'; -import { useConnectors } from '../../../../containers/case/configure/use_connectors'; - -interface Props { - caseId: string; - userCanCrud: boolean; -} - -const MyWrapper = styled(WrapperPage)` - padding-bottom: 0; -`; - -const MyEuiFlexGroup = styled(EuiFlexGroup)` - height: 100%; -`; - -const MyEuiHorizontalRule = styled(EuiHorizontalRule)` - margin-left: 48px; - &.euiHorizontalRule--full { - width: calc(100% - 48px); - } -`; - -export interface CaseProps extends Props { - fetchCase: () => void; - caseData: Case; - updateCase: (newCase: Case) => void; -} - -export const CaseComponent = React.memo( - ({ caseId, caseData, fetchCase, updateCase, userCanCrud }) => { - const basePath = window.location.origin + useBasePath(); - const caseLink = `${basePath}/app/siem#/case/${caseId}`; - const search = useGetUrlSearch(navTabs.case); - const [initLoadingData, setInitLoadingData] = useState(true); - const { - caseUserActions, - fetchCaseUserActions, - caseServices, - hasDataToPush, - isLoading: isLoadingUserActions, - participants, - } = useGetCaseUserActions(caseId, caseData.connectorId); - const { isLoading, updateKey, updateCaseProperty } = useUpdateCase({ - caseId, - }); - - // Update Fields - const onUpdateField = useCallback( - (newUpdateKey: keyof Case, updateValue: Case[keyof Case]) => { - const handleUpdateNewCase = (newCase: Case) => - updateCase({ ...newCase, comments: caseData.comments }); - switch (newUpdateKey) { - case 'title': - const titleUpdate = getTypedPayload(updateValue); - if (titleUpdate.length > 0) { - updateCaseProperty({ - fetchCaseUserActions, - updateKey: 'title', - updateValue: titleUpdate, - updateCase: handleUpdateNewCase, - version: caseData.version, - }); - } - break; - case 'connectorId': - const connectorId = getTypedPayload(updateValue); - if (connectorId.length > 0) { - updateCaseProperty({ - fetchCaseUserActions, - updateKey: 'connector_id', - updateValue: connectorId, - updateCase: handleUpdateNewCase, - version: caseData.version, - }); - } - break; - case 'description': - const descriptionUpdate = getTypedPayload(updateValue); - if (descriptionUpdate.length > 0) { - updateCaseProperty({ - fetchCaseUserActions, - updateKey: 'description', - updateValue: descriptionUpdate, - updateCase: handleUpdateNewCase, - version: caseData.version, - }); - } - break; - case 'tags': - const tagsUpdate = getTypedPayload(updateValue); - updateCaseProperty({ - fetchCaseUserActions, - updateKey: 'tags', - updateValue: tagsUpdate, - updateCase: handleUpdateNewCase, - version: caseData.version, - }); - break; - case 'status': - const statusUpdate = getTypedPayload(updateValue); - if (caseData.status !== updateValue) { - updateCaseProperty({ - fetchCaseUserActions, - updateKey: 'status', - updateValue: statusUpdate, - updateCase: handleUpdateNewCase, - version: caseData.version, - }); - } - default: - return null; - } - }, - [fetchCaseUserActions, updateCaseProperty, updateCase, caseData] - ); - const handleUpdateCase = useCallback( - (newCase: Case) => { - updateCase(newCase); - fetchCaseUserActions(newCase.id); - }, - [updateCase, fetchCaseUserActions] - ); - - const { loading: isLoadingConnectors, connectors } = useConnectors(); - const caseConnectorName = useMemo( - () => connectors.find(c => c.id === caseData.connectorId)?.name ?? 'none', - [connectors, caseData.connectorId] - ); - - const currentExternalIncident = useMemo( - () => - caseServices != null && caseServices[caseData.connectorId] != null - ? caseServices[caseData.connectorId] - : null, - [caseServices, caseData.connectorId] - ); - - const { pushButton, pushCallouts } = usePushToService({ - caseConnectorId: caseData.connectorId, - caseConnectorName, - caseServices, - caseId: caseData.id, - caseStatus: caseData.status, - connectors, - updateCase: handleUpdateCase, - userCanCrud, - }); - - const onSubmitConnector = useCallback( - connectorId => onUpdateField('connectorId', connectorId), - [onUpdateField] - ); - const onSubmitTags = useCallback(newTags => onUpdateField('tags', newTags), [onUpdateField]); - const onSubmitTitle = useCallback(newTitle => onUpdateField('title', newTitle), [ - onUpdateField, - ]); - const toggleStatusCase = useCallback( - e => onUpdateField('status', e.target.checked ? 'closed' : 'open'), - [onUpdateField] - ); - const handleRefresh = useCallback(() => { - fetchCaseUserActions(caseData.id); - fetchCase(); - }, [caseData.id, fetchCase, fetchCaseUserActions]); - - const spyState = useMemo(() => ({ caseTitle: caseData.title }), [caseData.title]); - - const caseStatusData = useMemo( - () => - caseData.status === 'open' - ? { - 'data-test-subj': 'case-view-createdAt', - value: caseData.createdAt, - title: i18n.CASE_OPENED, - buttonLabel: i18n.CLOSE_CASE, - status: caseData.status, - icon: 'folderCheck', - badgeColor: 'secondary', - isSelected: false, - } - : { - 'data-test-subj': 'case-view-closedAt', - value: caseData.closedAt ?? '', - title: i18n.CASE_CLOSED, - buttonLabel: i18n.REOPEN_CASE, - status: caseData.status, - icon: 'folderExclamation', - badgeColor: 'danger', - isSelected: true, - }, - [caseData.closedAt, caseData.createdAt, caseData.status] - ); - const emailContent = useMemo( - () => ({ - subject: i18n.EMAIL_SUBJECT(caseData.title), - body: i18n.EMAIL_BODY(caseLink), - }), - [caseLink, caseData.title] - ); - - useEffect(() => { - if (initLoadingData && !isLoadingUserActions) { - setInitLoadingData(false); - } - }, [initLoadingData, isLoadingUserActions]); - - return ( - <> - - - } - title={caseData.title} - > - - - - - - {!initLoadingData && pushCallouts != null && pushCallouts} - - - {initLoadingData && } - {!initLoadingData && ( - <> - - - - - - - {hasDataToPush && ( - - {pushButton} - - )} - - - )} - - - - - - - - - - - - - ); - } -); - -export const CaseView = React.memo(({ caseId, userCanCrud }: Props) => { - const { data, isLoading, isError, fetchCase, updateCase } = useGetCase(caseId); - if (isError) { - return null; - } - if (isLoading) { - return ( - - - - - - ); - } - - return ( - - ); -}); - -CaseComponent.displayName = 'CaseComponent'; -CaseView.displayName = 'CaseView'; diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/__mock__/index.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/__mock__/index.tsx deleted file mode 100644 index 0eccd8980ccd23..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/__mock__/index.tsx +++ /dev/null @@ -1,54 +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 { Connector } from '../../../../../containers/case/configure/types'; -import { ReturnConnectors } from '../../../../../containers/case/configure/use_connectors'; -import { connectorsMock } from '../../../../../containers/case/configure/mock'; -import { ReturnUseCaseConfigure } from '../../../../../containers/case/configure/use_configure'; -import { createUseKibanaMock } from '../../../../../mock/kibana_react'; -export { mapping } from '../../../../../containers/case/configure/mock'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { actionTypeRegistryMock } from '../../../../../../../triggers_actions_ui/public/application/action_type_registry.mock'; - -export const connectors: Connector[] = connectorsMock; - -export const searchURL = - '?timerange=(global:(linkTo:!(),timerange:(from:1585487656371,fromStr:now-24h,kind:relative,to:1585574056371,toStr:now)),timeline:(linkTo:!(),timerange:(from:1585227005527,kind:absolute,to:1585313405527)))'; - -export const useCaseConfigureResponse: ReturnUseCaseConfigure = { - closureType: 'close-by-user', - connectorId: 'none', - connectorName: 'none', - currentConfiguration: { - connectorId: 'none', - closureType: 'close-by-user', - connectorName: 'none', - }, - firstLoad: false, - loading: false, - mapping: null, - persistCaseConfigure: jest.fn(), - persistLoading: false, - refetchCaseConfigure: jest.fn(), - setClosureType: jest.fn(), - setConnector: jest.fn(), - setCurrentConfiguration: jest.fn(), - setMapping: jest.fn(), - version: '', -}; - -export const useConnectorsResponse: ReturnConnectors = { - loading: false, - connectors, - refetchConnectors: jest.fn(), -}; - -export const kibanaMockImplementationArgs = { - services: { - ...createUseKibanaMock()().services, - triggers_actions_ui: { actionTypeRegistry: actionTypeRegistryMock.create() }, - }, -}; diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx deleted file mode 100644 index 08975703241c74..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.test.tsx +++ /dev/null @@ -1,860 +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 { ReactWrapper, mount } from 'enzyme'; - -import { ConfigureCases } from './'; -import { TestProviders } from '../../../../mock'; -import { Connectors } from './connectors'; -import { ClosureOptions } from './closure_options'; -import { - ActionsConnectorsContextProvider, - ConnectorAddFlyout, - ConnectorEditFlyout, -} from '../../../../../../triggers_actions_ui/public'; - -import { useKibana } from '../../../../lib/kibana'; -import { useConnectors } from '../../../../containers/case/configure/use_connectors'; -import { useCaseConfigure } from '../../../../containers/case/configure/use_configure'; -import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; - -import { - connectors, - searchURL, - useCaseConfigureResponse, - useConnectorsResponse, - kibanaMockImplementationArgs, -} from './__mock__'; - -jest.mock('../../../../lib/kibana'); -jest.mock('../../../../containers/case/configure/use_connectors'); -jest.mock('../../../../containers/case/configure/use_configure'); -jest.mock('../../../../components/navigation/use_get_url_search'); - -const useKibanaMock = useKibana as jest.Mock; -const useConnectorsMock = useConnectors as jest.Mock; -const useCaseConfigureMock = useCaseConfigure as jest.Mock; -const useGetUrlSearchMock = useGetUrlSearch as jest.Mock; -describe('ConfigureCases', () => { - describe('rendering', () => { - let wrapper: ReactWrapper; - beforeEach(() => { - jest.resetAllMocks(); - useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse); - useConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, connectors: [] })); - useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); - useGetUrlSearchMock.mockImplementation(() => searchURL); - - wrapper = mount(, { wrappingComponent: TestProviders }); - }); - - test('it renders the Connectors', () => { - expect(wrapper.find('[data-test-subj="dropdown-connectors"]').exists()).toBeTruthy(); - }); - - test('it renders the ClosureType', () => { - expect(wrapper.find('[data-test-subj="closure-options-radio-group"]').exists()).toBeTruthy(); - }); - - test('it renders the ActionsConnectorsContextProvider', () => { - // Components from triggers_actions_ui do not have a data-test-subj - expect(wrapper.find(ActionsConnectorsContextProvider).exists()).toBeTruthy(); - }); - - test('it renders the ConnectorAddFlyout', () => { - // Components from triggers_actions_ui do not have a data-test-subj - expect(wrapper.find(ConnectorAddFlyout).exists()).toBeTruthy(); - }); - - test('it does NOT render the ConnectorEditFlyout', () => { - // Components from triggers_actions_ui do not have a data-test-subj - expect(wrapper.find(ConnectorEditFlyout).exists()).toBeFalsy(); - }); - - test('it does NOT render the EuiCallOut', () => { - expect( - wrapper.find('[data-test-subj="configure-cases-warning-callout"]').exists() - ).toBeFalsy(); - }); - - test('it does NOT render the EuiBottomBar', () => { - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeFalsy(); - }); - - test('it disables correctly ClosureOptions when the connector is set to none', () => { - expect(wrapper.find(ClosureOptions).prop('disabled')).toBe(true); - }); - }); - - describe('Unhappy path', () => { - let wrapper: ReactWrapper; - beforeEach(() => { - jest.resetAllMocks(); - useCaseConfigureMock.mockImplementation(() => ({ - ...useCaseConfigureResponse, - closureType: 'close-by-user', - connectorId: 'not-id', - connectorName: 'unchanged', - currentConfiguration: { - connectorName: 'unchanged', - connectorId: 'not-id', - closureType: 'close-by-user', - }, - })); - useConnectorsMock.mockImplementation(() => ({ ...useConnectorsResponse, connectors: [] })); - useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); - useGetUrlSearchMock.mockImplementation(() => searchURL); - wrapper = mount(, { wrappingComponent: TestProviders }); - }); - - test('it shows the warning callout when configuration is invalid', () => { - expect( - wrapper.find('[data-test-subj="configure-cases-warning-callout"]').exists() - ).toBeTruthy(); - }); - - test('it hides the update connector button when the connectorId is invalid', () => { - expect( - wrapper - .find('button[data-test-subj="case-configure-update-selected-connector-button"]') - .exists() - ).toBeFalsy(); - }); - }); - - describe('Happy path', () => { - let wrapper: ReactWrapper; - - beforeEach(() => { - jest.resetAllMocks(); - useCaseConfigureMock.mockImplementation(() => ({ - ...useCaseConfigureResponse, - mapping: connectors[0].config.casesConfiguration.mapping, - closureType: 'close-by-user', - connectorId: 'servicenow-1', - connectorName: 'unchanged', - currentConfiguration: { - connectorName: 'unchanged', - connectorId: 'servicenow-1', - closureType: 'close-by-user', - }, - })); - useConnectorsMock.mockImplementation(() => useConnectorsResponse); - useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); - useGetUrlSearchMock.mockImplementation(() => searchURL); - - wrapper = mount(, { wrappingComponent: TestProviders }); - }); - - test('it renders the ConnectorEditFlyout', () => { - expect(wrapper.find(ConnectorEditFlyout).exists()).toBeTruthy(); - }); - - test('it renders with correct props', () => { - // Connector - expect(wrapper.find(Connectors).prop('connectors')).toEqual(connectors); - expect(wrapper.find(Connectors).prop('disabled')).toBe(false); - expect(wrapper.find(Connectors).prop('isLoading')).toBe(false); - expect(wrapper.find(Connectors).prop('selectedConnector')).toBe('servicenow-1'); - - // ClosureOptions - expect(wrapper.find(ClosureOptions).prop('disabled')).toBe(false); - expect(wrapper.find(ClosureOptions).prop('closureTypeSelected')).toBe('close-by-user'); - - // Flyouts - expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(false); - expect(wrapper.find(ConnectorAddFlyout).prop('actionTypes')).toEqual([ - expect.objectContaining({ - id: '.servicenow', - }), - expect.objectContaining({ - id: '.jira', - }), - ]); - - expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(false); - expect(wrapper.find(ConnectorEditFlyout).prop('initialConnector')).toEqual(connectors[0]); - }); - - test('it does not shows the action bar when there is no change', () => { - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeFalsy(); - }); - - test('it disables correctly when the user cannot crud', () => { - const newWrapper = mount(, { - wrappingComponent: TestProviders, - }); - - expect(newWrapper.find('button[data-test-subj="dropdown-connectors"]').prop('disabled')).toBe( - true - ); - - expect( - newWrapper - .find('button[data-test-subj="case-configure-add-connector-button"]') - .prop('disabled') - ).toBe(true); - - expect( - newWrapper - .find('button[data-test-subj="case-configure-update-selected-connector-button"]') - .prop('disabled') - ).toBe(true); - - // Two closure options - expect( - newWrapper - .find('[data-test-subj="closure-options-radio-group"] input') - .first() - .prop('disabled') - ).toBe(true); - - expect( - newWrapper - .find('[data-test-subj="closure-options-radio-group"] input') - .at(1) - .prop('disabled') - ).toBe(true); - }); - }); - - describe('loading connectors', () => { - let wrapper: ReactWrapper; - - beforeEach(() => { - jest.resetAllMocks(); - jest.restoreAllMocks(); - jest.clearAllMocks(); - useCaseConfigureMock.mockImplementation(() => ({ - ...useCaseConfigureResponse, - mapping: connectors[1].config.casesConfiguration.mapping, - closureType: 'close-by-user', - connectorId: 'servicenow-2', - connectorName: 'unchanged', - currentConfiguration: { - connectorName: 'unchanged', - connectorId: 'servicenow-1', - closureType: 'close-by-user', - }, - })); - useConnectorsMock.mockImplementation(() => ({ - ...useConnectorsResponse, - loading: true, - })); - useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); - useGetUrlSearchMock.mockImplementation(() => searchURL); - wrapper = mount(, { wrappingComponent: TestProviders }); - }); - - test('it disables correctly Connector when loading connectors', () => { - expect( - wrapper.find('button[data-test-subj="dropdown-connectors"]').prop('disabled') - ).toBeTruthy(); - }); - - test('it pass the correct value to isLoading attribute on Connector', () => { - expect(wrapper.find(Connectors).prop('isLoading')).toBe(true); - }); - - test('it disables correctly ClosureOptions when loading connectors', () => { - expect(wrapper.find(ClosureOptions).prop('disabled')).toBe(true); - }); - - test('it hides the update connector button when loading the connectors', () => { - expect( - wrapper - .find('button[data-test-subj="case-configure-update-selected-connector-button"]') - .prop('disabled') - ).toBe(true); - }); - - test('it disables the buttons of action bar when loading connectors', () => { - const newWrapper = mount(, { - wrappingComponent: TestProviders, - }); - - expect( - newWrapper - .find('button[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') - .first() - .prop('disabled') - ).toBe(true); - - expect( - newWrapper - .find('button[data-test-subj="case-configure-action-bottom-bar-save-button"]') - .first() - .prop('disabled') - ).toBe(true); - }); - }); - - describe('saving configuration', () => { - let wrapper: ReactWrapper; - - beforeEach(() => { - jest.resetAllMocks(); - useCaseConfigureMock.mockImplementation(() => ({ - ...useCaseConfigureResponse, - connectorId: 'servicenow-1', - persistLoading: true, - })); - - useConnectorsMock.mockImplementation(() => useConnectorsResponse); - useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); - useGetUrlSearchMock.mockImplementation(() => searchURL); - wrapper = mount(, { wrappingComponent: TestProviders }); - }); - - test('it disables correctly Connector when saving configuration', () => { - expect(wrapper.find(Connectors).prop('disabled')).toBe(true); - }); - - test('it disables correctly ClosureOptions when saving configuration', () => { - expect( - wrapper - .find('[data-test-subj="closure-options-radio-group"] input') - .first() - .prop('disabled') - ).toBe(true); - - expect( - wrapper - .find('[data-test-subj="closure-options-radio-group"] input') - .at(1) - .prop('disabled') - ).toBe(true); - }); - - test('it disables the update connector button when saving the configuration', () => { - expect( - wrapper - .find('button[data-test-subj="case-configure-update-selected-connector-button"]') - .prop('disabled') - ).toBe(true); - }); - - test('it disables the buttons of action bar when saving configuration', () => { - useCaseConfigureMock.mockImplementation(() => ({ - ...useCaseConfigureResponse, - mapping: connectors[1].config.casesConfiguration.mapping, - closureType: 'close-by-user', - connectorId: 'servicenow-2', - connectorName: 'unchanged', - currentConfiguration: { - connectorName: 'unchanged', - connectorId: 'servicenow-1', - closureType: 'close-by-user', - }, - persistLoading: true, - })); - - const newWrapper = mount(, { - wrappingComponent: TestProviders, - }); - - expect( - newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') - .first() - .prop('isDisabled') - ).toBe(true); - - expect( - newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') - .first() - .prop('isDisabled') - ).toBe(true); - }); - - test('it shows the loading spinner when saving configuration', () => { - useCaseConfigureMock.mockImplementation(() => ({ - ...useCaseConfigureResponse, - mapping: connectors[1].config.casesConfiguration.mapping, - closureType: 'close-by-user', - connectorId: 'servicenow-2', - connectorName: 'unchanged', - currentConfiguration: { - connectorName: 'unchanged', - connectorId: 'servicenow-1', - closureType: 'close-by-user', - }, - persistLoading: true, - })); - - const newWrapper = mount(, { - wrappingComponent: TestProviders, - }); - - expect( - newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') - .first() - .prop('isLoading') - ).toBe(true); - - expect( - newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') - .first() - .prop('isLoading') - ).toBe(true); - }); - }); - - describe('loading configuration', () => { - let wrapper: ReactWrapper; - - beforeEach(() => { - jest.resetAllMocks(); - useCaseConfigureMock.mockImplementation(() => ({ - ...useCaseConfigureResponse, - loading: true, - })); - useConnectorsMock.mockImplementation(() => ({ - ...useConnectorsResponse, - })); - useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); - useGetUrlSearchMock.mockImplementation(() => searchURL); - wrapper = mount(, { wrappingComponent: TestProviders }); - }); - - test('it hides the update connector button when loading the configuration', () => { - expect( - wrapper - .find('button[data-test-subj="case-configure-update-selected-connector-button"]') - .exists() - ).toBeFalsy(); - }); - }); - - describe('update connector', () => { - let wrapper: ReactWrapper; - const persistCaseConfigure = jest.fn(); - - beforeEach(() => { - jest.resetAllMocks(); - useCaseConfigureMock.mockImplementation(() => ({ - ...useCaseConfigureResponse, - mapping: connectors[1].config.casesConfiguration.mapping, - closureType: 'close-by-user', - connectorId: 'servicenow-2', - connectorName: 'unchanged', - currentConfiguration: { - connectorName: 'unchanged', - connectorId: 'servicenow-1', - closureType: 'close-by-user', - }, - persistCaseConfigure, - })); - useConnectorsMock.mockImplementation(() => useConnectorsResponse); - useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); - useGetUrlSearchMock.mockImplementation(() => searchURL); - - wrapper = mount(, { wrappingComponent: TestProviders }); - }); - - test('it submits the configuration correctly', () => { - wrapper - .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') - .first() - .simulate('click'); - - wrapper.update(); - - expect(persistCaseConfigure).toHaveBeenCalled(); - expect(persistCaseConfigure).toHaveBeenCalledWith({ - connectorId: 'servicenow-2', - connectorName: 'My Connector 2', - closureType: 'close-by-user', - }); - }); - - test('it has the correct url on cancel button', () => { - expect( - wrapper - .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') - .first() - .prop('href') - ).toBe(`#/link-to/case${searchURL}`); - }); - - test('it disables the buttons of action bar when loading configuration', () => { - useCaseConfigureMock.mockImplementation(() => ({ - ...useCaseConfigureResponse, - mapping: connectors[1].config.casesConfiguration.mapping, - closureType: 'close-by-user', - connectorId: 'servicenow-2', - connectorName: 'unchanged', - currentConfiguration: { - connectorName: 'unchanged', - connectorId: 'servicenow-1', - closureType: 'close-by-user', - }, - loading: true, - })); - const newWrapper = mount(, { - wrappingComponent: TestProviders, - }); - - expect( - newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-cancel-button"]') - .first() - .prop('isDisabled') - ).toBe(true); - - expect( - newWrapper - .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') - .first() - .prop('isDisabled') - ).toBe(true); - }); - }); - - describe('user interactions', () => { - beforeEach(() => { - jest.resetAllMocks(); - useCaseConfigureMock.mockImplementation(() => ({ - ...useCaseConfigureResponse, - mapping: connectors[1].config.casesConfiguration.mapping, - closureType: 'close-by-user', - connectorId: 'servicenow-2', - connectorName: 'unchanged', - currentConfiguration: { - connectorName: 'unchanged', - connectorId: 'servicenow-2', - closureType: 'close-by-user', - }, - })); - useConnectorsMock.mockImplementation(() => useConnectorsResponse); - useKibanaMock.mockImplementation(() => kibanaMockImplementationArgs); - useGetUrlSearchMock.mockImplementation(() => searchURL); - }); - - test('it show the add flyout when pressing the add connector button', () => { - const wrapper = mount(, { wrappingComponent: TestProviders }); - wrapper - .find('button[data-test-subj="case-configure-add-connector-button"]') - .simulate('click'); - wrapper.update(); - - expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(true); - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeFalsy(); - }); - - test('it show the edit flyout when pressing the update connector button', () => { - const wrapper = mount(, { wrappingComponent: TestProviders }); - wrapper - .find('button[data-test-subj="case-configure-update-selected-connector-button"]') - .simulate('click'); - wrapper.update(); - - expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(true); - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeFalsy(); - }); - - test('it tracks the changes successfully', () => { - useCaseConfigureMock.mockImplementation(() => ({ - ...useCaseConfigureResponse, - mapping: connectors[1].config.casesConfiguration.mapping, - closureType: 'close-by-user', - connectorId: 'servicenow-2', - connectorName: 'unchanged', - currentConfiguration: { - connectorName: 'unchanged', - connectorId: 'servicenow-1', - closureType: 'close-by-pushing', - }, - })); - const wrapper = mount(, { wrappingComponent: TestProviders }); - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.update(); - wrapper.find('button[data-test-subj="dropdown-connector-servicenow-2"]').simulate('click'); - wrapper.update(); - wrapper.find('input[id="close-by-pushing"]').simulate('change'); - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') - .first() - .text() - ).toBe('2 unsaved changes'); - }); - - test('it tracks the changes successfully when name changes', () => { - useCaseConfigureMock.mockImplementation(() => ({ - ...useCaseConfigureResponse, - mapping: connectors[1].config.casesConfiguration.mapping, - closureType: 'close-by-user', - connectorId: 'servicenow-2', - connectorName: 'nameChange', - currentConfiguration: { - connectorId: 'servicenow-1', - closureType: 'close-by-pushing', - connectorName: 'before', - }, - })); - - const wrapper = mount(, { wrappingComponent: TestProviders }); - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.update(); - wrapper.find('button[data-test-subj="dropdown-connector-servicenow-2"]').simulate('click'); - wrapper.update(); - wrapper.find('input[id="close-by-pushing"]').simulate('change'); - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') - .first() - .text() - ).toBe('2 unsaved changes'); - }); - - test('it tracks and reverts the changes successfully ', () => { - const wrapper = mount(, { wrappingComponent: TestProviders }); - // change settings - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.update(); - wrapper.find('button[data-test-subj="dropdown-connector-servicenow-2"]').simulate('click'); - wrapper.update(); - wrapper.find('input[id="close-by-pushing"]').simulate('change'); - wrapper.update(); - - // revert back to initial settings - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.update(); - wrapper.find('button[data-test-subj="dropdown-connector-servicenow-1"]').simulate('click'); - wrapper.update(); - wrapper.find('input[id="close-by-user"]').simulate('change'); - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeFalsy(); - }); - - test('it close and restores the action bar when the add connector button is pressed', () => { - useCaseConfigureMock - .mockImplementationOnce(() => ({ - ...useCaseConfigureResponse, - mapping: connectors[1].config.casesConfiguration.mapping, - closureType: 'close-by-user', - connectorId: 'servicenow-2', - currentConfiguration: { connectorId: 'servicenow-2', closureType: 'close-by-user' }, - })) - .mockImplementation(() => ({ - ...useCaseConfigureResponse, - mapping: connectors[1].config.casesConfiguration.mapping, - closureType: 'close-by-pushing', - connectorId: 'servicenow-2', - currentConfiguration: { connectorId: 'servicenow-2', closureType: 'close-by-user' }, - })); - const wrapper = mount(, { wrappingComponent: TestProviders }); - // Change closure type - wrapper.find('input[id="close-by-pushing"]').simulate('change'); - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeTruthy(); - - // Press add connector button - wrapper - .find('button[data-test-subj="case-configure-add-connector-button"]') - .simulate('click'); - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeFalsy(); - - expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(true); - - // Close the add flyout - wrapper.find('button[data-test-subj="euiFlyoutCloseButton"]').simulate('click'); - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeTruthy(); - - expect(wrapper.find(ConnectorAddFlyout).prop('addFlyoutVisible')).toBe(false); - - expect( - wrapper - .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') - .first() - .text() - ).toBe('1 unsaved changes'); - }); - - test('it close and restores the action bar when the update connector button is pressed', () => { - useCaseConfigureMock - .mockImplementationOnce(() => ({ - ...useCaseConfigureResponse, - mapping: connectors[1].config.casesConfiguration.mapping, - closureType: 'close-by-user', - connectorId: 'servicenow-2', - currentConfiguration: { connectorId: 'servicenow-2', closureType: 'close-by-user' }, - })) - .mockImplementation(() => ({ - ...useCaseConfigureResponse, - mapping: connectors[1].config.casesConfiguration.mapping, - closureType: 'close-by-pushing', - connectorId: 'servicenow-2', - currentConfiguration: { connectorId: 'servicenow-2', closureType: 'close-by-user' }, - })); - const wrapper = mount(, { wrappingComponent: TestProviders }); - - // Change closure type - wrapper.find('input[id="close-by-pushing"]').simulate('change'); - wrapper.update(); - - // Press update connector button - wrapper - .find('button[data-test-subj="case-configure-update-selected-connector-button"]') - .simulate('click'); - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeFalsy(); - - expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(true); - - // Close the edit flyout - wrapper.find('button[data-test-subj="euiFlyoutCloseButton"]').simulate('click'); - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeTruthy(); - - expect(wrapper.find(ConnectorEditFlyout).prop('editFlyoutVisible')).toBe(false); - - expect( - wrapper - .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') - .first() - .text() - ).toBe('1 unsaved changes'); - }); - - test('it shows the action bar when the connector is changed', () => { - useCaseConfigureMock - .mockImplementationOnce(() => ({ - ...useCaseConfigureResponse, - mapping: connectors[0].config.casesConfiguration.mapping, - closureType: 'close-by-user', - connectorId: 'servicenow-1', - currentConfiguration: { connectorId: 'servicenow-1', closureType: 'close-by-user' }, - })) - .mockImplementation(() => ({ - ...useCaseConfigureResponse, - mapping: connectors[1].config.casesConfiguration.mapping, - closureType: 'close-by-user', - connectorId: 'servicenow-2', - currentConfiguration: { connectorId: 'servicenow-1', closureType: 'close-by-user' }, - })); - const wrapper = mount(, { wrappingComponent: TestProviders }); - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.update(); - wrapper.find('button[data-test-subj="dropdown-connector-servicenow-2"]').simulate('click'); - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeTruthy(); - expect( - wrapper - .find('[data-test-subj="case-configure-action-bottom-bar-total-changes"]') - .first() - .text() - ).toBe('1 unsaved changes'); - }); - - test('it closes the action bar when pressing save', () => { - useCaseConfigureMock - .mockImplementationOnce(() => ({ - ...useCaseConfigureResponse, - mapping: connectors[1].config.casesConfiguration.mapping, - closureType: 'close-by-user', - connectorId: 'servicenow-2', - currentConfiguration: { connectorId: 'servicenow-2', closureType: 'close-by-user' }, - })) - .mockImplementation(() => ({ - ...useCaseConfigureResponse, - mapping: connectors[1].config.casesConfiguration.mapping, - closureType: 'close-by-pushing', - connectorId: 'servicenow-2', - currentConfiguration: { connectorId: 'servicenow-2', closureType: 'close-by-user' }, - })); - const wrapper = mount(, { wrappingComponent: TestProviders }); - wrapper.find('input[id="close-by-pushing"]').simulate('change'); - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeTruthy(); - - wrapper - .find('[data-test-subj="case-configure-action-bottom-bar-save-button"]') - .first() - .simulate('click'); - - wrapper.update(); - - expect( - wrapper.find('[data-test-subj="case-configure-action-bottom-bar"]').exists() - ).toBeFalsy(); - }); - - test('the text of the update button is changed successfully', () => { - useCaseConfigureMock - .mockImplementationOnce(() => ({ - ...useCaseConfigureResponse, - connectorId: 'servicenow-1', - })) - .mockImplementation(() => ({ - ...useCaseConfigureResponse, - connectorId: 'servicenow-2', - })); - - const wrapper = mount(, { wrappingComponent: TestProviders }); - - wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.update(); - wrapper.find('button[data-test-subj="dropdown-connector-servicenow-2"]').simulate('click'); - wrapper.update(); - - expect( - wrapper - .find('button[data-test-subj="case-configure-update-selected-connector-button"]') - .text() - ).toBe('Update My Connector 2'); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.tsx b/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.tsx deleted file mode 100644 index 739083a5009ecd..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/index.tsx +++ /dev/null @@ -1,289 +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, { useCallback, useEffect, useState, Dispatch, SetStateAction } from 'react'; -import styled, { css } from 'styled-components'; - -import { - EuiFlexGroup, - EuiFlexItem, - EuiButton, - EuiCallOut, - EuiBottomBar, - EuiButtonEmpty, - EuiText, -} from '@elastic/eui'; - -import { difference } from 'lodash/fp'; -import { useKibana } from '../../../../lib/kibana'; -import { useConnectors } from '../../../../containers/case/configure/use_connectors'; -import { useCaseConfigure } from '../../../../containers/case/configure/use_configure'; -import { - ActionsConnectorsContextProvider, - ActionType, - ConnectorAddFlyout, - ConnectorEditFlyout, -} from '../../../../../../triggers_actions_ui/public'; - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ActionConnectorTableItem } from '../../../../../../triggers_actions_ui/public/types'; -import { getCaseUrl } from '../../../../components/link_to'; -import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; -import { connectorsConfiguration } from '../../../../lib/connectors/config'; - -import { Connectors } from '../configure_cases/connectors'; -import { ClosureOptions } from '../configure_cases/closure_options'; -import { SectionWrapper } from '../wrappers'; -import { navTabs } from '../../../../pages/home/home_navigations'; -import * as i18n from './translations'; - -const FormWrapper = styled.div` - ${({ theme }) => css` - & > * { - margin-top 40px; - } - - & > :first-child { - margin-top: 0; - } - - padding-top: ${theme.eui.paddingSizes.xl}; - padding-bottom: ${theme.eui.paddingSizes.xl}; - `} -`; - -const actionTypes: ActionType[] = Object.values(connectorsConfiguration); - -interface ConfigureCasesComponentProps { - userCanCrud: boolean; -} - -const ConfigureCasesComponent: React.FC = ({ userCanCrud }) => { - const search = useGetUrlSearch(navTabs.case); - const { http, triggers_actions_ui, notifications, application, docLinks } = useKibana().services; - - const [connectorIsValid, setConnectorIsValid] = useState(true); - const [addFlyoutVisible, setAddFlyoutVisibility] = useState(false); - const [editFlyoutVisible, setEditFlyoutVisibility] = useState(false); - const [editedConnectorItem, setEditedConnectorItem] = useState( - null - ); - - const [actionBarVisible, setActionBarVisible] = useState(false); - const [totalConfigurationChanges, setTotalConfigurationChanges] = useState(0); - - const { - connectorId, - closureType, - currentConfiguration, - loading: loadingCaseConfigure, - persistLoading, - persistCaseConfigure, - setConnector, - setClosureType, - } = useCaseConfigure(); - - const { loading: isLoadingConnectors, connectors, refetchConnectors } = useConnectors(); - - // ActionsConnectorsContextProvider reloadConnectors prop expects a Promise. - // TODO: Fix it if reloadConnectors type change. - const reloadConnectors = useCallback(async () => refetchConnectors(), []); - const isLoadingAny = isLoadingConnectors || persistLoading || loadingCaseConfigure; - const updateConnectorDisabled = isLoadingAny || !connectorIsValid || connectorId === 'none'; - - const handleSubmit = useCallback( - // TO DO give a warning/error to user when field are not mapped so they have chance to do it - () => { - setActionBarVisible(false); - persistCaseConfigure({ - connectorId, - connectorName: connectors.find(c => c.id === connectorId)?.name ?? '', - closureType, - }); - }, - [connectorId, connectors, closureType] - ); - - const onClickAddConnector = useCallback(() => { - setActionBarVisible(false); - setAddFlyoutVisibility(true); - }, []); - - const onClickUpdateConnector = useCallback(() => { - setActionBarVisible(false); - setEditFlyoutVisibility(true); - }, []); - - const handleActionBar = useCallback(() => { - const currentConfigurationMinusName = { - connectorId: currentConfiguration.connectorId, - closureType: currentConfiguration.closureType, - }; - const unsavedChanges = difference(Object.values(currentConfigurationMinusName), [ - connectorId, - closureType, - ]).length; - setActionBarVisible(!(unsavedChanges === 0)); - setTotalConfigurationChanges(unsavedChanges); - }, [currentConfiguration, connectorId, closureType]); - - const handleSetAddFlyoutVisibility = useCallback( - (isVisible: boolean) => { - handleActionBar(); - setAddFlyoutVisibility(isVisible); - }, - [currentConfiguration, connectorId, closureType] - ); - - const handleSetEditFlyoutVisibility = useCallback( - (isVisible: boolean) => { - handleActionBar(); - setEditFlyoutVisibility(isVisible); - }, - [currentConfiguration, connectorId, closureType] - ); - - useEffect(() => { - if ( - !isLoadingConnectors && - connectorId !== 'none' && - !connectors.some(c => c.id === connectorId) - ) { - setConnectorIsValid(false); - } else if ( - !isLoadingConnectors && - (connectorId === 'none' || connectors.some(c => c.id === connectorId)) - ) { - setConnectorIsValid(true); - } - }, [connectors, connectorId]); - - useEffect(() => { - if (!isLoadingConnectors && connectorId !== 'none') { - setEditedConnectorItem( - connectors.find(c => c.id === connectorId) as ActionConnectorTableItem - ); - } - }, [connectors, connectorId]); - - useEffect(() => { - handleActionBar(); - }, [ - connectors, - connectorId, - closureType, - currentConfiguration.connectorId, - currentConfiguration.closureType, - ]); - - return ( - - {!connectorIsValid && ( - - - {i18n.WARNING_NO_CONNECTOR_MESSAGE} - - - )} - - - - - - - {actionBarVisible && ( - - - - - - {i18n.UNSAVED_CHANGES(totalConfigurationChanges)} - - - - - - - - {i18n.CANCEL} - - - - - {i18n.SAVE_CHANGES} - - - - - - - )} - - >} - actionTypes={actionTypes} - /> - {editedConnectorItem && ( - > - } - /> - )} - - - ); -}; - -export const ConfigureCases = React.memo(ConfigureCasesComponent); diff --git a/x-pack/plugins/siem/public/pages/case/components/configure_cases/utils.ts b/x-pack/plugins/siem/public/pages/case/components/configure_cases/utils.ts deleted file mode 100644 index a44378c22e8929..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/configure_cases/utils.ts +++ /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 { - CaseField, - ActionType, - CasesConfigurationMapping, - ThirdPartyField, -} from '../../../../containers/case/configure/types'; - -export const setActionTypeToMapping = ( - caseField: CaseField, - newActionType: ActionType, - mapping: CasesConfigurationMapping[] -): CasesConfigurationMapping[] => { - const findItemIndex = mapping.findIndex(item => item.source === caseField); - - if (findItemIndex >= 0) { - return [ - ...mapping.slice(0, findItemIndex), - { ...mapping[findItemIndex], actionType: newActionType }, - ...mapping.slice(findItemIndex + 1), - ]; - } - - return [...mapping]; -}; - -export const setThirdPartyToMapping = ( - caseField: CaseField, - newThirdPartyField: ThirdPartyField, - mapping: CasesConfigurationMapping[] -): CasesConfigurationMapping[] => - mapping.map(item => { - if (item.source !== caseField && item.target === newThirdPartyField) { - return { ...item, target: 'not_mapped' }; - } else if (item.source === caseField) { - return { ...item, target: newThirdPartyField }; - } - return item; - }); diff --git a/x-pack/plugins/siem/public/pages/case/components/connector_selector/form.tsx b/x-pack/plugins/siem/public/pages/case/components/connector_selector/form.tsx deleted file mode 100644 index 5f0e498bb40562..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/connector_selector/form.tsx +++ /dev/null @@ -1,65 +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 { EuiFormRow } from '@elastic/eui'; -import React, { useCallback, useEffect } from 'react'; - -import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../shared_imports'; -import { ConnectorsDropdown } from '../configure_cases/connectors_dropdown'; -import { Connector } from '../../../../../../case/common/api/cases'; - -interface ConnectorSelectorProps { - connectors: Connector[]; - dataTestSubj: string; - field: FieldHook; - idAria: string; - defaultValue?: string; - disabled: boolean; - isLoading: boolean; -} -export const ConnectorSelector = ({ - connectors, - dataTestSubj, - defaultValue, - field, - idAria, - disabled = false, - isLoading = false, -}: ConnectorSelectorProps) => { - const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); - - useEffect(() => { - field.setValue(defaultValue); - }, [defaultValue]); - - const handleContentChange = useCallback( - (newContent: string) => { - field.setValue(newContent); - }, - [field] - ); - - return ( - - - - ); -}; diff --git a/x-pack/plugins/siem/public/pages/case/components/create/index.test.tsx b/x-pack/plugins/siem/public/pages/case/components/create/index.test.tsx deleted file mode 100644 index 4c2e15ddfa98a9..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/create/index.test.tsx +++ /dev/null @@ -1,162 +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 { mount } from 'enzyme'; - -import { Create } from './'; -import { TestProviders } from '../../../../mock'; -import { getFormMock } from '../__mock__/form'; -import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; - -import { useInsertTimeline } from '../../../../components/timeline/insert_timeline_popover/use_insert_timeline'; -import { usePostCase } from '../../../../containers/case/use_post_case'; -import { useGetTags } from '../../../../containers/case/use_get_tags'; - -jest.mock('../../../../components/timeline/insert_timeline_popover/use_insert_timeline'); -jest.mock('../../../../containers/case/use_post_case'); -import { useForm } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks'; -import { wait } from '../../../../lib/helpers'; -import { SiemPageName } from '../../../home/types'; -jest.mock( - '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' -); -jest.mock('../../../../containers/case/use_get_tags'); -jest.mock( - '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider', - () => ({ - FormDataProvider: ({ children }: { children: ({ tags }: { tags: string[] }) => void }) => - children({ tags: ['rad', 'dude'] }), - }) -); - -export const useFormMock = useForm as jest.Mock; - -const useInsertTimelineMock = useInsertTimeline as jest.Mock; -const usePostCaseMock = usePostCase as jest.Mock; - -const postCase = jest.fn(); -const handleCursorChange = jest.fn(); -const handleOnTimelineChange = jest.fn(); - -const defaultInsertTimeline = { - cursorPosition: { - start: 0, - end: 0, - }, - handleCursorChange, - handleOnTimelineChange, -}; - -const sampleTags = ['coke', 'pepsi']; -const sampleData = { - description: 'what a great description', - tags: sampleTags, - title: 'what a cool title', -}; -const defaultPostCase = { - isLoading: false, - isError: false, - caseData: null, - postCase, -}; -describe('Create case', () => { - // Suppress warnings about "noSuggestions" prop - /* eslint-disable no-console */ - const originalError = console.error; - beforeAll(() => { - console.error = jest.fn(); - }); - afterAll(() => { - console.error = originalError; - }); - /* eslint-enable no-console */ - const fetchTags = jest.fn(); - const formHookMock = getFormMock(sampleData); - beforeEach(() => { - jest.resetAllMocks(); - useInsertTimelineMock.mockImplementation(() => defaultInsertTimeline); - usePostCaseMock.mockImplementation(() => defaultPostCase); - useFormMock.mockImplementation(() => ({ form: formHookMock })); - jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); - (useGetTags as jest.Mock).mockImplementation(() => ({ - tags: sampleTags, - fetchTags, - })); - }); - - it('should post case on submit click', async () => { - const wrapper = mount( - - - - - - ); - wrapper - .find(`[data-test-subj="create-case-submit"]`) - .first() - .simulate('click'); - await wait(); - expect(postCase).toBeCalledWith(sampleData); - }); - - it('should redirect to all cases on cancel click', () => { - const wrapper = mount( - - - - - - ); - wrapper - .find(`[data-test-subj="create-case-cancel"]`) - .first() - .simulate('click'); - expect(mockHistory.replace.mock.calls[0][0].pathname).toEqual(`/${SiemPageName.case}`); - }); - it('should redirect to new case when caseData is there', () => { - const sampleId = '777777'; - usePostCaseMock.mockImplementation(() => ({ ...defaultPostCase, caseData: { id: sampleId } })); - mount( - - - - - - ); - expect(mockHistory.replace.mock.calls[0][0].pathname).toEqual( - `/${SiemPageName.case}/${sampleId}` - ); - }); - - it('should render spinner when loading', () => { - usePostCaseMock.mockImplementation(() => ({ ...defaultPostCase, isLoading: true })); - const wrapper = mount( - - - - - - ); - expect(wrapper.find(`[data-test-subj="create-case-loading-spinner"]`).exists()).toBeTruthy(); - }); - it('Tag options render with new tags added', () => { - const wrapper = mount( - - - - - - ); - expect( - wrapper - .find(`[data-test-subj="caseTags"] [data-test-subj="input"]`) - .first() - .prop('options') - ).toEqual([{ label: 'coke' }, { label: 'pepsi' }, { label: 'rad' }, { label: 'dude' }]); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/case/components/create/index.tsx b/x-pack/plugins/siem/public/pages/case/components/create/index.tsx deleted file mode 100644 index 6731b88572cdd3..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/create/index.tsx +++ /dev/null @@ -1,215 +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, { useCallback, useEffect, useState } from 'react'; -import { - EuiButton, - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingSpinner, - EuiPanel, -} from '@elastic/eui'; -import styled, { css } from 'styled-components'; -import { Redirect } from 'react-router-dom'; - -import { isEqual } from 'lodash/fp'; -import { CasePostRequest } from '../../../../../../case/common/api'; -import { - Field, - Form, - getUseField, - useForm, - UseField, - FormDataProvider, -} from '../../../../shared_imports'; -import { usePostCase } from '../../../../containers/case/use_post_case'; -import { schema } from './schema'; -import { InsertTimelinePopover } from '../../../../components/timeline/insert_timeline_popover'; -import { useInsertTimeline } from '../../../../components/timeline/insert_timeline_popover/use_insert_timeline'; -import * as i18n from '../../translations'; -import { SiemPageName } from '../../../home/types'; -import { MarkdownEditorForm } from '../../../../components/markdown_editor/form'; -import { useGetTags } from '../../../../containers/case/use_get_tags'; - -export const CommonUseField = getUseField({ component: Field }); - -const ContainerBig = styled.div` - ${({ theme }) => css` - margin-top: ${theme.eui.euiSizeXL}; - `} -`; - -const Container = styled.div` - ${({ theme }) => css` - margin-top: ${theme.eui.euiSize}; - `} -`; -const MySpinner = styled(EuiLoadingSpinner)` - position: absolute; - top: 50%; - left: 50%; - z-index: 99; -`; - -const initialCaseValue: CasePostRequest = { - description: '', - tags: [], - title: '', -}; - -export const Create = React.memo(() => { - const { caseData, isLoading, postCase } = usePostCase(); - const [isCancel, setIsCancel] = useState(false); - const { form } = useForm({ - defaultValue: initialCaseValue, - options: { stripEmptyFields: false }, - schema, - }); - const { tags: tagOptions } = useGetTags(); - const [options, setOptions] = useState( - tagOptions.map(label => ({ - label, - })) - ); - useEffect( - () => - setOptions( - tagOptions.map(label => ({ - label, - })) - ), - [tagOptions] - ); - const { handleCursorChange, handleOnTimelineChange } = useInsertTimeline( - form, - 'description' - ); - - const onSubmit = useCallback(async () => { - const { isValid, data } = await form.submit(); - if (isValid) { - await postCase(data); - } - }, [form]); - - const handleSetIsCancel = useCallback(() => { - setIsCancel(true); - }, []); - - if (caseData != null && caseData.id) { - return ; - } - - if (isCancel) { - return ; - } - - return ( - - {isLoading && } -
- - - - - - - ), - }} - /> - - - {({ tags: anotherTags }) => { - const current: string[] = options.map(opt => opt.label); - const newOptions = anotherTags.reduce((acc: string[], item: string) => { - if (!acc.includes(item)) { - return [...acc, item]; - } - return acc; - }, current); - if (!isEqual(current, newOptions)) { - setOptions( - newOptions.map((label: string) => ({ - label, - })) - ); - } - return null; - }} - - - - - - - {i18n.CANCEL} - - - - - {i18n.CREATE_CASE} - - - - -
- ); -}); - -Create.displayName = 'Create'; diff --git a/x-pack/plugins/siem/public/pages/case/components/create/schema.tsx b/x-pack/plugins/siem/public/pages/case/components/create/schema.tsx deleted file mode 100644 index a4e0bb69165317..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/create/schema.tsx +++ /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. - */ - -import { CasePostRequest } from '../../../../../../case/common/api'; -import { FIELD_TYPES, fieldValidators, FormSchema } from '../../../../shared_imports'; -import * as i18n from '../../translations'; - -import { OptionalFieldLabel } from './optional_field_label'; -const { emptyField } = fieldValidators; - -export const schemaTags = { - type: FIELD_TYPES.COMBO_BOX, - label: i18n.TAGS, - helpText: i18n.TAGS_HELP, - labelAppend: OptionalFieldLabel, -}; - -export const schema: FormSchema = { - title: { - type: FIELD_TYPES.TEXT, - label: i18n.NAME, - validations: [ - { - validator: emptyField(i18n.TITLE_REQUIRED), - }, - ], - }, - description: { - label: i18n.DESCRIPTION, - validations: [ - { - validator: emptyField(i18n.DESCRIPTION_REQUIRED), - }, - ], - }, - tags: schemaTags, -}; diff --git a/x-pack/plugins/siem/public/pages/case/components/edit_connector/index.test.tsx b/x-pack/plugins/siem/public/pages/case/components/edit_connector/index.test.tsx deleted file mode 100644 index 29776360b72da1..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/edit_connector/index.test.tsx +++ /dev/null @@ -1,154 +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 { mount } from 'enzyme'; - -import { EditConnector } from './index'; -import { getFormMock, useFormMock } from '../__mock__/form'; -import { TestProviders } from '../../../../mock'; -import { connectorsMock } from '../../../../containers/case/configure/mock'; -import { wait } from '../../../../lib/helpers'; -import { act } from 'react-dom/test-utils'; -jest.mock( - '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' -); -const onSubmit = jest.fn(); -const defaultProps = { - connectors: connectorsMock, - disabled: false, - isLoading: false, - onSubmit, - selectedConnector: 'none', -}; - -describe('EditConnector ', () => { - const sampleConnector = '123'; - const formHookMock = getFormMock({ connector: sampleConnector }); - beforeEach(() => { - jest.clearAllMocks(); - jest.resetAllMocks(); - useFormMock.mockImplementation(() => ({ form: formHookMock })); - }); - it('Renders no connector, and then edit', () => { - const wrapper = mount( - - - - ); - - expect( - wrapper - .find(`[data-test-subj="dropdown-connectors"]`) - .last() - .prop('disabled') - ).toBeTruthy(); - - expect( - wrapper - .find(`span[data-test-subj="dropdown-connector-no-connector"]`) - .last() - .exists() - ).toBeTruthy(); - wrapper - .find(`[data-test-subj="connector-edit-button"]`) - .last() - .simulate('click'); - - expect( - wrapper - .find(`[data-test-subj="edit-connectors-submit"]`) - .last() - .exists() - ).toBeTruthy(); - - expect( - wrapper - .find(`[data-test-subj="dropdown-connectors"]`) - .last() - .prop('disabled') - ).toBeFalsy(); - }); - it('Edit external service on submit', async () => { - const wrapper = mount( - - - - ); - wrapper - .find(`[data-test-subj="connector-edit-button"]`) - .last() - .simulate('click'); - expect( - wrapper - .find(`[data-test-subj="edit-connectors-submit"]`) - .last() - .exists() - ).toBeTruthy(); - await act(async () => { - wrapper - .find(`[data-test-subj="edit-connectors-submit"]`) - .last() - .simulate('click'); - await wait(); - expect(onSubmit).toBeCalledWith(sampleConnector); - }); - }); - it('Resets selector on cancel', async () => { - const props = { - ...defaultProps, - }; - const wrapper = mount( - - - - ); - wrapper - .find(`[data-test-subj="connector-edit-button"]`) - .last() - .simulate('click'); - await act(async () => { - wrapper - .find(`[data-test-subj="edit-connectors-cancel"]`) - .last() - .simulate('click'); - await wait(); - wrapper.update(); - expect(formHookMock.setFieldValue).toBeCalledWith( - 'connector', - defaultProps.selectedConnector - ); - }); - }); - it('Renders disabled button', () => { - const props = { ...defaultProps, disabled: true }; - const wrapper = mount( - - - - ); - expect( - wrapper - .find(`[data-test-subj="connector-edit-button"]`) - .last() - .prop('disabled') - ).toBeTruthy(); - }); - it('Renders loading spinner', () => { - const props = { ...defaultProps, isLoading: true }; - const wrapper = mount( - - - - ); - expect( - wrapper - .find(`[data-test-subj="connector-loading"]`) - .last() - .exists() - ).toBeTruthy(); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/case/components/edit_connector/index.tsx b/x-pack/plugins/siem/public/pages/case/components/edit_connector/index.tsx deleted file mode 100644 index 83be8b5ad7e5a4..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/edit_connector/index.tsx +++ /dev/null @@ -1,149 +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, { useCallback, useState } from 'react'; -import { - EuiText, - EuiHorizontalRule, - EuiFlexGroup, - EuiFlexItem, - EuiButton, - EuiButtonEmpty, - EuiButtonIcon, - EuiLoadingSpinner, -} from '@elastic/eui'; -import styled, { css } from 'styled-components'; -import * as i18n from '../../translations'; -import { Form, UseField, useForm } from '../../../../shared_imports'; -import { schema } from './schema'; -import { ConnectorSelector } from '../connector_selector/form'; -import { Connector } from '../../../../../../case/common/api/cases'; - -interface EditConnectorProps { - connectors: Connector[]; - disabled?: boolean; - isLoading: boolean; - onSubmit: (a: string[]) => void; - selectedConnector: string; -} - -const MyFlexGroup = styled(EuiFlexGroup)` - ${({ theme }) => css` - margin-top: ${theme.eui.euiSizeM}; - p { - font-size: ${theme.eui.euiSizeM}; - } - `} -`; - -export const EditConnector = React.memo( - ({ - connectors, - disabled = false, - isLoading, - onSubmit, - selectedConnector, - }: EditConnectorProps) => { - const { form } = useForm({ - defaultValue: { connectors }, - options: { stripEmptyFields: false }, - schema, - }); - const [isEditConnector, setIsEditConnector] = useState(false); - const handleOnClick = useCallback(() => { - setIsEditConnector(true); - }, []); - - const onCancelConnector = useCallback(() => { - form.setFieldValue('connector', selectedConnector); - setIsEditConnector(false); - }, [form, selectedConnector]); - - const onSubmitConnector = useCallback(async () => { - const { isValid, data: newData } = await form.submit(); - if (isValid && newData.connector) { - onSubmit(newData.connector); - setIsEditConnector(false); - } - }, [form, onSubmit]); - return ( - - - -

{i18n.CONNECTORS}

-
- {isLoading && } - {!isLoading && ( - - - - )} -
- - - - -
- - - - - -
-
- {isEditConnector && ( - - - - - {i18n.SAVE} - - - - - {i18n.CANCEL} - - - - - )} -
-
-
- ); - } -); - -EditConnector.displayName = 'EditConnector'; diff --git a/x-pack/plugins/siem/public/pages/case/components/edit_connector/schema.tsx b/x-pack/plugins/siem/public/pages/case/components/edit_connector/schema.tsx deleted file mode 100644 index 4b9008839e695c..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/edit_connector/schema.tsx +++ /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 { FormSchema } from '../../../../shared_imports'; - -export const schema: FormSchema = { - connector: { - defaultValue: 'none', - }, -}; diff --git a/x-pack/plugins/siem/public/pages/case/components/tag_list/index.test.tsx b/x-pack/plugins/siem/public/pages/case/components/tag_list/index.test.tsx deleted file mode 100644 index 9ddb96a4ed295a..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/tag_list/index.test.tsx +++ /dev/null @@ -1,180 +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 { mount } from 'enzyme'; -import { act } from 'react-dom/test-utils'; - -import { TagList } from './'; -import { getFormMock } from '../__mock__/form'; -import { TestProviders } from '../../../../mock'; -import { wait } from '../../../../lib/helpers'; -import { useForm } from '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks'; -import { useGetTags } from '../../../../containers/case/use_get_tags'; - -jest.mock( - '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form' -); -jest.mock('../../../../containers/case/use_get_tags'); -jest.mock( - '../../../../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib/components/form_data_provider', - () => ({ - FormDataProvider: ({ children }: { children: ({ tags }: { tags: string[] }) => void }) => - children({ tags: ['rad', 'dude'] }), - }) -); -const onSubmit = jest.fn(); -const defaultProps = { - disabled: false, - isLoading: false, - onSubmit, - tags: [], -}; - -describe('TagList ', () => { - // Suppress warnings about "noSuggestions" prop - /* eslint-disable no-console */ - const originalError = console.error; - beforeAll(() => { - console.error = jest.fn(); - }); - afterAll(() => { - console.error = originalError; - }); - /* eslint-enable no-console */ - const sampleTags = ['coke', 'pepsi']; - const fetchTags = jest.fn(); - const formHookMock = getFormMock({ tags: sampleTags }); - beforeEach(() => { - jest.resetAllMocks(); - (useForm as jest.Mock).mockImplementation(() => ({ form: formHookMock })); - - (useGetTags as jest.Mock).mockImplementation(() => ({ - tags: sampleTags, - fetchTags, - })); - }); - it('Renders no tags, and then edit', () => { - const wrapper = mount( - - - - ); - expect( - wrapper - .find(`[data-test-subj="no-tags"]`) - .last() - .exists() - ).toBeTruthy(); - wrapper - .find(`[data-test-subj="tag-list-edit-button"]`) - .last() - .simulate('click'); - expect( - wrapper - .find(`[data-test-subj="no-tags"]`) - .last() - .exists() - ).toBeFalsy(); - expect( - wrapper - .find(`[data-test-subj="edit-tags"]`) - .last() - .exists() - ).toBeTruthy(); - }); - it('Edit tag on submit', async () => { - const wrapper = mount( - - - - ); - wrapper - .find(`[data-test-subj="tag-list-edit-button"]`) - .last() - .simulate('click'); - await act(async () => { - wrapper - .find(`[data-test-subj="edit-tags-submit"]`) - .last() - .simulate('click'); - await wait(); - expect(onSubmit).toBeCalledWith(sampleTags); - }); - }); - it('Tag options render with new tags added', () => { - const wrapper = mount( - - - - ); - wrapper - .find(`[data-test-subj="tag-list-edit-button"]`) - .last() - .simulate('click'); - expect( - wrapper - .find(`[data-test-subj="caseTags"] [data-test-subj="input"]`) - .first() - .prop('options') - ).toEqual([{ label: 'coke' }, { label: 'pepsi' }, { label: 'rad' }, { label: 'dude' }]); - }); - it('Cancels on cancel', async () => { - const props = { - ...defaultProps, - tags: ['pepsi'], - }; - const wrapper = mount( - - - - ); - expect( - wrapper - .find(`[data-test-subj="case-tag"]`) - .last() - .exists() - ).toBeTruthy(); - wrapper - .find(`[data-test-subj="tag-list-edit-button"]`) - .last() - .simulate('click'); - await act(async () => { - expect( - wrapper - .find(`[data-test-subj="case-tag"]`) - .last() - .exists() - ).toBeFalsy(); - wrapper - .find(`[data-test-subj="edit-tags-cancel"]`) - .last() - .simulate('click'); - await wait(); - wrapper.update(); - expect( - wrapper - .find(`[data-test-subj="case-tag"]`) - .last() - .exists() - ).toBeTruthy(); - }); - }); - it('Renders disabled button', () => { - const props = { ...defaultProps, disabled: true }; - const wrapper = mount( - - - - ); - expect( - wrapper - .find(`[data-test-subj="tag-list-edit-button"]`) - .last() - .prop('disabled') - ).toBeTruthy(); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/case/components/tag_list/index.tsx b/x-pack/plugins/siem/public/pages/case/components/tag_list/index.tsx deleted file mode 100644 index c61feab0bab989..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/tag_list/index.tsx +++ /dev/null @@ -1,179 +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, { useCallback, useEffect, useState } from 'react'; -import { - EuiText, - EuiHorizontalRule, - EuiFlexGroup, - EuiFlexItem, - EuiBadge, - EuiButton, - EuiButtonEmpty, - EuiButtonIcon, - EuiLoadingSpinner, -} from '@elastic/eui'; -import styled, { css } from 'styled-components'; -import { isEqual } from 'lodash/fp'; -import * as i18n from './translations'; -import { Form, FormDataProvider, useForm } from '../../../../shared_imports'; -import { schema } from './schema'; -import { CommonUseField } from '../create'; -import { useGetTags } from '../../../../containers/case/use_get_tags'; - -interface TagListProps { - disabled?: boolean; - isLoading: boolean; - onSubmit: (a: string[]) => void; - tags: string[]; -} - -const MyFlexGroup = styled(EuiFlexGroup)` - ${({ theme }) => css` - margin-top: ${theme.eui.euiSizeM}; - p { - font-size: ${theme.eui.euiSizeM}; - } - `} -`; - -export const TagList = React.memo( - ({ disabled = false, isLoading, onSubmit, tags }: TagListProps) => { - const { form } = useForm({ - defaultValue: { tags }, - options: { stripEmptyFields: false }, - schema, - }); - const [isEditTags, setIsEditTags] = useState(false); - - const onSubmitTags = useCallback(async () => { - const { isValid, data: newData } = await form.submit(); - if (isValid && newData.tags) { - onSubmit(newData.tags); - setIsEditTags(false); - } - }, [form, onSubmit]); - const { tags: tagOptions } = useGetTags(); - const [options, setOptions] = useState( - tagOptions.map(label => ({ - label, - })) - ); - - useEffect( - () => - setOptions( - tagOptions.map(label => ({ - label, - })) - ), - [tagOptions] - ); - - return ( - - - -

{i18n.TAGS}

-
- {isLoading && } - {!isLoading && ( - - - - )} -
- - - {tags.length === 0 && !isEditTags &&

{i18n.NO_TAGS}

} - {tags.length > 0 && - !isEditTags && - tags.map((tag, key) => ( - - - {tag} - - - ))} - {isEditTags && ( - - -
- - - {({ tags: anotherTags }) => { - const current: string[] = options.map(opt => opt.label); - const newOptions = anotherTags.reduce((acc: string[], item: string) => { - if (!acc.includes(item)) { - return [...acc, item]; - } - return acc; - }, current); - if (!isEqual(current, newOptions)) { - setOptions( - newOptions.map((label: string) => ({ - label, - })) - ); - } - return null; - }} - - -
- - - - - {i18n.SAVE} - - - - - {i18n.CANCEL} - - - - -
- )} -
-
- ); - } -); - -TagList.displayName = 'TagList'; diff --git a/x-pack/plugins/siem/public/pages/case/components/tag_list/schema.tsx b/x-pack/plugins/siem/public/pages/case/components/tag_list/schema.tsx deleted file mode 100644 index 50ba114de528e0..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/tag_list/schema.tsx +++ /dev/null @@ -1,11 +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 { FormSchema } from '../../../../shared_imports'; -import { schemaTags } from '../create/schema'; - -export const schema: FormSchema = { - tags: schemaTags, -}; diff --git a/x-pack/plugins/siem/public/pages/case/components/use_push_to_service/helpers.tsx b/x-pack/plugins/siem/public/pages/case/components/use_push_to_service/helpers.tsx deleted file mode 100644 index 0613c40d1181d6..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/use_push_to_service/helpers.tsx +++ /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 { EuiLink } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React from 'react'; - -import * as i18n from './translations'; -import { ActionLicense } from '../../../../containers/case/types'; - -export const getLicenseError = () => ({ - title: i18n.PUSH_DISABLE_BY_LICENSE_TITLE, - description: ( - - {i18n.LINK_CLOUD_DEPLOYMENT} - - ), - }} - /> - ), -}); - -export const getKibanaConfigError = () => ({ - title: i18n.PUSH_DISABLE_BY_KIBANA_CONFIG_TITLE, - description: ( - - {'coming soon...'} - - ), - }} - /> - ), -}); - -export const getActionLicenseError = ( - actionLicense: ActionLicense | null -): Array<{ title: string; description: JSX.Element }> => { - let errors: Array<{ title: string; description: JSX.Element }> = []; - if (actionLicense != null && !actionLicense.enabledInLicense) { - errors = [...errors, getLicenseError()]; - } - if (actionLicense != null && !actionLicense.enabledInConfig) { - errors = [...errors, getKibanaConfigError()]; - } - return errors; -}; diff --git a/x-pack/plugins/siem/public/pages/case/components/use_push_to_service/index.test.tsx b/x-pack/plugins/siem/public/pages/case/components/use_push_to_service/index.test.tsx deleted file mode 100644 index b19c2dbf5273a5..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/use_push_to_service/index.test.tsx +++ /dev/null @@ -1,156 +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. - */ -/* eslint-disable react/display-name */ -import React from 'react'; -import { renderHook, act } from '@testing-library/react-hooks'; -import { usePushToService, ReturnUsePushToService, UsePushToService } from './'; -import { TestProviders } from '../../../../mock'; -import { usePostPushToService } from '../../../../containers/case/use_post_push_to_service'; -import { basicPush, actionLicenses } from '../../../../containers/case/mock'; -import * as i18n from './translations'; -import { useGetActionLicense } from '../../../../containers/case/use_get_action_license'; -import { getKibanaConfigError, getLicenseError } from './helpers'; -import { connectorsMock } from '../../../../containers/case/configure/mock'; -jest.mock('../../../../containers/case/use_get_action_license'); -jest.mock('../../../../containers/case/use_post_push_to_service'); -jest.mock('../../../../containers/case/configure/api'); - -describe('usePushToService', () => { - const caseId = '12345'; - const updateCase = jest.fn(); - const postPushToService = jest.fn(); - const mockPostPush = { - isLoading: false, - postPushToService, - }; - const mockConnector = connectorsMock[0]; - const actionLicense = actionLicenses[0]; - const caseServices = { - '123': { - ...basicPush, - firstPushIndex: 0, - lastPushIndex: 0, - commentsToUpdate: [], - hasDataToPush: true, - }, - }; - const defaultArgs = { - caseConnectorId: mockConnector.id, - caseConnectorName: mockConnector.name, - caseId, - caseServices, - caseStatus: 'open', - connectors: connectorsMock, - updateCase, - userCanCrud: true, - }; - beforeEach(() => { - jest.resetAllMocks(); - (usePostPushToService as jest.Mock).mockImplementation(() => mockPostPush); - (useGetActionLicense as jest.Mock).mockImplementation(() => ({ - isLoading: false, - actionLicense, - })); - }); - it('push case button posts the push with correct args', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook( - () => usePushToService(defaultArgs), - { - wrapper: ({ children }) => {children}, - } - ); - await waitForNextUpdate(); - result.current.pushButton.props.children.props.onClick(); - expect(postPushToService).toBeCalledWith({ - caseId, - caseServices, - connectorId: mockConnector.id, - connectorName: mockConnector.name, - updateCase, - }); - expect(result.current.pushCallouts).toBeNull(); - }); - }); - it('Displays message when user does not have premium license', async () => { - (useGetActionLicense as jest.Mock).mockImplementation(() => ({ - isLoading: false, - actionLicense: { - ...actionLicense, - enabledInLicense: false, - }, - })); - await act(async () => { - const { result, waitForNextUpdate } = renderHook( - () => usePushToService(defaultArgs), - { - wrapper: ({ children }) => {children}, - } - ); - await waitForNextUpdate(); - const errorsMsg = result.current.pushCallouts?.props.messages; - expect(errorsMsg).toHaveLength(1); - expect(errorsMsg[0].title).toEqual(getLicenseError().title); - }); - }); - it('Displays message when user does not have case enabled in config', async () => { - (useGetActionLicense as jest.Mock).mockImplementation(() => ({ - isLoading: false, - actionLicense: { - ...actionLicense, - enabledInConfig: false, - }, - })); - await act(async () => { - const { result, waitForNextUpdate } = renderHook( - () => usePushToService(defaultArgs), - { - wrapper: ({ children }) => {children}, - } - ); - await waitForNextUpdate(); - const errorsMsg = result.current.pushCallouts?.props.messages; - expect(errorsMsg).toHaveLength(1); - expect(errorsMsg[0].title).toEqual(getKibanaConfigError().title); - }); - }); - it('Displays message when user does not have a connector configured', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook( - () => - usePushToService({ - ...defaultArgs, - caseConnectorId: 'none', - }), - { - wrapper: ({ children }) => {children}, - } - ); - await waitForNextUpdate(); - const errorsMsg = result.current.pushCallouts?.props.messages; - expect(errorsMsg).toHaveLength(1); - expect(errorsMsg[0].title).toEqual(i18n.PUSH_DISABLE_BY_NO_CASE_CONFIG_TITLE); - }); - }); - it('Displays message when case is closed', async () => { - await act(async () => { - const { result, waitForNextUpdate } = renderHook( - () => - usePushToService({ - ...defaultArgs, - caseStatus: 'closed', - }), - { - wrapper: ({ children }) => {children}, - } - ); - await waitForNextUpdate(); - const errorsMsg = result.current.pushCallouts?.props.messages; - expect(errorsMsg).toHaveLength(1); - expect(errorsMsg[0].title).toEqual(i18n.PUSH_DISABLE_BECAUSE_CASE_CLOSED_TITLE); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/case/components/use_push_to_service/index.tsx b/x-pack/plugins/siem/public/pages/case/components/use_push_to_service/index.tsx deleted file mode 100644 index 7f3a951339ef1c..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/use_push_to_service/index.tsx +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiButton, EuiLink, EuiToolTip } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { useCallback, useMemo } from 'react'; - -import { Case } from '../../../../containers/case/types'; -import { useGetActionLicense } from '../../../../containers/case/use_get_action_license'; -import { usePostPushToService } from '../../../../containers/case/use_post_push_to_service'; -import { getConfigureCasesUrl } from '../../../../components/link_to'; -import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; -import { navTabs } from '../../../home/home_navigations'; -import { CaseCallOut } from '../callout'; -import { getLicenseError, getKibanaConfigError } from './helpers'; -import * as i18n from './translations'; -import { Connector } from '../../../../../../case/common/api/cases'; -import { CaseServices } from '../../../../containers/case/use_get_case_user_actions'; - -export interface UsePushToService { - caseId: string; - caseStatus: string; - caseConnectorId: string; - caseConnectorName: string; - caseServices: CaseServices; - connectors: Connector[]; - updateCase: (newCase: Case) => void; - userCanCrud: boolean; -} - -export interface ReturnUsePushToService { - pushButton: JSX.Element; - pushCallouts: JSX.Element | null; -} - -export const usePushToService = ({ - caseConnectorId, - caseConnectorName, - caseId, - caseServices, - caseStatus, - connectors, - updateCase, - userCanCrud, -}: UsePushToService): ReturnUsePushToService => { - const urlSearch = useGetUrlSearch(navTabs.case); - - const { isLoading, postPushToService } = usePostPushToService(); - - const { isLoading: loadingLicense, actionLicense } = useGetActionLicense(); - - const handlePushToService = useCallback(() => { - if (caseConnectorId != null && caseConnectorId !== 'none') { - postPushToService({ - caseId, - caseServices, - connectorId: caseConnectorId, - connectorName: caseConnectorName, - updateCase, - }); - } - }, [caseId, caseServices, caseConnectorId, caseConnectorName, postPushToService, updateCase]); - - const errorsMsg = useMemo(() => { - let errors: Array<{ title: string; description: JSX.Element }> = []; - if (actionLicense != null && !actionLicense.enabledInLicense) { - errors = [...errors, getLicenseError()]; - } - if (connectors.length === 0 && !loadingLicense) { - errors = [ - ...errors, - { - title: i18n.PUSH_DISABLE_BY_NO_CONFIG_TITLE, - description: ( - - {i18n.LINK_CONNECTOR_CONFIGURE} - - ), - }} - /> - ), - }, - ]; - } else if (caseConnectorId === 'none' && !loadingLicense) { - errors = [ - ...errors, - { - title: i18n.PUSH_DISABLE_BY_NO_CASE_CONFIG_TITLE, - description: ( - - ), - }, - ]; - } - if (caseStatus === 'closed') { - errors = [ - ...errors, - { - title: i18n.PUSH_DISABLE_BECAUSE_CASE_CLOSED_TITLE, - description: ( - - ), - }, - ]; - } - if (actionLicense != null && !actionLicense.enabledInConfig) { - errors = [...errors, getKibanaConfigError()]; - } - return errors; - }, [actionLicense, caseStatus, connectors.length, caseConnectorId, loadingLicense, urlSearch]); - - const pushToServiceButton = useMemo(() => { - return ( - 0 || !userCanCrud} - isLoading={isLoading} - > - {caseServices[caseConnectorId] - ? i18n.UPDATE_THIRD(caseConnectorName) - : i18n.PUSH_THIRD(caseConnectorName)} - - ); - }, [ - caseConnectorId, - caseConnectorName, - connectors, - errorsMsg, - handlePushToService, - isLoading, - loadingLicense, - userCanCrud, - ]); - - const objToReturn = useMemo(() => { - return { - pushButton: - errorsMsg.length > 0 ? ( - {errorsMsg[0].description}

} - > - {pushToServiceButton} -
- ) : ( - <>{pushToServiceButton} - ), - pushCallouts: - errorsMsg.length > 0 ? ( - - ) : null, - }; - }, [errorsMsg, pushToServiceButton]); - - return objToReturn; -}; diff --git a/x-pack/plugins/siem/public/pages/case/components/user_action_tree/helpers.test.tsx b/x-pack/plugins/siem/public/pages/case/components/user_action_tree/helpers.test.tsx deleted file mode 100644 index 6e7c2979f80bb4..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/user_action_tree/helpers.test.tsx +++ /dev/null @@ -1,169 +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 { basicPush, getUserAction } from '../../../../containers/case/mock'; -import { getLabelTitle } from './helpers'; -import * as i18n from '../case_view/translations'; -import { mount } from 'enzyme'; -import { connectorsMock } from '../../../../containers/case/configure/mock'; - -describe('User action tree helpers', () => { - const connectors = connectorsMock; - it('label title generated for update tags', () => { - const action = getUserAction(['title'], 'update'); - const result: string | JSX.Element = getLabelTitle({ - action, - connectors, - field: 'tags', - firstPush: false, - }); - - const wrapper = mount(<>{result}); - expect( - wrapper - .find(`[data-test-subj="ua-tags-label"]`) - .first() - .text() - ).toEqual(` ${i18n.TAGS.toLowerCase()}`); - - expect( - wrapper - .find(`[data-test-subj="ua-tag"]`) - .first() - .text() - ).toEqual(action.newValue); - }); - it('label title generated for update title', () => { - const action = getUserAction(['title'], 'update'); - const result: string | JSX.Element = getLabelTitle({ - action, - connectors, - field: 'title', - firstPush: false, - }); - - expect(result).toEqual( - `${i18n.CHANGED_FIELD.toLowerCase()} ${i18n.CASE_NAME.toLowerCase()} ${i18n.TO} "${ - action.newValue - }"` - ); - }); - it('label title generated for update description', () => { - const action = getUserAction(['description'], 'update'); - const result: string | JSX.Element = getLabelTitle({ - action, - connectors, - field: 'description', - firstPush: false, - }); - - expect(result).toEqual(`${i18n.EDITED_FIELD} ${i18n.DESCRIPTION.toLowerCase()}`); - }); - it('label title generated for update status to open', () => { - const action = { ...getUserAction(['status'], 'update'), newValue: 'open' }; - const result: string | JSX.Element = getLabelTitle({ - action, - connectors, - field: 'status', - firstPush: false, - }); - - expect(result).toEqual(`${i18n.REOPENED_CASE.toLowerCase()} ${i18n.CASE}`); - }); - it('label title generated for update status to closed', () => { - const action = { ...getUserAction(['status'], 'update'), newValue: 'closed' }; - const result: string | JSX.Element = getLabelTitle({ - action, - connectors, - field: 'status', - firstPush: false, - }); - - expect(result).toEqual(`${i18n.CLOSED_CASE.toLowerCase()} ${i18n.CASE}`); - }); - it('label title generated for update comment', () => { - const action = getUserAction(['comment'], 'update'); - const result: string | JSX.Element = getLabelTitle({ - action, - connectors, - field: 'comment', - firstPush: false, - }); - - expect(result).toEqual(`${i18n.EDITED_FIELD} ${i18n.COMMENT.toLowerCase()}`); - }); - it('label title generated for pushed incident', () => { - const action = getUserAction(['pushed'], 'push-to-service'); - const result: string | JSX.Element = getLabelTitle({ - action, - connectors, - field: 'pushed', - firstPush: true, - }); - - const wrapper = mount(<>{result}); - expect( - wrapper - .find(`[data-test-subj="pushed-label"]`) - .first() - .text() - ).toEqual(`${i18n.PUSHED_NEW_INCIDENT} ${basicPush.connectorName}`); - expect( - wrapper - .find(`[data-test-subj="pushed-value"]`) - .first() - .prop('href') - ).toEqual(JSON.parse(action.newValue).external_url); - }); - it('label title generated for needs update incident', () => { - const action = getUserAction(['pushed'], 'push-to-service'); - const result: string | JSX.Element = getLabelTitle({ - action, - connectors, - field: 'pushed', - firstPush: false, - }); - - const wrapper = mount(<>{result}); - expect( - wrapper - .find(`[data-test-subj="pushed-label"]`) - .first() - .text() - ).toEqual(`${i18n.UPDATE_INCIDENT} ${basicPush.connectorName}`); - expect( - wrapper - .find(`[data-test-subj="pushed-value"]`) - .first() - .prop('href') - ).toEqual(JSON.parse(action.newValue).external_url); - }); - it('label title generated for update connector', () => { - const action = getUserAction(['connector_id'], 'update'); - const result: string | JSX.Element = getLabelTitle({ - action, - connectors, - field: 'tags', - firstPush: false, - }); - - const wrapper = mount(<>{result}); - expect( - wrapper - .find(`[data-test-subj="ua-tags-label"]`) - .first() - .text() - ).toEqual(` ${i18n.TAGS.toLowerCase()}`); - - expect( - wrapper - .find(`[data-test-subj="ua-tag"]`) - .first() - .text() - ).toEqual(action.newValue); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/case/components/user_action_tree/helpers.tsx b/x-pack/plugins/siem/public/pages/case/components/user_action_tree/helpers.tsx deleted file mode 100644 index 285fa3c58c18a7..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/user_action_tree/helpers.tsx +++ /dev/null @@ -1,80 +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 { EuiFlexGroup, EuiFlexItem, EuiBadge, EuiLink } from '@elastic/eui'; -import React from 'react'; - -import { CaseFullExternalService, Connector } from '../../../../../../case/common/api'; -import { CaseUserActions } from '../../../../containers/case/types'; -import * as i18n from '../case_view/translations'; - -interface LabelTitle { - action: CaseUserActions; - connectors: Connector[]; - field: string; - firstPush: boolean; -} - -export const getLabelTitle = ({ action, connectors, field, firstPush }: LabelTitle) => { - if (field === 'tags') { - return getTagsLabelTitle(action); - } else if (field === 'title' && action.action === 'update') { - return `${i18n.CHANGED_FIELD.toLowerCase()} ${i18n.CASE_NAME.toLowerCase()} ${i18n.TO} "${ - action.newValue - }"`; - } else if (field === 'connector_id' && action.action === 'update') { - const newConnector = connectors.find(c => c.id === action.newValue); - return action.newValue != null && action.newValue !== 'none' && newConnector != null - ? i18n.SELECTED_THIRD_PARTY(newConnector.name) - : i18n.REMOVED_THIRD_PARTY; - } else if (field === 'description' && action.action === 'update') { - return `${i18n.EDITED_FIELD} ${i18n.DESCRIPTION.toLowerCase()}`; - } else if (field === 'status' && action.action === 'update') { - return `${ - action.newValue === 'open' ? i18n.REOPENED_CASE.toLowerCase() : i18n.CLOSED_CASE.toLowerCase() - } ${i18n.CASE}`; - } else if (field === 'comment' && action.action === 'update') { - return `${i18n.EDITED_FIELD} ${i18n.COMMENT.toLowerCase()}`; - } else if (field === 'pushed' && action.action === 'push-to-service' && action.newValue != null) { - return getPushedServiceLabelTitle(action, firstPush); - } - return ''; -}; - -const getTagsLabelTitle = (action: CaseUserActions) => ( - - - {action.action === 'add' && i18n.ADDED_FIELD} - {action.action === 'delete' && i18n.REMOVED_FIELD} {i18n.TAGS.toLowerCase()} - - {action.newValue != null && - action.newValue.split(',').map(tag => ( - - - {tag} - - - ))} - -); - -const getPushedServiceLabelTitle = (action: CaseUserActions, firstPush: boolean) => { - const pushedVal = JSON.parse(action.newValue ?? '') as CaseFullExternalService; - return ( - - - {`${firstPush ? i18n.PUSHED_NEW_INCIDENT : i18n.UPDATE_INCIDENT} ${ - pushedVal?.connector_name - }`} - - - - {pushedVal?.external_title} - - - - ); -}; diff --git a/x-pack/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx b/x-pack/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx deleted file mode 100644 index b9a94f83fded1a..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/user_action_tree/index.test.tsx +++ /dev/null @@ -1,343 +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 { mount } from 'enzyme'; - -import { Router, routeData, mockHistory, mockLocation } from '../__mock__/router'; -import { getFormMock, useFormMock } from '../__mock__/form'; -import { useUpdateComment } from '../../../../containers/case/use_update_comment'; -import { basicCase, basicPush, getUserAction } from '../../../../containers/case/mock'; -import { UserActionTree } from './'; -import { TestProviders } from '../../../../mock'; -import { wait } from '../../../../lib/helpers'; -import { act } from 'react-dom/test-utils'; - -const fetchUserActions = jest.fn(); -const onUpdateField = jest.fn(); -const updateCase = jest.fn(); -const defaultProps = { - caseServices: {}, - caseUserActions: [], - connectors: [], - data: basicCase, - fetchUserActions, - isLoadingDescription: false, - isLoadingUserActions: false, - onUpdateField, - updateCase, - userCanCrud: true, -}; -const useUpdateCommentMock = useUpdateComment as jest.Mock; -jest.mock('../../../../containers/case/use_update_comment'); - -const patchComment = jest.fn(); -describe('UserActionTree ', () => { - const sampleData = { - content: 'what a great comment update', - }; - beforeEach(() => { - jest.clearAllMocks(); - jest.resetAllMocks(); - useUpdateCommentMock.mockImplementation(() => ({ - isLoadingIds: [], - patchComment, - })); - const formHookMock = getFormMock(sampleData); - useFormMock.mockImplementation(() => ({ form: formHookMock })); - jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); - }); - - it('Loading spinner when user actions loading and displays fullName/username', () => { - const wrapper = mount( - - - - - - ); - expect(wrapper.find(`[data-test-subj="user-actions-loading"]`).exists()).toBeTruthy(); - - expect( - wrapper - .find(`[data-test-subj="user-action-avatar"]`) - .first() - .prop('name') - ).toEqual(defaultProps.data.createdBy.fullName); - expect( - wrapper - .find(`[data-test-subj="user-action-title"] strong`) - .first() - .text() - ).toEqual(defaultProps.data.createdBy.username); - }); - it('Renders service now update line with top and bottom when push is required', () => { - const ourActions = [ - getUserAction(['pushed'], 'push-to-service'), - getUserAction(['comment'], 'update'), - ]; - const props = { - ...defaultProps, - caseServices: { - '123': { - ...basicPush, - firstPushIndex: 0, - lastPushIndex: 0, - commentsToUpdate: [`${ourActions[ourActions.length - 1].commentId}`], - hasDataToPush: true, - }, - }, - caseUserActions: ourActions, - }; - const wrapper = mount( - - - - - - ); - expect(wrapper.find(`[data-test-subj="show-top-footer"]`).exists()).toBeTruthy(); - expect(wrapper.find(`[data-test-subj="show-bottom-footer"]`).exists()).toBeTruthy(); - }); - it('Renders service now update line with top only when push is up to date', () => { - const ourActions = [getUserAction(['pushed'], 'push-to-service')]; - const props = { - ...defaultProps, - caseUserActions: ourActions, - caseServices: { - '123': { - ...basicPush, - firstPushIndex: 0, - lastPushIndex: 0, - commentsToUpdate: [], - hasDataToPush: false, - }, - }, - }; - const wrapper = mount( - - - - - - ); - expect(wrapper.find(`[data-test-subj="show-top-footer"]`).exists()).toBeTruthy(); - expect(wrapper.find(`[data-test-subj="show-bottom-footer"]`).exists()).toBeFalsy(); - }); - - it('Outlines comment when update move to link is clicked', () => { - const ourActions = [getUserAction(['comment'], 'create'), getUserAction(['comment'], 'update')]; - const props = { - ...defaultProps, - caseUserActions: ourActions, - }; - const wrapper = mount( - - - - - - ); - expect( - wrapper - .find(`[data-test-subj="comment-create-action"]`) - .first() - .prop('idToOutline') - ).toEqual(''); - wrapper - .find(`[data-test-subj="comment-update-action"] [data-test-subj="move-to-link"]`) - .first() - .simulate('click'); - expect( - wrapper - .find(`[data-test-subj="comment-create-action"]`) - .first() - .prop('idToOutline') - ).toEqual(ourActions[0].commentId); - }); - - it('Switches to markdown when edit is clicked and back to panel when canceled', () => { - const ourActions = [getUserAction(['comment'], 'create')]; - const props = { - ...defaultProps, - caseUserActions: ourActions, - }; - const wrapper = mount( - - - - - - ); - expect( - wrapper - .find( - `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]` - ) - .exists() - ).toEqual(false); - wrapper - .find(`[data-test-subj="comment-create-action"] [data-test-subj="property-actions-ellipses"]`) - .first() - .simulate('click'); - wrapper - .find(`[data-test-subj="comment-create-action"] [data-test-subj="property-actions-pencil"]`) - .first() - .simulate('click'); - expect( - wrapper - .find( - `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]` - ) - .exists() - ).toEqual(true); - wrapper - .find( - `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-cancel-markdown"]` - ) - .first() - .simulate('click'); - expect( - wrapper - .find( - `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]` - ) - .exists() - ).toEqual(false); - }); - - it('calls update comment when comment markdown is saved', async () => { - const ourActions = [getUserAction(['comment'], 'create')]; - const props = { - ...defaultProps, - caseUserActions: ourActions, - }; - const wrapper = mount( - - - - - - ); - wrapper - .find(`[data-test-subj="comment-create-action"] [data-test-subj="property-actions-ellipses"]`) - .first() - .simulate('click'); - wrapper - .find(`[data-test-subj="comment-create-action"] [data-test-subj="property-actions-pencil"]`) - .first() - .simulate('click'); - wrapper - .find( - `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-save-markdown"]` - ) - .first() - .simulate('click'); - await act(async () => { - await wait(); - wrapper.update(); - expect( - wrapper - .find( - `[data-test-subj="user-action-${props.data.comments[0].id}"] [data-test-subj="user-action-markdown-form"]` - ) - .exists() - ).toEqual(false); - expect(patchComment).toBeCalledWith({ - commentUpdate: sampleData.content, - caseId: props.data.id, - commentId: props.data.comments[0].id, - fetchUserActions, - updateCase, - version: props.data.comments[0].version, - }); - }); - }); - - it('calls update description when description markdown is saved', async () => { - const props = defaultProps; - const wrapper = mount( - - - - - - ); - wrapper - .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-ellipses"]`) - .first() - .simulate('click'); - wrapper - .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-pencil"]`) - .first() - .simulate('click'); - wrapper - .find( - `[data-test-subj="user-action-description"] [data-test-subj="user-action-save-markdown"]` - ) - .first() - .simulate('click'); - await act(async () => { - await wait(); - expect( - wrapper - .find( - `[data-test-subj="user-action-${props.data.id}"] [data-test-subj="user-action-markdown-form"]` - ) - .exists() - ).toEqual(false); - expect(onUpdateField).toBeCalledWith('description', sampleData.content); - }); - }); - - it('quotes', async () => { - const commentData = { - comment: '', - }; - const formHookMock = getFormMock(commentData); - const setFieldValue = jest.fn(); - useFormMock.mockImplementation(() => ({ form: { ...formHookMock, setFieldValue } })); - const props = defaultProps; - const wrapper = mount( - - - - - - ); - wrapper - .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-ellipses"]`) - .first() - .simulate('click'); - wrapper - .find(`[data-test-subj="description-action"] [data-test-subj="property-actions-quote"]`) - .first() - .simulate('click'); - expect(setFieldValue).toBeCalledWith('comment', `> ${props.data.description} \n`); - }); - it('Outlines comment when url param is provided', () => { - const commentId = 'neat-comment-id'; - const ourActions = [getUserAction(['comment'], 'create')]; - const props = { - ...defaultProps, - caseUserActions: ourActions, - }; - jest.spyOn(routeData, 'useParams').mockReturnValue({ commentId }); - const wrapper = mount( - - - - - - ); - expect( - wrapper - .find(`[data-test-subj="comment-create-action"]`) - .first() - .prop('idToOutline') - ).toEqual(commentId); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/case/components/user_action_tree/index.tsx b/x-pack/plugins/siem/public/pages/case/components/user_action_tree/index.tsx deleted file mode 100644 index 80d2c20631432f..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/user_action_tree/index.tsx +++ /dev/null @@ -1,303 +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 { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; -import React, { useCallback, useMemo, useRef, useState, useEffect } from 'react'; -import { useParams } from 'react-router-dom'; -import styled from 'styled-components'; - -import * as i18n from '../case_view/translations'; - -import { Case, CaseUserActions } from '../../../../containers/case/types'; -import { useUpdateComment } from '../../../../containers/case/use_update_comment'; -import { useCurrentUser } from '../../../../lib/kibana'; -import { AddComment } from '../add_comment'; -import { getLabelTitle } from './helpers'; -import { UserActionItem } from './user_action_item'; -import { UserActionMarkdown } from './user_action_markdown'; -import { Connector } from '../../../../../../case/common/api/cases'; -import { CaseServices } from '../../../../containers/case/use_get_case_user_actions'; -import { parseString } from '../../../../containers/case/utils'; - -export interface UserActionTreeProps { - caseServices: CaseServices; - caseUserActions: CaseUserActions[]; - connectors: Connector[]; - data: Case; - fetchUserActions: () => void; - isLoadingDescription: boolean; - isLoadingUserActions: boolean; - onUpdateField: (updateKey: keyof Case, updateValue: string | string[]) => void; - updateCase: (newCase: Case) => void; - userCanCrud: boolean; -} - -const MyEuiFlexGroup = styled(EuiFlexGroup)` - margin-bottom: 8px; -`; - -const DESCRIPTION_ID = 'description'; -const NEW_ID = 'newComment'; - -export const UserActionTree = React.memo( - ({ - data: caseData, - caseServices, - caseUserActions, - connectors, - fetchUserActions, - isLoadingDescription, - isLoadingUserActions, - onUpdateField, - updateCase, - userCanCrud, - }: UserActionTreeProps) => { - const { commentId } = useParams(); - const handlerTimeoutId = useRef(0); - const [initLoading, setInitLoading] = useState(true); - const [selectedOutlineCommentId, setSelectedOutlineCommentId] = useState(''); - const { isLoadingIds, patchComment } = useUpdateComment(); - const currentUser = useCurrentUser(); - const [manageMarkdownEditIds, setManangeMardownEditIds] = useState([]); - const [insertQuote, setInsertQuote] = useState(null); - const handleManageMarkdownEditId = useCallback( - (id: string) => { - if (!manageMarkdownEditIds.includes(id)) { - setManangeMardownEditIds([...manageMarkdownEditIds, id]); - } else { - setManangeMardownEditIds(manageMarkdownEditIds.filter(myId => id !== myId)); - } - }, - [manageMarkdownEditIds] - ); - - const handleSaveComment = useCallback( - ({ id, version }: { id: string; version: string }, content: string) => { - patchComment({ - caseId: caseData.id, - commentId: id, - commentUpdate: content, - fetchUserActions, - version, - updateCase, - }); - }, - [caseData, handleManageMarkdownEditId, patchComment, updateCase] - ); - - const handleOutlineComment = useCallback( - (id: string) => { - const moveToTarget = document.getElementById(`${id}-permLink`); - if (moveToTarget != null) { - const yOffset = -60; - const y = moveToTarget.getBoundingClientRect().top + window.pageYOffset + yOffset; - window.scrollTo({ - top: y, - behavior: 'smooth', - }); - if (id === 'add-comment') { - moveToTarget.getElementsByTagName('textarea')[0].focus(); - } - } - window.clearTimeout(handlerTimeoutId.current); - setSelectedOutlineCommentId(id); - handlerTimeoutId.current = window.setTimeout(() => { - setSelectedOutlineCommentId(''); - window.clearTimeout(handlerTimeoutId.current); - }, 2400); - }, - [handlerTimeoutId.current] - ); - - const handleManageQuote = useCallback( - (quote: string) => { - const addCarrots = quote.replace(new RegExp('\r?\n', 'g'), ' \n> '); - setInsertQuote(`> ${addCarrots} \n`); - handleOutlineComment('add-comment'); - }, - [handleOutlineComment] - ); - - const handleUpdate = useCallback( - (newCase: Case) => { - updateCase(newCase); - fetchUserActions(); - }, - [fetchUserActions, updateCase] - ); - - const MarkdownDescription = useMemo( - () => ( - { - onUpdateField(DESCRIPTION_ID, content); - }} - onChangeEditable={handleManageMarkdownEditId} - /> - ), - [caseData.description, handleManageMarkdownEditId, manageMarkdownEditIds, onUpdateField] - ); - - const MarkdownNewComment = useMemo( - () => ( - - ), - [caseData.id, handleUpdate, insertQuote, userCanCrud] - ); - - useEffect(() => { - if (initLoading && !isLoadingUserActions && isLoadingIds.length === 0) { - setInitLoading(false); - if (commentId != null) { - handleOutlineComment(commentId); - } - } - }, [commentId, initLoading, isLoadingUserActions, isLoadingIds]); - return ( - <> - {i18n.ADDED_DESCRIPTION}} - fullName={caseData.createdBy.fullName ?? caseData.createdBy.username ?? ''} - markdown={MarkdownDescription} - onEdit={handleManageMarkdownEditId.bind(null, DESCRIPTION_ID)} - onQuote={handleManageQuote.bind(null, caseData.description)} - username={caseData.createdBy.username ?? i18n.UNKNOWN} - /> - - {caseUserActions.map((action, index) => { - if (action.commentId != null && action.action === 'create') { - const comment = caseData.comments.find(c => c.id === action.commentId); - if (comment != null) { - return ( - {i18n.ADDED_COMMENT}} - fullName={comment.createdBy.fullName ?? comment.createdBy.username ?? ''} - markdown={ - - } - onEdit={handleManageMarkdownEditId.bind(null, comment.id)} - onQuote={handleManageQuote.bind(null, comment.comment)} - outlineComment={handleOutlineComment} - username={comment.createdBy.username ?? ''} - updatedAt={comment.updatedAt} - /> - ); - } - } - if (action.actionField.length === 1) { - const myField = action.actionField[0]; - const parsedValue = parseString(`${action.newValue}`); - const { firstPush, parsedConnectorId, parsedConnectorName } = - parsedValue != null - ? { - firstPush: caseServices[parsedValue.connector_id].firstPushIndex === index, - parsedConnectorId: parsedValue.connector_id, - parsedConnectorName: parsedValue.connector_name, - } - : { - firstPush: false, - parsedConnectorId: 'none', - parsedConnectorName: 'none', - }; - const labelTitle: string | JSX.Element = getLabelTitle({ - action, - field: myField, - firstPush, - connectors, - }); - - return ( - {labelTitle}} - linkId={ - action.action === 'update' && action.commentId != null ? action.commentId : null - } - fullName={action.actionBy.fullName ?? action.actionBy.username ?? ''} - outlineComment={handleOutlineComment} - showTopFooter={ - action.action === 'push-to-service' && - index === caseServices[parsedConnectorId].lastPushIndex - } - showBottomFooter={ - action.action === 'push-to-service' && - index === caseServices[parsedConnectorId].lastPushIndex && - caseServices[parsedConnectorId].hasDataToPush - } - username={action.actionBy.username ?? ''} - /> - ); - } - return null; - })} - {(isLoadingUserActions || isLoadingIds.includes(NEW_ID)) && ( - - - - - - )} - - - ); - } -); - -UserActionTree.displayName = 'UserActionTree'; diff --git a/x-pack/plugins/siem/public/pages/case/components/user_list/index.test.tsx b/x-pack/plugins/siem/public/pages/case/components/user_list/index.test.tsx deleted file mode 100644 index 51acb3b810d92e..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/user_list/index.test.tsx +++ /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. - */ - -import React from 'react'; -import { shallow } from 'enzyme'; -import { UserList } from './'; -import * as i18n from '../case_view/translations'; - -describe('UserList ', () => { - const title = 'Case Title'; - const caseLink = 'http://reddit.com'; - const user = { username: 'username', fullName: 'Full Name', email: 'testemail@elastic.co' }; - const open = jest.fn(); - beforeAll(() => { - window.open = open; - }); - beforeEach(() => { - jest.resetAllMocks(); - }); - it('triggers mailto when email icon clicked', () => { - const wrapper = shallow( - - ); - wrapper.find('[data-test-subj="user-list-email-button"]').simulate('click'); - expect(open).toBeCalledWith( - `mailto:${user.email}?subject=${i18n.EMAIL_SUBJECT(title)}&body=${i18n.EMAIL_BODY(caseLink)}`, - '_blank' - ); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/case/components/user_list/index.tsx b/x-pack/plugins/siem/public/pages/case/components/user_list/index.tsx deleted file mode 100644 index 579e8e48fa147a..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/components/user_list/index.tsx +++ /dev/null @@ -1,108 +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, { useCallback } from 'react'; -import { isEmpty } from 'lodash/fp'; - -import { - EuiButtonIcon, - EuiText, - EuiHorizontalRule, - EuiAvatar, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingSpinner, - EuiToolTip, -} from '@elastic/eui'; - -import styled, { css } from 'styled-components'; - -import { ElasticUser } from '../../../../containers/case/types'; -import * as i18n from './translations'; - -interface UserListProps { - email: { - subject: string; - body: string; - }; - headline: string; - loading?: boolean; - users: ElasticUser[]; -} - -const MyAvatar = styled(EuiAvatar)` - top: -4px; -`; - -const MyFlexGroup = styled(EuiFlexGroup)` - ${({ theme }) => css` - margin-top: ${theme.eui.euiSizeM}; - `} -`; - -const renderUsers = ( - users: ElasticUser[], - handleSendEmail: (emailAddress: string | undefined | null) => void -) => - users.map(({ fullName, username, email }, key) => ( - - - - - - - - {fullName ? fullName : username ?? ''}

}> -

- - {username} - -

-
-
-
-
- - - -
- )); - -export const UserList = React.memo(({ email, headline, loading, users }: UserListProps) => { - const handleSendEmail = useCallback( - (emailAddress: string | undefined | null) => { - if (emailAddress && emailAddress != null) { - window.open(`mailto:${emailAddress}?subject=${email.subject}&body=${email.body}`, '_blank'); - } - }, - [email.subject] - ); - return users.filter(({ username }) => username != null && username !== '').length > 0 ? ( - -

{headline}

- - {loading && ( - - - - - - )} - {renderUsers( - users.filter(({ username }) => username != null && username !== ''), - handleSendEmail - )} -
- ) : null; -}); - -UserList.displayName = 'UserList'; diff --git a/x-pack/plugins/siem/public/pages/case/configure_cases.tsx b/x-pack/plugins/siem/public/pages/case/configure_cases.tsx deleted file mode 100644 index 7515efa0e1b7ae..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/configure_cases.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useMemo } from 'react'; -import { Redirect } from 'react-router-dom'; - -import { getCaseUrl } from '../../components/link_to'; -import { useGetUrlSearch } from '../../components/navigation/use_get_url_search'; -import { WrapperPage } from '../../components/wrapper_page'; -import { useGetUserSavedObjectPermissions } from '../../lib/kibana'; -import { SpyRoute } from '../../utils/route/spy_routes'; -import { navTabs } from '../home/home_navigations'; -import { CaseHeaderPage } from './components/case_header_page'; -import { ConfigureCases } from './components/configure_cases'; -import { WhitePageWrapper, SectionWrapper } from './components/wrappers'; -import * as i18n from './translations'; - -const wrapperPageStyle: Record = { - paddingLeft: '0', - paddingRight: '0', - paddingBottom: '0', -}; - -const ConfigureCasesPageComponent: React.FC = () => { - const userPermissions = useGetUserSavedObjectPermissions(); - const search = useGetUrlSearch(navTabs.case); - - const backOptions = useMemo( - () => ({ - href: getCaseUrl(search), - text: i18n.BACK_TO_ALL, - }), - [search] - ); - - if (userPermissions != null && !userPermissions.read) { - return ; - } - - return ( - <> - - - - - - - - - - - ); -}; - -export const ConfigureCasesPage = React.memo(ConfigureCasesPageComponent); diff --git a/x-pack/plugins/siem/public/pages/case/create_case.tsx b/x-pack/plugins/siem/public/pages/case/create_case.tsx deleted file mode 100644 index 06cb7fadfb8d30..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/create_case.tsx +++ /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. - */ - -import React, { useMemo } from 'react'; -import { Redirect } from 'react-router-dom'; - -import { getCaseUrl } from '../../components/link_to'; -import { useGetUrlSearch } from '../../components/navigation/use_get_url_search'; -import { WrapperPage } from '../../components/wrapper_page'; -import { useGetUserSavedObjectPermissions } from '../../lib/kibana'; -import { SpyRoute } from '../../utils/route/spy_routes'; -import { navTabs } from '../home/home_navigations'; -import { CaseHeaderPage } from './components/case_header_page'; -import { Create } from './components/create'; -import * as i18n from './translations'; - -export const CreateCasePage = React.memo(() => { - const userPermissions = useGetUserSavedObjectPermissions(); - const search = useGetUrlSearch(navTabs.case); - - const backOptions = useMemo( - () => ({ - href: getCaseUrl(search), - text: i18n.BACK_TO_ALL, - }), - [search] - ); - - if (userPermissions != null && !userPermissions.crud) { - return ; - } - - return ( - <> - - - - - - - ); -}); - -CreateCasePage.displayName = 'CreateCasePage'; diff --git a/x-pack/plugins/siem/public/pages/case/index.tsx b/x-pack/plugins/siem/public/pages/case/index.tsx deleted file mode 100644 index 124cefa726a8b1..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/index.tsx +++ /dev/null @@ -1,42 +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 { Route, Switch } from 'react-router-dom'; -import { SiemPageName } from '../home/types'; -import { CaseDetailsPage } from './case_details'; -import { CasesPage } from './case'; -import { CreateCasePage } from './create_case'; -import { ConfigureCasesPage } from './configure_cases'; - -const casesPagePath = `/:pageName(${SiemPageName.case})`; -const caseDetailsPagePath = `${casesPagePath}/:detailName`; -const caseDetailsPagePathWithCommentId = `${casesPagePath}/:detailName/:commentId`; -const createCasePagePath = `${casesPagePath}/create`; -const configureCasesPagePath = `${casesPagePath}/configure`; - -const CaseContainerComponent: React.FC = () => ( - - - - - - - - - - - - - - - - - -); - -export const Case = React.memo(CaseContainerComponent); diff --git a/x-pack/plugins/siem/public/pages/case/utils.ts b/x-pack/plugins/siem/public/pages/case/utils.ts deleted file mode 100644 index f1aea747485e41..00000000000000 --- a/x-pack/plugins/siem/public/pages/case/utils.ts +++ /dev/null @@ -1,42 +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 { isEmpty } from 'lodash/fp'; - -import { ChromeBreadcrumb } from 'src/core/public'; - -import { getCaseDetailsUrl, getCaseUrl, getCreateCaseUrl } from '../../components/link_to'; -import { RouteSpyState } from '../../utils/route/types'; -import * as i18n from './translations'; - -export const getBreadcrumbs = (params: RouteSpyState, search: string[]): ChromeBreadcrumb[] => { - const queryParameters = !isEmpty(search[0]) ? search[0] : null; - - let breadcrumb = [ - { - text: i18n.PAGE_TITLE, - href: getCaseUrl(queryParameters), - }, - ]; - if (params.detailName === 'create') { - breadcrumb = [ - ...breadcrumb, - { - text: i18n.CREATE_BC_TITLE, - href: getCreateCaseUrl(queryParameters), - }, - ]; - } else if (params.detailName != null) { - breadcrumb = [ - ...breadcrumb, - { - text: params.state?.caseTitle ?? '', - href: getCaseDetailsUrl({ id: params.detailName, search: queryParameters }), - }, - ]; - } - return breadcrumb; -}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/activity_monitor/columns.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/activity_monitor/columns.tsx deleted file mode 100644 index 78315d3ba79d4d..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/components/activity_monitor/columns.tsx +++ /dev/null @@ -1,97 +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. - */ - -/* eslint-disable react/display-name */ - -import { - EuiIconTip, - EuiLink, - EuiTextColor, - EuiBasicTableColumn, - EuiTableActionsColumnType, -} from '@elastic/eui'; -import React from 'react'; -import { getEmptyTagValue } from '../../../../components/empty_value'; -import { ColumnTypes } from './types'; - -const actions: EuiTableActionsColumnType['actions'] = [ - { - available: (item: ColumnTypes) => item.status === 'Running', - description: 'Stop', - icon: 'stop', - isPrimary: true, - name: 'Stop', - onClick: () => {}, - type: 'icon', - }, - { - available: (item: ColumnTypes) => item.status === 'Stopped', - description: 'Resume', - icon: 'play', - isPrimary: true, - name: 'Resume', - onClick: () => {}, - type: 'icon', - }, -]; - -// Michael: Are we able to do custom, in-table-header filters, as shown in my wireframes? -export const columns: Array> = [ - { - field: 'rule' as const, - name: 'Rule', - render: (value: ColumnTypes['rule'], _: ColumnTypes) => ( - {value.name} - ), - sortable: true, - truncateText: true, - }, - { - field: 'ran' as const, - name: 'Ran', - render: (value: ColumnTypes['ran'], _: ColumnTypes) => '--', - sortable: true, - truncateText: true, - }, - { - field: 'lookedBackTo' as const, - name: 'Looked back to', - render: (value: ColumnTypes['lookedBackTo'], _: ColumnTypes) => '--', - sortable: true, - truncateText: true, - }, - { - field: 'status' as const, - name: 'Status', - sortable: true, - truncateText: true, - }, - { - field: 'response' as const, - name: 'Response', - render: (value: ColumnTypes['response'], _: ColumnTypes) => { - return value === undefined ? ( - getEmptyTagValue() - ) : ( - <> - {value === 'Fail' ? ( - - {value} - - ) : ( - {value} - )} - - ); - }, - sortable: true, - truncateText: true, - }, - { - actions, - width: '40px', - }, -]; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.tsx deleted file mode 100644 index 31420ad07cd509..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/components/activity_monitor/index.tsx +++ /dev/null @@ -1,320 +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 { EuiBasicTable, EuiPanel, EuiSpacer } from '@elastic/eui'; -import React, { useState, useCallback } from 'react'; -import { HeaderSection } from '../../../../components/header_section'; -import { - UtilityBar, - UtilityBarAction, - UtilityBarGroup, - UtilityBarSection, - UtilityBarText, -} from '../../../../components/utility_bar'; -import { columns } from './columns'; -import { ColumnTypes, PageTypes, SortTypes } from './types'; - -export const ActivityMonitor = React.memo(() => { - const sampleTableData: ColumnTypes[] = [ - { - id: 1, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Running', - }, - { - id: 2, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Stopped', - }, - { - id: 3, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Fail', - }, - { - id: 4, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 5, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 6, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 7, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 8, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 9, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 10, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 11, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 12, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 13, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 14, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 15, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 16, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 17, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 18, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 19, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 20, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - { - id: 21, - rule: { - href: '#/detections/rules/rule-details', - name: 'Automated exfiltration', - }, - ran: '2019-12-28 00:00:00.000-05:00', - lookedBackTo: '2019-12-28 00:00:00.000-05:00', - status: 'Completed', - response: 'Success', - }, - ]; - - const [itemsTotalState] = useState(sampleTableData.length); - const [pageState, setPageState] = useState({ index: 0, size: 20 }); - // const [selectedState, setSelectedState] = useState([]); - const [sortState, setSortState] = useState({ field: 'ran', direction: 'desc' }); - - const handleChange = useCallback( - ({ page, sort }: { page?: PageTypes; sort?: SortTypes }) => { - setPageState(page!); - setSortState(sort!); - }, - [setPageState, setSortState] - ); - - return ( - <> - - - - - - - - {'Showing: 39 activites'} - - - - {'Selected: 2 activities'} - - {'Stop selected'} - - - - {'Clear 7 filters'} - - - - { - // @ts-ignore `Columns` interface differs from EUI's `column` type and is used all over this plugin, so ignore the differences instead of refactoring a lot of code - } - item.status !== 'Completed', - selectableMessage: (selectable: boolean) => - selectable ? '' : 'Completed runs cannot be acted upon', - onSelectionChange: (selectedItems: ColumnTypes[]) => { - // setSelectedState(selectedItems); - }, - }} - sorting={{ - sort: sortState, - }} - /> - - - ); -}); -ActivityMonitor.displayName = 'ActivityMonitor'; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/index.tsx deleted file mode 100644 index c9450739190139..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/components/detection_engine_header_page/index.tsx +++ /dev/null @@ -1,24 +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 { HeaderPage, HeaderPageProps } from '../../../../components/header_page'; -import * as i18n from './translations'; - -const DetectionEngineHeaderPageComponent: React.FC = props => ( - -); - -DetectionEngineHeaderPageComponent.defaultProps = { - badgeOptions: { - beta: true, - text: i18n.PAGE_BADGE_LABEL, - tooltip: i18n.PAGE_BADGE_TOOLTIP, - }, -}; - -export const DetectionEngineHeaderPage = React.memo(DetectionEngineHeaderPageComponent); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/actions.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/actions.test.tsx deleted file mode 100644 index ab75fcb6d6d1fe..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/actions.test.tsx +++ /dev/null @@ -1,384 +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 sinon from 'sinon'; -import moment from 'moment'; - -import { sendSignalToTimelineAction, determineToAndFrom } from './actions'; -import { - mockEcsDataWithSignal, - defaultTimelineProps, - apolloClient, - mockTimelineApolloResult, -} from '../../../../mock/'; -import { CreateTimeline, UpdateTimelineLoading } from './types'; -import { Ecs } from '../../../../graphql/types'; -import { TimelineType } from '../../../../../common/types/timeline'; - -jest.mock('apollo-client'); - -describe('signals actions', () => { - const anchor = '2020-03-01T17:59:46.349Z'; - const unix = moment(anchor).valueOf(); - let createTimeline: CreateTimeline; - let updateTimelineIsLoading: UpdateTimelineLoading; - let clock: sinon.SinonFakeTimers; - - beforeEach(() => { - // jest carries state between mocked implementations when using - // spyOn. So now we're doing all three of these. - // https://github.com/facebook/jest/issues/7136#issuecomment-565976599 - jest.resetAllMocks(); - jest.restoreAllMocks(); - jest.clearAllMocks(); - - createTimeline = jest.fn() as jest.Mocked; - updateTimelineIsLoading = jest.fn() as jest.Mocked; - - jest.spyOn(apolloClient, 'query').mockResolvedValue(mockTimelineApolloResult); - - clock = sinon.useFakeTimers(unix); - }); - - afterEach(() => { - clock.restore(); - }); - - describe('sendSignalToTimelineAction', () => { - describe('timeline id is NOT empty string and apollo client exists', () => { - test('it invokes updateTimelineIsLoading to set to true', async () => { - await sendSignalToTimelineAction({ - apolloClient, - createTimeline, - ecsData: mockEcsDataWithSignal, - updateTimelineIsLoading, - }); - - expect(updateTimelineIsLoading).toHaveBeenCalledTimes(1); - expect(updateTimelineIsLoading).toHaveBeenCalledWith({ id: 'timeline-1', isLoading: true }); - }); - - test('it invokes createTimeline with designated timeline template if "timelineTemplate" exists', async () => { - await sendSignalToTimelineAction({ - apolloClient, - createTimeline, - ecsData: mockEcsDataWithSignal, - updateTimelineIsLoading, - }); - const expected = { - from: 1541444305937, - timeline: { - columns: [ - { - aggregatable: undefined, - category: undefined, - columnHeaderType: 'not-filtered', - description: undefined, - example: undefined, - id: '@timestamp', - placeholder: undefined, - type: undefined, - width: 190, - }, - { - aggregatable: undefined, - category: undefined, - columnHeaderType: 'not-filtered', - description: undefined, - example: undefined, - id: 'message', - placeholder: undefined, - type: undefined, - width: 180, - }, - { - aggregatable: undefined, - category: undefined, - columnHeaderType: 'not-filtered', - description: undefined, - example: undefined, - id: 'event.category', - placeholder: undefined, - type: undefined, - width: 180, - }, - { - aggregatable: undefined, - category: undefined, - columnHeaderType: 'not-filtered', - description: undefined, - example: undefined, - id: 'host.name', - placeholder: undefined, - type: undefined, - width: 180, - }, - { - aggregatable: undefined, - category: undefined, - columnHeaderType: 'not-filtered', - description: undefined, - example: undefined, - id: 'source.ip', - placeholder: undefined, - type: undefined, - width: 180, - }, - { - aggregatable: undefined, - category: undefined, - columnHeaderType: 'not-filtered', - description: undefined, - example: undefined, - id: 'destination.ip', - placeholder: undefined, - type: undefined, - width: 180, - }, - { - aggregatable: undefined, - category: undefined, - columnHeaderType: 'not-filtered', - description: undefined, - example: undefined, - id: 'user.name', - placeholder: undefined, - type: undefined, - width: 180, - }, - ], - dataProviders: [], - dateRange: { - end: 1541444605937, - start: 1541444305937, - }, - deletedEventIds: [], - description: 'This is a sample rule description', - eventIdToNoteIds: {}, - eventType: 'all', - filters: [ - { - $state: { - store: 'appState', - }, - meta: { - key: 'host.name', - negate: false, - params: { - query: 'apache', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'host.name': 'apache', - }, - }, - }, - ], - highlightedDropAndProviderId: '', - historyIds: [], - id: '', - isFavorite: false, - isLive: false, - isLoading: false, - isSaving: false, - isSelectAllChecked: false, - itemsPerPage: 25, - itemsPerPageOptions: [10, 25, 50, 100], - kqlMode: 'filter', - kqlQuery: { - filterQuery: { - kuery: { - expression: '', - kind: 'kuery', - }, - serializedQuery: '', - }, - filterQueryDraft: { - expression: '', - kind: 'kuery', - }, - }, - loadingEventIds: [], - noteIds: [], - pinnedEventIds: {}, - pinnedEventsSaveObject: {}, - savedObjectId: null, - selectedEventIds: {}, - show: true, - showCheckboxes: false, - showRowRenderers: true, - sort: { - columnId: '@timestamp', - sortDirection: 'desc', - }, - title: '', - timelineType: TimelineType.default, - templateTimelineId: null, - templateTimelineVersion: null, - version: null, - width: 1100, - }, - to: 1541444605937, - ruleNote: '# this is some markdown documentation', - }; - - expect(createTimeline).toHaveBeenCalledWith(expected); - }); - - test('it invokes createTimeline with kqlQuery.filterQuery.kuery.kind as "kuery" if not specified in returned timeline template', async () => { - const mockTimelineApolloResultModified = { - ...mockTimelineApolloResult, - kqlQuery: { - filterQuery: { - kuery: { - expression: [''], - }, - }, - filterQueryDraft: { - expression: [''], - }, - }, - }; - jest.spyOn(apolloClient, 'query').mockResolvedValue(mockTimelineApolloResultModified); - - await sendSignalToTimelineAction({ - apolloClient, - createTimeline, - ecsData: mockEcsDataWithSignal, - updateTimelineIsLoading, - }); - // @ts-ignore - const createTimelineArg = createTimeline.mock.calls[0][0]; - - expect(createTimeline).toHaveBeenCalledTimes(1); - expect(createTimelineArg.timeline.kqlQuery.filterQuery.kuery.kind).toEqual('kuery'); - }); - - test('it invokes createTimeline with kqlQuery.filterQueryDraft.kuery.kind as "kuery" if not specified in returned timeline template', async () => { - const mockTimelineApolloResultModified = { - ...mockTimelineApolloResult, - kqlQuery: { - filterQuery: { - kuery: { - expression: [''], - }, - }, - filterQueryDraft: { - expression: [''], - }, - }, - }; - jest.spyOn(apolloClient, 'query').mockResolvedValue(mockTimelineApolloResultModified); - - await sendSignalToTimelineAction({ - apolloClient, - createTimeline, - ecsData: mockEcsDataWithSignal, - updateTimelineIsLoading, - }); - // @ts-ignore - const createTimelineArg = createTimeline.mock.calls[0][0]; - - expect(createTimeline).toHaveBeenCalledTimes(1); - expect(createTimelineArg.timeline.kqlQuery.filterQueryDraft.kind).toEqual('kuery'); - }); - - test('it invokes createTimeline with default timeline if apolloClient throws', async () => { - jest.spyOn(apolloClient, 'query').mockImplementation(() => { - throw new Error('Test error'); - }); - - await sendSignalToTimelineAction({ - apolloClient, - createTimeline, - ecsData: mockEcsDataWithSignal, - updateTimelineIsLoading, - }); - - expect(updateTimelineIsLoading).toHaveBeenCalledWith({ id: 'timeline-1', isLoading: true }); - expect(updateTimelineIsLoading).toHaveBeenCalledWith({ - id: 'timeline-1', - isLoading: false, - }); - expect(createTimeline).toHaveBeenCalledTimes(1); - expect(createTimeline).toHaveBeenCalledWith(defaultTimelineProps); - }); - }); - - describe('timelineId is empty string', () => { - test('it invokes createTimeline with timelineDefaults', async () => { - const ecsDataMock: Ecs = { - ...mockEcsDataWithSignal, - signal: { - rule: { - ...mockEcsDataWithSignal.signal?.rule!, - timeline_id: null, - }, - }, - }; - - await sendSignalToTimelineAction({ - apolloClient, - createTimeline, - ecsData: ecsDataMock, - updateTimelineIsLoading, - }); - - expect(updateTimelineIsLoading).not.toHaveBeenCalled(); - expect(createTimeline).toHaveBeenCalledTimes(1); - expect(createTimeline).toHaveBeenCalledWith(defaultTimelineProps); - }); - }); - - describe('apolloClient is not defined', () => { - test('it invokes createTimeline with timelineDefaults', async () => { - const ecsDataMock: Ecs = { - ...mockEcsDataWithSignal, - signal: { - rule: { - ...mockEcsDataWithSignal.signal?.rule!, - timeline_id: [''], - }, - }, - }; - - await sendSignalToTimelineAction({ - createTimeline, - ecsData: ecsDataMock, - updateTimelineIsLoading, - }); - - expect(updateTimelineIsLoading).not.toHaveBeenCalled(); - expect(createTimeline).toHaveBeenCalledTimes(1); - expect(createTimeline).toHaveBeenCalledWith(defaultTimelineProps); - }); - }); - }); - - describe('determineToAndFrom', () => { - test('it uses ecs.Data.timestamp if one is provided', () => { - const ecsDataMock: Ecs = { - ...mockEcsDataWithSignal, - timestamp: '2020-03-20T17:59:46.349Z', - }; - const result = determineToAndFrom({ ecsData: ecsDataMock }); - - expect(result.from).toEqual(1584726886349); - expect(result.to).toEqual(1584727186349); - }); - - test('it uses current time timestamp if ecsData.timestamp is not provided', () => { - const { timestamp, ...ecsDataMock } = { - ...mockEcsDataWithSignal, - }; - const result = determineToAndFrom({ ecsData: ecsDataMock }); - - expect(result.from).toEqual(1583085286349); - expect(result.to).toEqual(1583085586349); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx deleted file mode 100644 index c71ede32d8403e..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/actions.tsx +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import dateMath from '@elastic/datemath'; -import { getOr, isEmpty } from 'lodash/fp'; -import moment from 'moment'; - -import { updateSignalStatus } from '../../../../containers/detection_engine/signals/api'; -import { SendSignalToTimelineActionProps, UpdateSignalStatusActionProps } from './types'; -import { TimelineNonEcsData, GetOneTimeline, TimelineResult, Ecs } from '../../../../graphql/types'; -import { oneTimelineQuery } from '../../../../containers/timeline/one/index.gql_query'; -import { - omitTypenameInTimeline, - formatTimelineResultToModel, -} from '../../../../components/open_timeline/helpers'; -import { convertKueryToElasticSearchQuery } from '../../../../lib/keury'; -import { timelineDefaults } from '../../../../store/timeline/defaults'; -import { - replaceTemplateFieldFromQuery, - replaceTemplateFieldFromMatchFilters, - replaceTemplateFieldFromDataProviders, -} from './helpers'; - -export const getUpdateSignalsQuery = (eventIds: Readonly) => { - return { - query: { - bool: { - filter: { - terms: { - _id: [...eventIds], - }, - }, - }, - }, - }; -}; - -export const getFilterAndRuleBounds = ( - data: TimelineNonEcsData[][] -): [string[], number, number] => { - const stringFilter = data?.[0].filter(d => d.field === 'signal.rule.filters')?.[0]?.value ?? []; - - const eventTimes = data - .flatMap(signal => signal.filter(d => d.field === 'signal.original_time')?.[0]?.value ?? []) - .map(d => moment(d)); - - return [stringFilter, moment.min(eventTimes).valueOf(), moment.max(eventTimes).valueOf()]; -}; - -export const updateSignalStatusAction = async ({ - query, - signalIds, - status, - setEventsLoading, - setEventsDeleted, -}: UpdateSignalStatusActionProps) => { - try { - setEventsLoading({ eventIds: signalIds, isLoading: true }); - - const queryObject = query ? { query: JSON.parse(query) } : getUpdateSignalsQuery(signalIds); - - await updateSignalStatus({ query: queryObject, status }); - // TODO: Only delete those that were successfully updated from updatedRules - setEventsDeleted({ eventIds: signalIds, isDeleted: true }); - } catch (e) { - // TODO: Show error toasts - } finally { - setEventsLoading({ eventIds: signalIds, isLoading: false }); - } -}; - -export const determineToAndFrom = ({ ecsData }: { ecsData: Ecs }) => { - const ellapsedTimeRule = moment.duration( - moment().diff( - dateMath.parse(ecsData.signal?.rule?.from != null ? ecsData.signal?.rule?.from[0] : 'now-0s') - ) - ); - - const from = moment(ecsData.timestamp ?? new Date()) - .subtract(ellapsedTimeRule) - .valueOf(); - const to = moment(ecsData.timestamp ?? new Date()).valueOf(); - - return { to, from }; -}; - -export const sendSignalToTimelineAction = async ({ - apolloClient, - createTimeline, - ecsData, - updateTimelineIsLoading, -}: SendSignalToTimelineActionProps) => { - let openSignalInBasicTimeline = true; - const noteContent = ecsData.signal?.rule?.note != null ? ecsData.signal?.rule?.note[0] : ''; - const timelineId = - ecsData.signal?.rule?.timeline_id != null ? ecsData.signal?.rule?.timeline_id[0] : ''; - const { to, from } = determineToAndFrom({ ecsData }); - - if (timelineId !== '' && apolloClient != null) { - try { - updateTimelineIsLoading({ id: 'timeline-1', isLoading: true }); - const responseTimeline = await apolloClient.query< - GetOneTimeline.Query, - GetOneTimeline.Variables - >({ - query: oneTimelineQuery, - fetchPolicy: 'no-cache', - variables: { - id: timelineId, - }, - }); - const resultingTimeline: TimelineResult = getOr({}, 'data.getOneTimeline', responseTimeline); - - if (!isEmpty(resultingTimeline)) { - const timelineTemplate: TimelineResult = omitTypenameInTimeline(resultingTimeline); - openSignalInBasicTimeline = false; - const { timeline } = formatTimelineResultToModel(timelineTemplate, true); - const query = replaceTemplateFieldFromQuery( - timeline.kqlQuery?.filterQuery?.kuery?.expression ?? '', - ecsData - ); - const filters = replaceTemplateFieldFromMatchFilters(timeline.filters ?? [], ecsData); - const dataProviders = replaceTemplateFieldFromDataProviders( - timeline.dataProviders ?? [], - ecsData - ); - createTimeline({ - from, - timeline: { - ...timeline, - dataProviders, - eventType: 'all', - filters, - dateRange: { - start: from, - end: to, - }, - kqlQuery: { - filterQuery: { - kuery: { - kind: timeline.kqlQuery?.filterQuery?.kuery?.kind ?? 'kuery', - expression: query, - }, - serializedQuery: convertKueryToElasticSearchQuery(query), - }, - filterQueryDraft: { - kind: timeline.kqlQuery?.filterQuery?.kuery?.kind ?? 'kuery', - expression: query, - }, - }, - show: true, - }, - to, - ruleNote: noteContent, - }); - } - } catch { - openSignalInBasicTimeline = true; - updateTimelineIsLoading({ id: 'timeline-1', isLoading: false }); - } - } - - if (openSignalInBasicTimeline) { - createTimeline({ - from, - timeline: { - ...timelineDefaults, - dataProviders: [ - { - and: [], - id: `send-signal-to-timeline-action-default-draggable-event-details-value-formatted-field-value-timeline-1-signal-id-${ecsData._id}`, - name: ecsData._id, - enabled: true, - excluded: false, - kqlQuery: '', - queryMatch: { - field: '_id', - value: ecsData._id, - operator: ':', - }, - }, - ], - id: 'timeline-1', - dateRange: { - start: from, - end: to, - }, - eventType: 'all', - kqlQuery: { - filterQuery: { - kuery: { - kind: 'kuery', - expression: '', - }, - serializedQuery: '', - }, - filterQueryDraft: { - kind: 'kuery', - expression: '', - }, - }, - }, - to, - ruleNote: noteContent, - }); - } -}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/helpers.test.ts b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/helpers.test.ts deleted file mode 100644 index a948d2b940b0c9..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/helpers.test.ts +++ /dev/null @@ -1,273 +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 { - getStringArray, - replaceTemplateFieldFromQuery, - replaceTemplateFieldFromMatchFilters, - reformatDataProviderWithNewValue, -} from './helpers'; -import { mockEcsData } from '../../../../mock/mock_ecs'; -import { Filter } from '../../../../../../../../src/plugins/data/public'; -import { DataProvider } from '../../../../components/timeline/data_providers/data_provider'; -import { mockDataProviders } from '../../../../components/timeline/data_providers/mock/mock_data_providers'; -import { cloneDeep } from 'lodash/fp'; - -describe('helpers', () => { - let mockEcsDataClone = cloneDeep(mockEcsData); - beforeEach(() => { - mockEcsDataClone = cloneDeep(mockEcsData); - }); - describe('getStringOrStringArray', () => { - test('it should correctly return a string array', () => { - const value = getStringArray('x', { - x: 'The nickname of the developer we all :heart:', - }); - expect(value).toEqual(['The nickname of the developer we all :heart:']); - }); - - test('it should correctly return a string array with a single element', () => { - const value = getStringArray('x', { - x: ['The nickname of the developer we all :heart:'], - }); - expect(value).toEqual(['The nickname of the developer we all :heart:']); - }); - - test('it should correctly return a string array with two elements of strings', () => { - const value = getStringArray('x', { - x: ['The nickname of the developer we all :heart:', 'We are all made of stars'], - }); - expect(value).toEqual([ - 'The nickname of the developer we all :heart:', - 'We are all made of stars', - ]); - }); - - test('it should correctly return a string array with deep elements', () => { - const value = getStringArray('x.y.z', { - x: { y: { z: 'zed' } }, - }); - expect(value).toEqual(['zed']); - }); - - test('it should correctly return a string array with a non-existent value', () => { - const value = getStringArray('non.existent', { - x: { y: { z: 'zed' } }, - }); - expect(value).toEqual([]); - }); - - test('it should trace an error if the value is not a string', () => { - const mockConsole: Console = ({ trace: jest.fn() } as unknown) as Console; - const value = getStringArray('a', { a: 5 }, mockConsole); - expect(value).toEqual([]); - expect( - mockConsole.trace - ).toHaveBeenCalledWith( - 'Data type that is not a string or string array detected:', - 5, - 'when trying to access field:', - 'a', - 'from data object of:', - { a: 5 } - ); - }); - - test('it should trace an error if the value is an array of mixed values', () => { - const mockConsole: Console = ({ trace: jest.fn() } as unknown) as Console; - const value = getStringArray('a', { a: ['hi', 5] }, mockConsole); - expect(value).toEqual([]); - expect( - mockConsole.trace - ).toHaveBeenCalledWith( - 'Data type that is not a string or string array detected:', - ['hi', 5], - 'when trying to access field:', - 'a', - 'from data object of:', - { a: ['hi', 5] } - ); - }); - }); - - describe('replaceTemplateFieldFromQuery', () => { - test('given an empty query string this returns an empty query string', () => { - const replacement = replaceTemplateFieldFromQuery('', mockEcsDataClone[0]); - expect(replacement).toEqual(''); - }); - - test('given a query string with spaces this returns an empty query string', () => { - const replacement = replaceTemplateFieldFromQuery(' ', mockEcsDataClone[0]); - expect(replacement).toEqual(''); - }); - - test('it should replace a query with a template value such as apache from a mock template', () => { - const replacement = replaceTemplateFieldFromQuery( - 'host.name: placeholdertext', - mockEcsDataClone[0] - ); - expect(replacement).toEqual('host.name: apache'); - }); - - test('it should replace a template field with an ECS value that is not an array', () => { - mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case - const replacement = replaceTemplateFieldFromQuery('host.name: *', mockEcsDataClone[0]); - expect(replacement).toEqual('host.name: *'); - }); - - test('it should NOT replace a query with a template value that is not part of the template fields array', () => { - const replacement = replaceTemplateFieldFromQuery( - 'user.id: placeholdertext', - mockEcsDataClone[0] - ); - expect(replacement).toEqual('user.id: placeholdertext'); - }); - }); - - describe('replaceTemplateFieldFromMatchFilters', () => { - test('given an empty query filter this will return an empty filter', () => { - const replacement = replaceTemplateFieldFromMatchFilters([], mockEcsDataClone[0]); - expect(replacement).toEqual([]); - }); - - test('given a query filter this will return that filter with the placeholder replaced', () => { - const filters: Filter[] = [ - { - meta: { - type: 'phrase', - key: 'host.name', - alias: 'alias', - disabled: false, - negate: false, - params: { query: 'Braden' }, - }, - query: { match_phrase: { 'host.name': 'Braden' } }, - }, - ]; - const replacement = replaceTemplateFieldFromMatchFilters(filters, mockEcsDataClone[0]); - const expected: Filter[] = [ - { - meta: { - type: 'phrase', - key: 'host.name', - alias: 'alias', - disabled: false, - negate: false, - params: { query: 'apache' }, - }, - query: { match_phrase: { 'host.name': 'apache' } }, - }, - ]; - expect(replacement).toEqual(expected); - }); - - test('given a query filter with a value not in the templateFields, this will NOT replace the placeholder value', () => { - const filters: Filter[] = [ - { - meta: { - type: 'phrase', - key: 'user.id', - alias: 'alias', - disabled: false, - negate: false, - params: { query: 'Evan' }, - }, - query: { match_phrase: { 'user.id': 'Evan' } }, - }, - ]; - const replacement = replaceTemplateFieldFromMatchFilters(filters, mockEcsDataClone[0]); - const expected: Filter[] = [ - { - meta: { - type: 'phrase', - key: 'user.id', - alias: 'alias', - disabled: false, - negate: false, - params: { query: 'Evan' }, - }, - query: { match_phrase: { 'user.id': 'Evan' } }, - }, - ]; - expect(replacement).toEqual(expected); - }); - }); - - describe('reformatDataProviderWithNewValue', () => { - test('it should replace a query with a template value such as apache from a mock data provider', () => { - const mockDataProvider: DataProvider = mockDataProviders[0]; - mockDataProvider.queryMatch.field = 'host.name'; - mockDataProvider.id = 'Braden'; - mockDataProvider.name = 'Braden'; - mockDataProvider.queryMatch.value = 'Braden'; - const replacement = reformatDataProviderWithNewValue(mockDataProvider, mockEcsDataClone[0]); - expect(replacement).toEqual({ - id: 'apache', - name: 'apache', - enabled: true, - excluded: false, - kqlQuery: '', - queryMatch: { - field: 'host.name', - value: 'apache', - operator: ':', - displayField: undefined, - displayValue: undefined, - }, - and: [], - }); - }); - - test('it should replace a query with a template value such as apache from a mock data provider using a string in the data provider', () => { - mockEcsDataClone[0].host!.name = ('apache' as unknown) as string[]; // very unsafe cast for this test case - const mockDataProvider: DataProvider = mockDataProviders[0]; - mockDataProvider.queryMatch.field = 'host.name'; - mockDataProvider.id = 'Braden'; - mockDataProvider.name = 'Braden'; - mockDataProvider.queryMatch.value = 'Braden'; - const replacement = reformatDataProviderWithNewValue(mockDataProvider, mockEcsDataClone[0]); - expect(replacement).toEqual({ - id: 'apache', - name: 'apache', - enabled: true, - excluded: false, - kqlQuery: '', - queryMatch: { - field: 'host.name', - value: 'apache', - operator: ':', - displayField: undefined, - displayValue: undefined, - }, - and: [], - }); - }); - - test('it should NOT replace a query with a template value that is not part of a template such as user.id', () => { - const mockDataProvider: DataProvider = mockDataProviders[0]; - mockDataProvider.queryMatch.field = 'user.id'; - mockDataProvider.id = 'my-id'; - mockDataProvider.name = 'Rebecca'; - mockDataProvider.queryMatch.value = 'Rebecca'; - const replacement = reformatDataProviderWithNewValue(mockDataProvider, mockEcsDataClone[0]); - expect(replacement).toEqual({ - id: 'my-id', - name: 'Rebecca', - enabled: true, - excluded: false, - kqlQuery: '', - queryMatch: { - field: 'user.id', - value: 'Rebecca', - operator: ':', - displayField: undefined, - displayValue: undefined, - }, - and: [], - }); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/helpers.ts b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/helpers.ts deleted file mode 100644 index 3fa2da37046b00..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/helpers.ts +++ /dev/null @@ -1,165 +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 { get, isEmpty } from 'lodash/fp'; -import { Filter, esKuery, KueryNode } from '../../../../../../../../src/plugins/data/public'; -import { - DataProvider, - DataProvidersAnd, -} from '../../../../components/timeline/data_providers/data_provider'; -import { Ecs } from '../../../../graphql/types'; - -interface FindValueToChangeInQuery { - field: string; - valueToChange: string; -} - -/** - * Fields that will be replaced with the template strings from a a saved timeline template. - * This is used for the signals detection engine feature when you save a timeline template - * and are the fields you can replace when creating a template. - */ -const templateFields = [ - 'host.name', - 'host.hostname', - 'host.domain', - 'host.id', - 'host.ip', - 'client.ip', - 'destination.ip', - 'server.ip', - 'source.ip', - 'network.community_id', - 'user.name', - 'process.name', -]; - -/** - * This will return an unknown as a string array if it exists from an unknown data type and a string - * that represents the path within the data object the same as lodash's "get". If the value is non-existent - * we will return an empty array. If it is a non string value then this will log a trace to the console - * that it encountered an error and return an empty array. - * @param field string of the field to access - * @param data The unknown data that is typically a ECS value to get the value - * @param localConsole The local console which can be sent in to make this pure (for tests) or use the default console - */ -export const getStringArray = (field: string, data: unknown, localConsole = console): string[] => { - const value: unknown | undefined = get(field, data); - if (value == null) { - return []; - } else if (typeof value === 'string') { - return [value]; - } else if (Array.isArray(value) && value.every(element => typeof element === 'string')) { - return value; - } else { - localConsole.trace( - 'Data type that is not a string or string array detected:', - value, - 'when trying to access field:', - field, - 'from data object of:', - data - ); - return []; - } -}; - -export const findValueToChangeInQuery = ( - kueryNode: KueryNode, - valueToChange: FindValueToChangeInQuery[] = [] -): FindValueToChangeInQuery[] => { - let localValueToChange = valueToChange; - if (kueryNode.function === 'is' && templateFields.includes(kueryNode.arguments[0].value)) { - localValueToChange = [ - ...localValueToChange, - { - field: kueryNode.arguments[0].value, - valueToChange: kueryNode.arguments[1].value, - }, - ]; - } - return kueryNode.arguments.reduce( - (addValueToChange: FindValueToChangeInQuery[], ast: KueryNode) => { - if (ast.function === 'is' && templateFields.includes(ast.arguments[0].value)) { - return [ - ...addValueToChange, - { - field: ast.arguments[0].value, - valueToChange: ast.arguments[1].value, - }, - ]; - } - if (ast.arguments) { - return findValueToChangeInQuery(ast, addValueToChange); - } - return addValueToChange; - }, - localValueToChange - ); -}; - -export const replaceTemplateFieldFromQuery = (query: string, ecsData: Ecs): string => { - if (query.trim() !== '') { - const valueToChange = findValueToChangeInQuery(esKuery.fromKueryExpression(query)); - return valueToChange.reduce((newQuery, vtc) => { - const newValue = getStringArray(vtc.field, ecsData); - if (newValue.length) { - return newQuery.replace(vtc.valueToChange, newValue[0]); - } else { - return newQuery; - } - }, query); - } else { - return ''; - } -}; - -export const replaceTemplateFieldFromMatchFilters = (filters: Filter[], ecsData: Ecs): Filter[] => - filters.map(filter => { - if ( - filter.meta.type === 'phrase' && - filter.meta.key != null && - templateFields.includes(filter.meta.key) - ) { - const newValue = getStringArray(filter.meta.key, ecsData); - if (newValue.length) { - filter.meta.params = { query: newValue[0] }; - filter.query = { match_phrase: { [filter.meta.key]: newValue[0] } }; - } - } - return filter; - }); - -export const reformatDataProviderWithNewValue = ( - dataProvider: T, - ecsData: Ecs -): T => { - if (templateFields.includes(dataProvider.queryMatch.field)) { - const newValue = getStringArray(dataProvider.queryMatch.field, ecsData); - if (newValue.length) { - dataProvider.id = dataProvider.id.replace(dataProvider.name, newValue[0]); - dataProvider.name = newValue[0]; - dataProvider.queryMatch.value = newValue[0]; - dataProvider.queryMatch.displayField = undefined; - dataProvider.queryMatch.displayValue = undefined; - } - } - return dataProvider; -}; - -export const replaceTemplateFieldFromDataProviders = ( - dataProviders: DataProvider[], - ecsData: Ecs -): DataProvider[] => - dataProviders.map(dataProvider => { - const newDataProvider = reformatDataProviderWithNewValue(dataProvider, ecsData); - if (newDataProvider.and != null && !isEmpty(newDataProvider.and)) { - newDataProvider.and = newDataProvider.and.map(andDataProvider => - reformatDataProviderWithNewValue(andDataProvider, ecsData) - ); - } - return newDataProvider; - }); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/index.tsx deleted file mode 100644 index 5442c8c19b5a70..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/index.tsx +++ /dev/null @@ -1,369 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiPanel, EuiLoadingContent } from '@elastic/eui'; -import { isEmpty } from 'lodash/fp'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import { Dispatch } from 'redux'; - -import { Filter, esQuery } from '../../../../../../../../src/plugins/data/public'; -import { useFetchIndexPatterns } from '../../../../containers/detection_engine/rules/fetch_index_patterns'; -import { StatefulEventsViewer } from '../../../../components/events_viewer'; -import { HeaderSection } from '../../../../components/header_section'; -import { combineQueries } from '../../../../components/timeline/helpers'; -import { useKibana } from '../../../../lib/kibana'; -import { inputsSelectors, State, inputsModel } from '../../../../store'; -import { timelineActions, timelineSelectors } from '../../../../store/timeline'; -import { TimelineModel } from '../../../../store/timeline/model'; -import { timelineDefaults } from '../../../../store/timeline/defaults'; -import { useApolloClient } from '../../../../utils/apollo_context'; - -import { updateSignalStatusAction } from './actions'; -import { - getSignalsActions, - requiredFieldsForActions, - signalsClosedFilters, - signalsDefaultModel, - signalsOpenFilters, -} from './default_config'; -import { - FILTER_CLOSED, - FILTER_OPEN, - SignalFilterOption, - SignalsTableFilterGroup, -} from './signals_filter_group'; -import { SignalsUtilityBar } from './signals_utility_bar'; -import * as i18n from './translations'; -import { - CreateTimelineProps, - SetEventsDeletedProps, - SetEventsLoadingProps, - UpdateSignalsStatusCallback, - UpdateSignalsStatusProps, -} from './types'; -import { dispatchUpdateTimeline } from '../../../../components/open_timeline/helpers'; - -export const SIGNALS_PAGE_TIMELINE_ID = 'signals-page'; - -interface OwnProps { - canUserCRUD: boolean; - defaultFilters?: Filter[]; - hasIndexWrite: boolean; - from: number; - loading: boolean; - signalsIndex: string; - to: number; -} - -type SignalsTableComponentProps = OwnProps & PropsFromRedux; - -export const SignalsTableComponent: React.FC = ({ - canUserCRUD, - clearEventsDeleted, - clearEventsLoading, - clearSelected, - defaultFilters, - from, - globalFilters, - globalQuery, - hasIndexWrite, - isSelectAllChecked, - loading, - loadingEventIds, - selectedEventIds, - setEventsDeleted, - setEventsLoading, - signalsIndex, - to, - updateTimeline, - updateTimelineIsLoading, -}) => { - const [selectAll, setSelectAll] = useState(false); - const apolloClient = useApolloClient(); - - const [showClearSelectionAction, setShowClearSelectionAction] = useState(false); - const [filterGroup, setFilterGroup] = useState(FILTER_OPEN); - const [{ browserFields, indexPatterns }] = useFetchIndexPatterns( - signalsIndex !== '' ? [signalsIndex] : [] - ); - const kibana = useKibana(); - - const getGlobalQuery = useCallback(() => { - if (browserFields != null && indexPatterns != null) { - return combineQueries({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - dataProviders: [], - indexPattern: indexPatterns, - browserFields, - filters: isEmpty(defaultFilters) - ? globalFilters - : [...(defaultFilters ?? []), ...globalFilters], - kqlQuery: globalQuery, - kqlMode: globalQuery.language, - start: from, - end: to, - isEventViewer: true, - }); - } - return null; - }, [browserFields, globalFilters, globalQuery, indexPatterns, kibana, to, from]); - - // Callback for creating a new timeline -- utilized by row/batch actions - const createTimelineCallback = useCallback( - ({ from: fromTimeline, timeline, to: toTimeline, ruleNote }: CreateTimelineProps) => { - updateTimelineIsLoading({ id: 'timeline-1', isLoading: false }); - updateTimeline({ - duplicate: true, - from: fromTimeline, - id: 'timeline-1', - notes: [], - timeline: { - ...timeline, - show: true, - }, - to: toTimeline, - ruleNote, - })(); - }, - [updateTimeline, updateTimelineIsLoading] - ); - - const setEventsLoadingCallback = useCallback( - ({ eventIds, isLoading }: SetEventsLoadingProps) => { - setEventsLoading!({ id: SIGNALS_PAGE_TIMELINE_ID, eventIds, isLoading }); - }, - [setEventsLoading, SIGNALS_PAGE_TIMELINE_ID] - ); - - const setEventsDeletedCallback = useCallback( - ({ eventIds, isDeleted }: SetEventsDeletedProps) => { - setEventsDeleted!({ id: SIGNALS_PAGE_TIMELINE_ID, eventIds, isDeleted }); - }, - [setEventsDeleted, SIGNALS_PAGE_TIMELINE_ID] - ); - - // Catches state change isSelectAllChecked->false upon user selection change to reset utility bar - useEffect(() => { - if (!isSelectAllChecked) { - setShowClearSelectionAction(false); - } else { - setSelectAll(false); - } - }, [isSelectAllChecked]); - - // Callback for when open/closed filter changes - const onFilterGroupChangedCallback = useCallback( - (newFilterGroup: SignalFilterOption) => { - clearEventsLoading!({ id: SIGNALS_PAGE_TIMELINE_ID }); - clearEventsDeleted!({ id: SIGNALS_PAGE_TIMELINE_ID }); - clearSelected!({ id: SIGNALS_PAGE_TIMELINE_ID }); - setFilterGroup(newFilterGroup); - }, - [clearEventsLoading, clearEventsDeleted, clearSelected, setFilterGroup] - ); - - // Callback for clearing entire selection from utility bar - const clearSelectionCallback = useCallback(() => { - clearSelected!({ id: SIGNALS_PAGE_TIMELINE_ID }); - setSelectAll(false); - setShowClearSelectionAction(false); - }, [clearSelected, setSelectAll, setShowClearSelectionAction]); - - // Callback for selecting all events on all pages from utility bar - // Dispatches to stateful_body's selectAll via TimelineTypeContext props - // as scope of response data required to actually set selectedEvents - const selectAllCallback = useCallback(() => { - setSelectAll(true); - setShowClearSelectionAction(true); - }, [setSelectAll, setShowClearSelectionAction]); - - const updateSignalsStatusCallback: UpdateSignalsStatusCallback = useCallback( - async (refetchQuery: inputsModel.Refetch, { signalIds, status }: UpdateSignalsStatusProps) => { - await updateSignalStatusAction({ - query: showClearSelectionAction ? getGlobalQuery()?.filterQuery : undefined, - signalIds: Object.keys(selectedEventIds), - status, - setEventsDeleted: setEventsDeletedCallback, - setEventsLoading: setEventsLoadingCallback, - }); - refetchQuery(); - }, - [ - getGlobalQuery, - selectedEventIds, - setEventsDeletedCallback, - setEventsLoadingCallback, - showClearSelectionAction, - ] - ); - - // Callback for creating the SignalUtilityBar which receives totalCount from EventsViewer component - const utilityBarCallback = useCallback( - (refetchQuery: inputsModel.Refetch, totalCount: number) => { - return ( - 0} - clearSelection={clearSelectionCallback} - hasIndexWrite={hasIndexWrite} - isFilteredToOpen={filterGroup === FILTER_OPEN} - selectAll={selectAllCallback} - selectedEventIds={selectedEventIds} - showClearSelection={showClearSelectionAction} - totalCount={totalCount} - updateSignalsStatus={updateSignalsStatusCallback.bind(null, refetchQuery)} - /> - ); - }, - [ - canUserCRUD, - hasIndexWrite, - clearSelectionCallback, - filterGroup, - loadingEventIds.length, - selectAllCallback, - selectedEventIds, - showClearSelectionAction, - updateSignalsStatusCallback, - ] - ); - - // Send to Timeline / Update Signal Status Actions for each table row - const additionalActions = useMemo( - () => - getSignalsActions({ - apolloClient, - canUserCRUD, - hasIndexWrite, - createTimeline: createTimelineCallback, - setEventsLoading: setEventsLoadingCallback, - setEventsDeleted: setEventsDeletedCallback, - status: filterGroup === FILTER_OPEN ? FILTER_CLOSED : FILTER_OPEN, - updateTimelineIsLoading, - }), - [ - apolloClient, - canUserCRUD, - createTimelineCallback, - hasIndexWrite, - filterGroup, - setEventsLoadingCallback, - setEventsDeletedCallback, - updateTimelineIsLoading, - ] - ); - - const defaultIndices = useMemo(() => [signalsIndex], [signalsIndex]); - const defaultFiltersMemo = useMemo(() => { - if (isEmpty(defaultFilters)) { - return filterGroup === FILTER_OPEN ? signalsOpenFilters : signalsClosedFilters; - } else if (defaultFilters != null && !isEmpty(defaultFilters)) { - return [ - ...defaultFilters, - ...(filterGroup === FILTER_OPEN ? signalsOpenFilters : signalsClosedFilters), - ]; - } - }, [defaultFilters, filterGroup]); - - const timelineTypeContext = useMemo( - () => ({ - documentType: i18n.SIGNALS_DOCUMENT_TYPE, - footerText: i18n.TOTAL_COUNT_OF_SIGNALS, - loadingText: i18n.LOADING_SIGNALS, - queryFields: requiredFieldsForActions, - timelineActions: additionalActions, - title: i18n.SIGNALS_TABLE_TITLE, - selectAll: canUserCRUD ? selectAll : false, - }), - [additionalActions, canUserCRUD, selectAll] - ); - - const headerFilterGroup = useMemo( - () => , - [onFilterGroupChangedCallback] - ); - - if (loading || isEmpty(signalsIndex)) { - return ( - - - - - ); - } - - return ( - - ); -}; - -const makeMapStateToProps = () => { - const getTimeline = timelineSelectors.getTimelineByIdSelector(); - const getGlobalInputs = inputsSelectors.globalSelector(); - const mapStateToProps = (state: State) => { - const timeline: TimelineModel = - getTimeline(state, SIGNALS_PAGE_TIMELINE_ID) ?? timelineDefaults; - const { deletedEventIds, isSelectAllChecked, loadingEventIds, selectedEventIds } = timeline; - - const globalInputs: inputsModel.InputsRange = getGlobalInputs(state); - const { query, filters } = globalInputs; - return { - globalQuery: query, - globalFilters: filters, - deletedEventIds, - isSelectAllChecked, - loadingEventIds, - selectedEventIds, - }; - }; - return mapStateToProps; -}; - -const mapDispatchToProps = (dispatch: Dispatch) => ({ - clearSelected: ({ id }: { id: string }) => dispatch(timelineActions.clearSelected({ id })), - setEventsLoading: ({ - id, - eventIds, - isLoading, - }: { - id: string; - eventIds: string[]; - isLoading: boolean; - }) => dispatch(timelineActions.setEventsLoading({ id, eventIds, isLoading })), - clearEventsLoading: ({ id }: { id: string }) => - dispatch(timelineActions.clearEventsLoading({ id })), - setEventsDeleted: ({ - id, - eventIds, - isDeleted, - }: { - id: string; - eventIds: string[]; - isDeleted: boolean; - }) => dispatch(timelineActions.setEventsDeleted({ id, eventIds, isDeleted })), - clearEventsDeleted: ({ id }: { id: string }) => - dispatch(timelineActions.clearEventsDeleted({ id })), - updateTimelineIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => - dispatch(timelineActions.updateIsLoading({ id, isLoading })), - updateTimeline: dispatchUpdateTimeline(dispatch), -}); - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const SignalsTable = connector(React.memo(SignalsTableComponent)); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.test.tsx deleted file mode 100644 index 6cab43b5285b50..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.test.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { shallow } from 'enzyme'; - -import { SignalsUtilityBar } from './index'; - -jest.mock('../../../../../lib/kibana'); - -describe('SignalsUtilityBar', () => { - it('renders correctly', () => { - const wrapper = shallow( - - ); - - expect(wrapper.find('[dataTestSubj="openCloseSignal"]')).toBeTruthy(); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx deleted file mode 100644 index b9268716f85f01..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/signals_utility_bar/index.tsx +++ /dev/null @@ -1,125 +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 { isEmpty } from 'lodash/fp'; -import React, { useCallback } from 'react'; -import numeral from '@elastic/numeral'; - -import { DEFAULT_NUMBER_FORMAT } from '../../../../../../common/constants'; -import { - UtilityBar, - UtilityBarAction, - UtilityBarGroup, - UtilityBarSection, - UtilityBarText, -} from '../../../../../components/utility_bar'; -import * as i18n from './translations'; -import { useUiSetting$ } from '../../../../../lib/kibana'; -import { TimelineNonEcsData } from '../../../../../graphql/types'; -import { UpdateSignalsStatus } from '../types'; -import { FILTER_CLOSED, FILTER_OPEN } from '../signals_filter_group'; - -interface SignalsUtilityBarProps { - canUserCRUD: boolean; - hasIndexWrite: boolean; - areEventsLoading: boolean; - clearSelection: () => void; - isFilteredToOpen: boolean; - selectAll: () => void; - selectedEventIds: Readonly>; - showClearSelection: boolean; - totalCount: number; - updateSignalsStatus: UpdateSignalsStatus; -} - -const SignalsUtilityBarComponent: React.FC = ({ - canUserCRUD, - hasIndexWrite, - areEventsLoading, - clearSelection, - totalCount, - selectedEventIds, - isFilteredToOpen, - selectAll, - showClearSelection, - updateSignalsStatus, -}) => { - const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); - - const handleUpdateStatus = useCallback(async () => { - await updateSignalsStatus({ - signalIds: Object.keys(selectedEventIds), - status: isFilteredToOpen ? FILTER_CLOSED : FILTER_OPEN, - }); - }, [selectedEventIds, updateSignalsStatus, isFilteredToOpen]); - - const formattedTotalCount = numeral(totalCount).format(defaultNumberFormat); - const formattedSelectedEventsCount = numeral(Object.keys(selectedEventIds).length).format( - defaultNumberFormat - ); - - return ( - <> - - - - - {i18n.SHOWING_SIGNALS(formattedTotalCount, totalCount)} - - - - - {canUserCRUD && hasIndexWrite && ( - <> - - {i18n.SELECTED_SIGNALS( - showClearSelection ? formattedTotalCount : formattedSelectedEventsCount, - showClearSelection ? totalCount : Object.keys(selectedEventIds).length - )} - - - - {isFilteredToOpen - ? i18n.BATCH_ACTION_CLOSE_SELECTED - : i18n.BATCH_ACTION_OPEN_SELECTED} - - - { - if (!showClearSelection) { - selectAll(); - } else { - clearSelection(); - } - }} - > - {showClearSelection - ? i18n.CLEAR_SELECTION - : i18n.SELECT_ALL_SIGNALS(formattedTotalCount, totalCount)} - - - )} - - - - - ); -}; - -export const SignalsUtilityBar = React.memo( - SignalsUtilityBarComponent, - (prevProps, nextProps) => - prevProps.areEventsLoading === nextProps.areEventsLoading && - prevProps.selectedEventIds === nextProps.selectedEventIds && - prevProps.totalCount === nextProps.totalCount && - prevProps.showClearSelection === nextProps.showClearSelection -); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/types.ts b/x-pack/plugins/siem/public/pages/detection_engine/components/signals/types.ts deleted file mode 100644 index 909b2176467461..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/components/signals/types.ts +++ /dev/null @@ -1,60 +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 ApolloClient from 'apollo-client'; - -import { Ecs } from '../../../../graphql/types'; -import { TimelineModel } from '../../../../store/timeline/model'; -import { inputsModel } from '../../../../store'; - -export interface SetEventsLoadingProps { - eventIds: string[]; - isLoading: boolean; -} - -export interface SetEventsDeletedProps { - eventIds: string[]; - isDeleted: boolean; -} - -export interface UpdateSignalsStatusProps { - signalIds: string[]; - status: 'open' | 'closed'; -} - -export type UpdateSignalsStatusCallback = ( - refetchQuery: inputsModel.Refetch, - { signalIds, status }: UpdateSignalsStatusProps -) => void; -export type UpdateSignalsStatus = ({ signalIds, status }: UpdateSignalsStatusProps) => void; - -export interface UpdateSignalStatusActionProps { - query?: string; - signalIds: string[]; - status: 'open' | 'closed'; - setEventsLoading: ({ eventIds, isLoading }: SetEventsLoadingProps) => void; - setEventsDeleted: ({ eventIds, isDeleted }: SetEventsDeletedProps) => void; -} - -export type SendSignalsToTimeline = () => void; - -export interface SendSignalToTimelineActionProps { - apolloClient?: ApolloClient<{}>; - createTimeline: CreateTimeline; - ecsData: Ecs; - updateTimelineIsLoading: UpdateTimelineLoading; -} - -export type UpdateTimelineLoading = ({ id, isLoading }: { id: string; isLoading: boolean }) => void; - -export interface CreateTimelineProps { - from: number; - timeline: TimelineModel; - to: number; - ruleNote?: string; -} - -export type CreateTimeline = ({ from, timeline, to }: CreateTimelineProps) => void; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/helpers.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/helpers.tsx deleted file mode 100644 index 24b12cae62d85d..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/helpers.tsx +++ /dev/null @@ -1,101 +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 { showAllOthersBucket } from '../../../../../common/constants'; -import { HistogramData, SignalsAggregation, SignalsBucket, SignalsGroupBucket } from './types'; -import { SignalSearchResponse } from '../../../../containers/detection_engine/signals/types'; -import * as i18n from './translations'; - -export const formatSignalsData = ( - signalsData: SignalSearchResponse<{}, SignalsAggregation> | null -) => { - const groupBuckets: SignalsGroupBucket[] = - signalsData?.aggregations?.signalsByGrouping?.buckets ?? []; - return groupBuckets.reduce((acc, { key: group, signals }) => { - const signalsBucket: SignalsBucket[] = signals.buckets ?? []; - - return [ - ...acc, - ...signalsBucket.map(({ key, doc_count }: SignalsBucket) => ({ - x: key, - y: doc_count, - g: group, - })), - ]; - }, []); -}; - -export const getSignalsHistogramQuery = ( - stackByField: string, - from: number, - to: number, - additionalFilters: Array<{ - bool: { filter: unknown[]; should: unknown[]; must_not: unknown[]; must: unknown[] }; - }> -) => { - const missing = showAllOthersBucket.includes(stackByField) - ? { - missing: stackByField.endsWith('.ip') ? '0.0.0.0' : i18n.ALL_OTHERS, - } - : {}; - - return { - aggs: { - signalsByGrouping: { - terms: { - field: stackByField, - ...missing, - order: { - _count: 'desc', - }, - size: 10, - }, - aggs: { - signals: { - date_histogram: { - field: '@timestamp', - fixed_interval: `${Math.floor((to - from) / 32)}ms`, - min_doc_count: 0, - extended_bounds: { - min: from, - max: to, - }, - }, - }, - }, - }, - }, - query: { - bool: { - filter: [ - ...additionalFilters, - { - range: { - '@timestamp': { - gte: from, - lte: to, - }, - }, - }, - ], - }, - }, - }; -}; - -/** - * Returns `true` when the signals histogram initial loading spinner should be shown - * - * @param isInitialLoading The loading spinner will only be displayed if this value is `true`, because after initial load, a different, non-spinner loading indicator is displayed - * @param isLoadingSignals When `true`, IO is being performed to request signals (for rendering in the histogram) - */ -export const showInitialLoadingSpinner = ({ - isInitialLoading, - isLoadingSignals, -}: { - isInitialLoading: boolean; - isLoadingSignals: boolean; -}): boolean => isInitialLoading && isLoadingSignals; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.test.tsx deleted file mode 100644 index 6921c49d8a8b48..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.test.tsx +++ /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 React from 'react'; -import { shallow } from 'enzyme'; - -import { SignalsHistogramPanel } from './index'; - -jest.mock('../../../../lib/kibana'); -jest.mock('../../../../components/navigation/use_get_url_search'); - -describe('SignalsHistogramPanel', () => { - it('renders correctly', () => { - const wrapper = shallow( - - ); - - expect(wrapper.find('[id="detections-histogram"]')).toBeTruthy(); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx deleted file mode 100644 index 9b336766b1724f..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/index.tsx +++ /dev/null @@ -1,284 +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 { Position } from '@elastic/charts'; -import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSelect, EuiPanel } from '@elastic/eui'; -import numeral from '@elastic/numeral'; -import React, { memo, useCallback, useMemo, useState, useEffect } from 'react'; -import styled from 'styled-components'; -import { isEmpty } from 'lodash/fp'; -import uuid from 'uuid'; - -import { DEFAULT_NUMBER_FORMAT } from '../../../../../common/constants'; -import { UpdateDateRange } from '../../../../components/charts/common'; -import { LegendItem } from '../../../../components/charts/draggable_legend_item'; -import { escapeDataProviderId } from '../../../../components/drag_and_drop/helpers'; -import { HeaderSection } from '../../../../components/header_section'; -import { Filter, esQuery, Query } from '../../../../../../../../src/plugins/data/public'; -import { useQuerySignals } from '../../../../containers/detection_engine/signals/use_query'; -import { getDetectionEngineUrl } from '../../../../components/link_to'; -import { defaultLegendColors } from '../../../../components/matrix_histogram/utils'; -import { InspectButtonContainer } from '../../../../components/inspect'; -import { useGetUrlSearch } from '../../../../components/navigation/use_get_url_search'; -import { MatrixLoader } from '../../../../components/matrix_histogram/matrix_loader'; -import { MatrixHistogramOption } from '../../../../components/matrix_histogram/types'; -import { useKibana, useUiSetting$ } from '../../../../lib/kibana'; -import { navTabs } from '../../../home/home_navigations'; -import { signalsHistogramOptions } from './config'; -import { formatSignalsData, getSignalsHistogramQuery, showInitialLoadingSpinner } from './helpers'; -import { SignalsHistogram } from './signals_histogram'; -import * as i18n from './translations'; -import { RegisterQuery, SignalsHistogramOption, SignalsAggregation, SignalsTotal } from './types'; - -const DEFAULT_PANEL_HEIGHT = 300; - -const StyledEuiPanel = styled(EuiPanel)<{ height?: number }>` - display: flex; - flex-direction: column; - ${({ height }) => (height != null ? `height: ${height}px;` : '')} - position: relative; -`; - -const defaultTotalSignalsObj: SignalsTotal = { - value: 0, - relation: 'eq', -}; - -export const DETECTIONS_HISTOGRAM_ID = 'detections-histogram'; - -const ViewSignalsFlexItem = styled(EuiFlexItem)` - margin-left: 24px; -`; - -interface SignalsHistogramPanelProps { - chartHeight?: number; - defaultStackByOption?: SignalsHistogramOption; - deleteQuery?: ({ id }: { id: string }) => void; - filters?: Filter[]; - from: number; - headerChildren?: React.ReactNode; - /** Override all defaults, and only display this field */ - onlyField?: string; - query?: Query; - legendPosition?: Position; - panelHeight?: number; - signalIndexName: string | null; - setQuery: (params: RegisterQuery) => void; - showLinkToSignals?: boolean; - showTotalSignalsCount?: boolean; - stackByOptions?: SignalsHistogramOption[]; - title?: string; - to: number; - updateDateRange: UpdateDateRange; -} - -const getHistogramOption = (fieldName: string): MatrixHistogramOption => ({ - text: fieldName, - value: fieldName, -}); - -const NO_LEGEND_DATA: LegendItem[] = []; - -export const SignalsHistogramPanel = memo( - ({ - chartHeight, - defaultStackByOption = signalsHistogramOptions[0], - deleteQuery, - filters, - headerChildren, - onlyField, - query, - from, - legendPosition = 'right', - panelHeight = DEFAULT_PANEL_HEIGHT, - setQuery, - signalIndexName, - showLinkToSignals = false, - showTotalSignalsCount = false, - stackByOptions, - to, - title = i18n.HISTOGRAM_HEADER, - updateDateRange, - }) => { - // create a unique, but stable (across re-renders) query id - const uniqueQueryId = useMemo(() => `${DETECTIONS_HISTOGRAM_ID}-${uuid.v4()}`, []); - const [isInitialLoading, setIsInitialLoading] = useState(true); - const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); - const [totalSignalsObj, setTotalSignalsObj] = useState(defaultTotalSignalsObj); - const [selectedStackByOption, setSelectedStackByOption] = useState( - onlyField == null ? defaultStackByOption : getHistogramOption(onlyField) - ); - const { - loading: isLoadingSignals, - data: signalsData, - setQuery: setSignalsQuery, - response, - request, - refetch, - } = useQuerySignals<{}, SignalsAggregation>( - getSignalsHistogramQuery(selectedStackByOption.value, from, to, []), - signalIndexName - ); - const kibana = useKibana(); - const urlSearch = useGetUrlSearch(navTabs.detections); - - const totalSignals = useMemo( - () => - i18n.SHOWING_SIGNALS( - numeral(totalSignalsObj.value).format(defaultNumberFormat), - totalSignalsObj.value, - totalSignalsObj.relation === 'gte' ? '>' : totalSignalsObj.relation === 'lte' ? '<' : '' - ), - [totalSignalsObj] - ); - - const setSelectedOptionCallback = useCallback((event: React.ChangeEvent) => { - setSelectedStackByOption( - stackByOptions?.find(co => co.value === event.target.value) ?? defaultStackByOption - ); - }, []); - - const formattedSignalsData = useMemo(() => formatSignalsData(signalsData), [signalsData]); - - const legendItems: LegendItem[] = useMemo( - () => - signalsData?.aggregations?.signalsByGrouping?.buckets != null - ? signalsData.aggregations.signalsByGrouping.buckets.map((bucket, i) => ({ - color: i < defaultLegendColors.length ? defaultLegendColors[i] : undefined, - dataProviderId: escapeDataProviderId( - `draggable-legend-item-${uuid.v4()}-${selectedStackByOption.value}-${bucket.key}` - ), - field: selectedStackByOption.value, - value: bucket.key, - })) - : NO_LEGEND_DATA, - [signalsData, selectedStackByOption.value] - ); - - useEffect(() => { - let canceled = false; - - if (!canceled && !showInitialLoadingSpinner({ isInitialLoading, isLoadingSignals })) { - setIsInitialLoading(false); - } - - return () => { - canceled = true; // prevent long running data fetches from updating state after unmounting - }; - }, [isInitialLoading, isLoadingSignals, setIsInitialLoading]); - - useEffect(() => { - return () => { - if (deleteQuery) { - deleteQuery({ id: uniqueQueryId }); - } - }; - }, []); - - useEffect(() => { - if (refetch != null && setQuery != null) { - setQuery({ - id: uniqueQueryId, - inspect: { - dsl: [request], - response: [response], - }, - loading: isLoadingSignals, - refetch, - }); - } - }, [setQuery, isLoadingSignals, signalsData, response, request, refetch]); - - useEffect(() => { - setTotalSignalsObj( - signalsData?.hits.total ?? { - value: 0, - relation: 'eq', - } - ); - }, [signalsData]); - - useEffect(() => { - const converted = esQuery.buildEsQuery( - undefined, - query != null ? [query] : [], - filters?.filter(f => f.meta.disabled === false) ?? [], - { - ...esQuery.getEsQueryConfig(kibana.services.uiSettings), - dateFormatTZ: undefined, - } - ); - - setSignalsQuery( - getSignalsHistogramQuery( - selectedStackByOption.value, - from, - to, - !isEmpty(converted) ? [converted] : [] - ) - ); - }, [selectedStackByOption.value, from, to, query, filters]); - - const linkButton = useMemo(() => { - if (showLinkToSignals) { - return ( - - {i18n.VIEW_SIGNALS} - - ); - } - }, [showLinkToSignals, urlSearch]); - - const titleText = useMemo(() => (onlyField == null ? title : i18n.TOP(onlyField)), [ - onlyField, - title, - ]); - - return ( - - - - - - {stackByOptions && ( - - )} - {headerChildren != null && headerChildren} - - {linkButton} - - - - {isInitialLoading ? ( - - ) : ( - - )} - - - ); - } -); - -SignalsHistogramPanel.displayName = 'SignalsHistogramPanel'; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/types.ts b/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/types.ts deleted file mode 100644 index 6ef4cecc4ec8b4..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/components/signals_histogram_panel/types.ts +++ /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 { inputsModel } from '../../../../store'; - -export interface SignalsHistogramOption { - text: string; - value: string; -} - -export interface HistogramData { - x: number; - y: number; - g: string; -} - -export interface SignalsAggregation { - signalsByGrouping: { - buckets: SignalsGroupBucket[]; - }; -} - -export interface SignalsBucket { - key_as_string: string; - key: number; - doc_count: number; -} - -export interface SignalsGroupBucket { - key: string; - signals: { - buckets: SignalsBucket[]; - }; -} - -export interface SignalsTotal { - value: number; - relation: string; -} - -export interface RegisterQuery { - id: string; - inspect: inputsModel.InspectQuery | null; - loading: boolean; - refetch: inputsModel.Refetch; -} diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/signals_info/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/signals_info/index.tsx deleted file mode 100644 index e7cdc3345c031a..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/components/signals_info/index.tsx +++ /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 { EuiLoadingSpinner } from '@elastic/eui'; -import { FormattedRelative } from '@kbn/i18n/react'; -import React, { useState, useEffect } from 'react'; - -import { useQuerySignals } from '../../../../containers/detection_engine/signals/use_query'; -import { buildLastSignalsQuery } from './query.dsl'; -import { Aggs } from './types'; - -interface SignalInfo { - ruleId?: string | null; -} - -type Return = [React.ReactNode, React.ReactNode]; - -export const useSignalInfo = ({ ruleId = null }: SignalInfo): Return => { - const [lastSignals, setLastSignals] = useState( - - ); - const [totalSignals, setTotalSignals] = useState( - - ); - - const { loading, data: signals } = useQuerySignals(buildLastSignalsQuery(ruleId)); - - useEffect(() => { - if (signals != null) { - const mySignals = signals; - setLastSignals( - mySignals.aggregations?.lastSeen.value != null ? ( - - ) : null - ); - setTotalSignals(<>{mySignals.hits.total.value}); - } else { - setLastSignals(null); - setTotalSignals(null); - } - }, [loading, signals]); - - return [lastSignals, totalSignals]; -}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/user_info/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/user_info/index.test.tsx deleted file mode 100644 index b3d710de5e94e8..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/components/user_info/index.test.tsx +++ /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. - */ - -import { renderHook } from '@testing-library/react-hooks'; -import { useUserInfo } from './index'; - -import { usePrivilegeUser } from '../../../../containers/detection_engine/signals/use_privilege_user'; -import { useSignalIndex } from '../../../../containers/detection_engine/signals/use_signal_index'; -import { useKibana } from '../../../../lib/kibana'; -jest.mock('../../../../containers/detection_engine/signals/use_privilege_user'); -jest.mock('../../../../containers/detection_engine/signals/use_signal_index'); -jest.mock('../../../../lib/kibana'); - -describe('useUserInfo', () => { - beforeAll(() => { - (usePrivilegeUser as jest.Mock).mockReturnValue({}); - (useSignalIndex as jest.Mock).mockReturnValue({}); - (useKibana as jest.Mock).mockReturnValue({ - services: { - application: { - capabilities: { - siem: { - crud: true, - }, - }, - }, - }, - }); - }); - it('returns default state', () => { - const { result } = renderHook(() => useUserInfo()); - - expect(result).toEqual({ - current: { - canUserCRUD: null, - hasEncryptionKey: null, - hasIndexManage: null, - hasIndexWrite: null, - isAuthenticated: null, - isSignalIndexExists: null, - loading: true, - signalIndexName: null, - }, - error: undefined, - }); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx deleted file mode 100644 index 9e45371fb6058e..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/components/user_info/index.tsx +++ /dev/null @@ -1,243 +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 { noop } from 'lodash/fp'; -import React, { useEffect, useReducer, Dispatch, createContext, useContext } from 'react'; - -import { usePrivilegeUser } from '../../../../containers/detection_engine/signals/use_privilege_user'; -import { useSignalIndex } from '../../../../containers/detection_engine/signals/use_signal_index'; -import { useKibana } from '../../../../lib/kibana'; - -export interface State { - canUserCRUD: boolean | null; - hasIndexManage: boolean | null; - hasIndexWrite: boolean | null; - isSignalIndexExists: boolean | null; - isAuthenticated: boolean | null; - hasEncryptionKey: boolean | null; - loading: boolean; - signalIndexName: string | null; -} - -const initialState: State = { - canUserCRUD: null, - hasIndexManage: null, - hasIndexWrite: null, - isSignalIndexExists: null, - isAuthenticated: null, - hasEncryptionKey: null, - loading: true, - signalIndexName: null, -}; - -export type Action = - | { type: 'updateLoading'; loading: boolean } - | { - type: 'updateHasIndexManage'; - hasIndexManage: boolean | null; - } - | { - type: 'updateHasIndexWrite'; - hasIndexWrite: boolean | null; - } - | { - type: 'updateIsSignalIndexExists'; - isSignalIndexExists: boolean | null; - } - | { - type: 'updateIsAuthenticated'; - isAuthenticated: boolean | null; - } - | { - type: 'updateHasEncryptionKey'; - hasEncryptionKey: boolean | null; - } - | { - type: 'updateCanUserCRUD'; - canUserCRUD: boolean | null; - } - | { - type: 'updateSignalIndexName'; - signalIndexName: string | null; - }; - -export const userInfoReducer = (state: State, action: Action): State => { - switch (action.type) { - case 'updateLoading': { - return { - ...state, - loading: action.loading, - }; - } - case 'updateHasIndexManage': { - return { - ...state, - hasIndexManage: action.hasIndexManage, - }; - } - case 'updateHasIndexWrite': { - return { - ...state, - hasIndexWrite: action.hasIndexWrite, - }; - } - case 'updateIsSignalIndexExists': { - return { - ...state, - isSignalIndexExists: action.isSignalIndexExists, - }; - } - case 'updateIsAuthenticated': { - return { - ...state, - isAuthenticated: action.isAuthenticated, - }; - } - case 'updateHasEncryptionKey': { - return { - ...state, - hasEncryptionKey: action.hasEncryptionKey, - }; - } - case 'updateCanUserCRUD': { - return { - ...state, - canUserCRUD: action.canUserCRUD, - }; - } - case 'updateSignalIndexName': { - return { - ...state, - signalIndexName: action.signalIndexName, - }; - } - default: - return state; - } -}; - -const StateUserInfoContext = createContext<[State, Dispatch]>([initialState, () => noop]); - -const useUserData = () => useContext(StateUserInfoContext); - -interface ManageUserInfoProps { - children: React.ReactNode; -} - -export const ManageUserInfo = ({ children }: ManageUserInfoProps) => ( - - {children} - -); - -export const useUserInfo = (): State => { - const [ - { - canUserCRUD, - hasIndexManage, - hasIndexWrite, - isSignalIndexExists, - isAuthenticated, - hasEncryptionKey, - loading, - signalIndexName, - }, - dispatch, - ] = useUserData(); - const { - loading: privilegeLoading, - isAuthenticated: isApiAuthenticated, - hasEncryptionKey: isApiEncryptionKey, - hasIndexManage: hasApiIndexManage, - hasIndexWrite: hasApiIndexWrite, - } = usePrivilegeUser(); - const { - loading: indexNameLoading, - signalIndexExists: isApiSignalIndexExists, - signalIndexName: apiSignalIndexName, - createDeSignalIndex: createSignalIndex, - } = useSignalIndex(); - - const uiCapabilities = useKibana().services.application.capabilities; - const capabilitiesCanUserCRUD: boolean = - typeof uiCapabilities.siem.crud === 'boolean' ? uiCapabilities.siem.crud : false; - - useEffect(() => { - if (loading !== privilegeLoading || indexNameLoading) { - dispatch({ type: 'updateLoading', loading: privilegeLoading || indexNameLoading }); - } - }, [loading, privilegeLoading, indexNameLoading]); - - useEffect(() => { - if (!loading && hasIndexManage !== hasApiIndexManage && hasApiIndexManage != null) { - dispatch({ type: 'updateHasIndexManage', hasIndexManage: hasApiIndexManage }); - } - }, [loading, hasIndexManage, hasApiIndexManage]); - - useEffect(() => { - if (!loading && hasIndexWrite !== hasApiIndexWrite && hasApiIndexWrite != null) { - dispatch({ type: 'updateHasIndexWrite', hasIndexWrite: hasApiIndexWrite }); - } - }, [loading, hasIndexWrite, hasApiIndexWrite]); - - useEffect(() => { - if ( - !loading && - isSignalIndexExists !== isApiSignalIndexExists && - isApiSignalIndexExists != null - ) { - dispatch({ type: 'updateIsSignalIndexExists', isSignalIndexExists: isApiSignalIndexExists }); - } - }, [loading, isSignalIndexExists, isApiSignalIndexExists]); - - useEffect(() => { - if (!loading && isAuthenticated !== isApiAuthenticated && isApiAuthenticated != null) { - dispatch({ type: 'updateIsAuthenticated', isAuthenticated: isApiAuthenticated }); - } - }, [loading, isAuthenticated, isApiAuthenticated]); - - useEffect(() => { - if (!loading && hasEncryptionKey !== isApiEncryptionKey && isApiEncryptionKey != null) { - dispatch({ type: 'updateHasEncryptionKey', hasEncryptionKey: isApiEncryptionKey }); - } - }, [loading, hasEncryptionKey, isApiEncryptionKey]); - - useEffect(() => { - if (!loading && canUserCRUD !== capabilitiesCanUserCRUD && capabilitiesCanUserCRUD != null) { - dispatch({ type: 'updateCanUserCRUD', canUserCRUD: capabilitiesCanUserCRUD }); - } - }, [loading, canUserCRUD, capabilitiesCanUserCRUD]); - - useEffect(() => { - if (!loading && signalIndexName !== apiSignalIndexName && apiSignalIndexName != null) { - dispatch({ type: 'updateSignalIndexName', signalIndexName: apiSignalIndexName }); - } - }, [loading, signalIndexName, apiSignalIndexName]); - - useEffect(() => { - if ( - isAuthenticated && - hasEncryptionKey && - hasIndexManage && - isSignalIndexExists != null && - !isSignalIndexExists && - createSignalIndex != null - ) { - createSignalIndex(); - } - }, [createSignalIndex, isAuthenticated, hasEncryptionKey, isSignalIndexExists, hasIndexManage]); - - return { - loading, - isSignalIndexExists, - isAuthenticated, - hasEncryptionKey, - canUserCRUD, - hasIndexManage, - hasIndexWrite, - signalIndexName, - }; -}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/index.test.tsx deleted file mode 100644 index 6c4980f1d15002..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/index.test.tsx +++ /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. - */ - -import React from 'react'; -import { shallow } from 'enzyme'; - -import '../../mock/match_media'; -import { DetectionEngineContainer } from './index'; - -describe('DetectionEngineContainer', () => { - it('renders correctly', () => { - const wrapper = shallow(); - - expect(wrapper.find('ManageUserInfo')).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/index.tsx deleted file mode 100644 index 15093488195106..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/index.tsx +++ /dev/null @@ -1,54 +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 { Redirect, Route, Switch, RouteComponentProps } from 'react-router-dom'; - -import { ManageUserInfo } from './components/user_info'; -import { CreateRulePage } from './rules/create'; -import { DetectionEnginePage } from './detection_engine'; -import { EditRulePage } from './rules/edit'; -import { RuleDetailsPage } from './rules/details'; -import { RulesPage } from './rules'; -import { DetectionEngineTab } from './types'; - -const detectionEnginePath = `/:pageName(detections)`; - -type Props = Partial> & { url: string }; - -const DetectionEngineContainerComponent: React.FC = () => ( - - - - - - - - - - - - - - - - - - ( - - )} - /> - - -); - -export const DetectionEngineContainer = React.memo(DetectionEngineContainerComponent); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts deleted file mode 100644 index 66964fae70f941..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/all/__mocks__/mock.ts +++ /dev/null @@ -1,218 +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 { esFilters } from '../../../../../../../../../src/plugins/data/public'; -import { Rule, RuleError } from '../../../../../containers/detection_engine/rules'; -import { AboutStepRule, ActionsStepRule, DefineStepRule, ScheduleStepRule } from '../../types'; -import { FieldValueQueryBar } from '../../components/query_bar'; - -export const mockQueryBar: FieldValueQueryBar = { - query: { - query: 'test query', - language: 'kuery', - }, - filters: [ - { - $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, - }, - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - ], - saved_id: 'test123', -}; - -export const mockRule = (id: string): Rule => ({ - actions: [], - created_at: '2020-01-10T21:11:45.839Z', - updated_at: '2020-01-10T21:11:45.839Z', - created_by: 'elastic', - description: '24/7', - enabled: true, - false_positives: [], - filters: [], - from: 'now-300s', - id, - immutable: false, - index: ['auditbeat-*'], - interval: '5m', - rule_id: 'b5ba41ab-aaf3-4f43-971b-bdf9434ce0ea', - language: 'kuery', - output_index: '.siem-signals-default', - max_signals: 100, - risk_score: 21, - name: 'Home Grown!', - query: '', - references: [], - saved_id: "Garrett's IP", - timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', - timeline_title: 'Untitled timeline', - meta: { from: '0m' }, - severity: 'low', - updated_by: 'elastic', - tags: [], - to: 'now', - type: 'saved_query', - threat: [], - throttle: 'no_actions', - note: '# this is some markdown documentation', - version: 1, -}); - -export const mockRuleWithEverything = (id: string): Rule => ({ - actions: [], - created_at: '2020-01-10T21:11:45.839Z', - updated_at: '2020-01-10T21:11:45.839Z', - created_by: 'elastic', - description: '24/7', - enabled: true, - false_positives: ['test'], - filters: [ - { - $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, - }, - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - ], - from: 'now-300s', - id, - immutable: false, - index: ['auditbeat-*'], - interval: '5m', - rule_id: 'b5ba41ab-aaf3-4f43-971b-bdf9434ce0ea', - language: 'kuery', - output_index: '.siem-signals-default', - max_signals: 100, - risk_score: 21, - name: 'Query with rule-id', - query: 'user.name: root or user.name: admin', - references: ['www.test.co'], - saved_id: 'test123', - timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', - timeline_title: 'Titled timeline', - meta: { from: '0m' }, - severity: 'low', - updated_by: 'elastic', - tags: ['tag1', 'tag2'], - to: 'now', - type: 'saved_query', - threat: [ - { - framework: 'mockFramework', - tactic: { - id: '1234', - name: 'tactic1', - reference: 'reference1', - }, - technique: [ - { - id: '456', - name: 'technique1', - reference: 'technique reference', - }, - ], - }, - ], - throttle: 'no_actions', - note: '# this is some markdown documentation', - version: 1, -}); - -export const mockAboutStepRule = (isNew = false): AboutStepRule => ({ - isNew, - name: 'Query with rule-id', - description: '24/7', - severity: 'low', - riskScore: 21, - references: ['www.test.co'], - falsePositives: ['test'], - tags: ['tag1', 'tag2'], - threat: [ - { - framework: 'mockFramework', - tactic: { - id: '1234', - name: 'tactic1', - reference: 'reference1', - }, - technique: [ - { - id: '456', - name: 'technique1', - reference: 'technique reference', - }, - ], - }, - ], - note: '# this is some markdown documentation', -}); - -export const mockActionsStepRule = (isNew = false, enabled = false): ActionsStepRule => ({ - isNew, - actions: [], - kibanaSiemAppUrl: 'http://localhost:5601/app/siem', - enabled, - throttle: 'no_actions', -}); - -export const mockDefineStepRule = (isNew = false): DefineStepRule => ({ - isNew, - ruleType: 'query', - anomalyThreshold: 50, - machineLearningJobId: '', - index: ['filebeat-'], - queryBar: mockQueryBar, - timeline: { - id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', - title: 'Titled timeline', - }, -}); - -export const mockScheduleStepRule = (isNew = false): ScheduleStepRule => ({ - isNew, - interval: '5m', - from: '6m', - to: 'now', -}); - -export const mockRuleError = (id: string): RuleError => ({ - rule_id: id, - error: { status_code: 404, message: `id: "${id}" not found` }, -}); - -export const mockRules: Rule[] = [ - mockRule('abe6c564-050d-45a5-aaf0-386c37dd1f61'), - mockRule('63f06f34-c181-4b2d-af35-f2ace572a1ee'), -]; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx deleted file mode 100644 index bc5d0c32bb9c6c..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/all/actions.tsx +++ /dev/null @@ -1,136 +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 H from 'history'; -import React, { Dispatch } from 'react'; - -import { DETECTION_ENGINE_PAGE_NAME } from '../../../../components/link_to/redirect_to_detection_engine'; -import { - deleteRules, - duplicateRules, - enableRules, - Rule, -} from '../../../../containers/detection_engine/rules'; -import { Action } from './reducer'; - -import { - ActionToaster, - displayErrorToast, - displaySuccessToast, - errorToToaster, -} from '../../../../components/toasters'; -import { track, METRIC_TYPE, TELEMETRY_EVENT } from '../../../../lib/telemetry'; - -import * as i18n from '../translations'; -import { bucketRulesResponse } from './helpers'; - -export const editRuleAction = (rule: Rule, history: H.History) => { - history.push(`/${DETECTION_ENGINE_PAGE_NAME}/rules/id/${rule.id}/edit`); -}; - -export const duplicateRulesAction = async ( - rules: Rule[], - ruleIds: string[], - dispatch: React.Dispatch, - dispatchToaster: Dispatch -) => { - try { - dispatch({ type: 'loadingRuleIds', ids: ruleIds, actionType: 'duplicate' }); - const response = await duplicateRules({ rules }); - const { errors } = bucketRulesResponse(response); - if (errors.length > 0) { - displayErrorToast( - i18n.DUPLICATE_RULE_ERROR, - errors.map(e => e.error.message), - dispatchToaster - ); - } else { - displaySuccessToast(i18n.SUCCESSFULLY_DUPLICATED_RULES(ruleIds.length), dispatchToaster); - } - dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); - } catch (error) { - dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); - errorToToaster({ title: i18n.DUPLICATE_RULE_ERROR, error, dispatchToaster }); - } -}; - -export const exportRulesAction = (exportRuleId: string[], dispatch: React.Dispatch) => { - dispatch({ type: 'exportRuleIds', ids: exportRuleId }); -}; - -export const deleteRulesAction = async ( - ruleIds: string[], - dispatch: React.Dispatch, - dispatchToaster: Dispatch, - onRuleDeleted?: () => void -) => { - try { - dispatch({ type: 'loadingRuleIds', ids: ruleIds, actionType: 'delete' }); - const response = await deleteRules({ ids: ruleIds }); - const { errors } = bucketRulesResponse(response); - dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); - if (errors.length > 0) { - displayErrorToast( - i18n.BATCH_ACTION_DELETE_SELECTED_ERROR(ruleIds.length), - errors.map(e => e.error.message), - dispatchToaster - ); - } else if (onRuleDeleted) { - onRuleDeleted(); - } - } catch (error) { - dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); - errorToToaster({ - title: i18n.BATCH_ACTION_DELETE_SELECTED_ERROR(ruleIds.length), - error, - dispatchToaster, - }); - } -}; - -export const enableRulesAction = async ( - ids: string[], - enabled: boolean, - dispatch: React.Dispatch, - dispatchToaster: Dispatch -) => { - const errorTitle = enabled - ? i18n.BATCH_ACTION_ACTIVATE_SELECTED_ERROR(ids.length) - : i18n.BATCH_ACTION_DEACTIVATE_SELECTED_ERROR(ids.length); - - try { - dispatch({ type: 'loadingRuleIds', ids, actionType: enabled ? 'enable' : 'disable' }); - - const response = await enableRules({ ids, enabled }); - const { rules, errors } = bucketRulesResponse(response); - - dispatch({ type: 'updateRules', rules }); - - if (errors.length > 0) { - displayErrorToast( - errorTitle, - errors.map(e => e.error.message), - dispatchToaster - ); - } - - if (rules.some(rule => rule.immutable)) { - track( - METRIC_TYPE.COUNT, - enabled ? TELEMETRY_EVENT.SIEM_RULE_ENABLED : TELEMETRY_EVENT.SIEM_RULE_DISABLED - ); - } - if (rules.some(rule => !rule.immutable)) { - track( - METRIC_TYPE.COUNT, - enabled ? TELEMETRY_EVENT.CUSTOM_RULE_ENABLED : TELEMETRY_EVENT.CUSTOM_RULE_DISABLED - ); - } - } catch (e) { - displayErrorToast(errorTitle, [e.message], dispatchToaster); - dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); - } -}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx deleted file mode 100644 index 8e79f037d82b06..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/all/columns.tsx +++ /dev/null @@ -1,333 +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. - */ - -/* eslint-disable react/display-name */ - -import { - EuiBadge, - EuiLink, - EuiBasicTableColumn, - EuiTableActionsColumnType, - EuiText, - EuiHealth, - EuiToolTip, -} from '@elastic/eui'; -import { FormattedRelative } from '@kbn/i18n/react'; -import * as H from 'history'; -import React, { Dispatch } from 'react'; - -import { isMlRule } from '../../../../../common/detection_engine/ml_helpers'; -import { Rule, RuleStatus } from '../../../../containers/detection_engine/rules'; -import { getEmptyTagValue } from '../../../../components/empty_value'; -import { FormattedDate } from '../../../../components/formatted_date'; -import { getRuleDetailsUrl } from '../../../../components/link_to/redirect_to_detection_engine'; -import { ActionToaster } from '../../../../components/toasters'; -import { TruncatableText } from '../../../../components/truncatable_text'; -import { getStatusColor } from '../components/rule_status/helpers'; -import { RuleSwitch } from '../components/rule_switch'; -import { SeverityBadge } from '../components/severity_badge'; -import * as i18n from '../translations'; -import { - deleteRulesAction, - duplicateRulesAction, - editRuleAction, - exportRulesAction, -} from './actions'; -import { Action } from './reducer'; -import { LocalizedDateTooltip } from '../../../../components/localized_date_tooltip'; -import * as detectionI18n from '../../translations'; - -export const getActions = ( - dispatch: React.Dispatch, - dispatchToaster: Dispatch, - history: H.History, - reFetchRules: (refreshPrePackagedRule?: boolean) => void -) => [ - { - description: i18n.EDIT_RULE_SETTINGS, - icon: 'controlsHorizontal', - name: i18n.EDIT_RULE_SETTINGS, - onClick: (rowItem: Rule) => editRuleAction(rowItem, history), - }, - { - description: i18n.DUPLICATE_RULE, - icon: 'copy', - name: i18n.DUPLICATE_RULE, - onClick: async (rowItem: Rule) => { - await duplicateRulesAction([rowItem], [rowItem.id], dispatch, dispatchToaster); - await reFetchRules(true); - }, - }, - { - 'data-test-subj': 'exportRuleAction', - description: i18n.EXPORT_RULE, - icon: 'exportAction', - name: i18n.EXPORT_RULE, - onClick: (rowItem: Rule) => exportRulesAction([rowItem.rule_id], dispatch), - enabled: (rowItem: Rule) => !rowItem.immutable, - }, - { - 'data-test-subj': 'deleteRuleAction', - description: i18n.DELETE_RULE, - icon: 'trash', - name: i18n.DELETE_RULE, - onClick: async (rowItem: Rule) => { - await deleteRulesAction([rowItem.id], dispatch, dispatchToaster); - await reFetchRules(true); - }, - }, -]; - -export type RuleStatusRowItemType = RuleStatus & { - name: string; - id: string; -}; -export type RulesColumns = EuiBasicTableColumn | EuiTableActionsColumnType; -export type RulesStatusesColumns = EuiBasicTableColumn; - -interface GetColumns { - dispatch: React.Dispatch; - dispatchToaster: Dispatch; - history: H.History; - hasMlPermissions: boolean; - hasNoPermissions: boolean; - loadingRuleIds: string[]; - reFetchRules: (refreshPrePackagedRule?: boolean) => void; -} - -// Michael: Are we able to do custom, in-table-header filters, as shown in my wireframes? -export const getColumns = ({ - dispatch, - dispatchToaster, - history, - hasMlPermissions, - hasNoPermissions, - loadingRuleIds, - reFetchRules, -}: GetColumns): RulesColumns[] => { - const cols: RulesColumns[] = [ - { - field: 'name', - name: i18n.COLUMN_RULE, - render: (value: Rule['name'], item: Rule) => ( - - {value} - - ), - truncateText: true, - width: '24%', - }, - { - field: 'risk_score', - name: i18n.COLUMN_RISK_SCORE, - render: (value: Rule['risk_score']) => ( - - {value} - - ), - truncateText: true, - width: '14%', - }, - { - field: 'severity', - name: i18n.COLUMN_SEVERITY, - render: (value: Rule['severity']) => , - truncateText: true, - width: '16%', - }, - { - field: 'status_date', - name: i18n.COLUMN_LAST_COMPLETE_RUN, - render: (value: Rule['status_date']) => { - return value == null ? ( - getEmptyTagValue() - ) : ( - - - - ); - }, - truncateText: true, - width: '20%', - }, - { - field: 'status', - name: i18n.COLUMN_LAST_RESPONSE, - render: (value: Rule['status']) => { - return ( - <> - - {value ?? getEmptyTagValue()} - - - ); - }, - width: '16%', - truncateText: true, - }, - { - field: 'tags', - name: i18n.COLUMN_TAGS, - render: (value: Rule['tags']) => ( - - {value.map((tag, i) => ( - - {tag} - - ))} - - ), - truncateText: true, - width: '20%', - }, - { - align: 'center', - field: 'enabled', - name: i18n.COLUMN_ACTIVATE, - render: (value: Rule['enabled'], item: Rule) => ( - - - - ), - sortable: true, - width: '95px', - }, - ]; - const actions: RulesColumns[] = [ - { - actions: getActions(dispatch, dispatchToaster, history, reFetchRules), - width: '40px', - } as EuiTableActionsColumnType, - ]; - - return hasNoPermissions ? cols : [...cols, ...actions]; -}; - -export const getMonitoringColumns = (): RulesStatusesColumns[] => { - const cols: RulesStatusesColumns[] = [ - { - field: 'name', - name: i18n.COLUMN_RULE, - render: (value: RuleStatus['current_status']['status'], item: RuleStatusRowItemType) => { - return ( - - {value} - - ); - }, - truncateText: true, - width: '24%', - }, - { - field: 'current_status.bulk_create_time_durations', - name: i18n.COLUMN_INDEXING_TIMES, - render: (value: RuleStatus['current_status']['bulk_create_time_durations']) => ( - - {value != null && value.length > 0 - ? Math.max(...value?.map(item => Number.parseFloat(item))) - : getEmptyTagValue()} - - ), - truncateText: true, - width: '14%', - }, - { - field: 'current_status.search_after_time_durations', - name: i18n.COLUMN_QUERY_TIMES, - render: (value: RuleStatus['current_status']['search_after_time_durations']) => ( - - {value != null && value.length > 0 - ? Math.max(...value?.map(item => Number.parseFloat(item))) - : getEmptyTagValue()} - - ), - truncateText: true, - width: '14%', - }, - { - field: 'current_status.gap', - name: i18n.COLUMN_GAP, - render: (value: RuleStatus['current_status']['gap']) => ( - - {value ?? getEmptyTagValue()} - - ), - truncateText: true, - width: '14%', - }, - { - field: 'current_status.last_look_back_date', - name: i18n.COLUMN_LAST_LOOKBACK_DATE, - render: (value: RuleStatus['current_status']['last_look_back_date']) => { - return value == null ? ( - getEmptyTagValue() - ) : ( - - ); - }, - truncateText: true, - width: '16%', - }, - { - field: 'current_status.status_date', - name: i18n.COLUMN_LAST_COMPLETE_RUN, - render: (value: RuleStatus['current_status']['status_date']) => { - return value == null ? ( - getEmptyTagValue() - ) : ( - - - - ); - }, - truncateText: true, - width: '20%', - }, - { - field: 'current_status.status', - name: i18n.COLUMN_LAST_RESPONSE, - render: (value: RuleStatus['current_status']['status']) => { - return ( - <> - - {value ?? getEmptyTagValue()} - - - ); - }, - width: '16%', - truncateText: true, - }, - { - field: 'activate', - name: i18n.COLUMN_ACTIVATE, - render: (value: Rule['enabled']) => ( - - {value ? i18n.ACTIVE : i18n.INACTIVE} - - ), - width: '95px', - }, - ]; - - return cols; -}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/all/helpers.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/helpers.test.tsx deleted file mode 100644 index 062d7967bf3018..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/all/helpers.test.tsx +++ /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. - */ - -import { bucketRulesResponse, showRulesTable } from './helpers'; -import { mockRule, mockRuleError } from './__mocks__/mock'; -import uuid from 'uuid'; -import { Rule, RuleError } from '../../../../containers/detection_engine/rules'; - -describe('AllRulesTable Helpers', () => { - const mockRule1: Readonly = mockRule(uuid.v4()); - const mockRule2: Readonly = mockRule(uuid.v4()); - const mockRuleError1: Readonly = mockRuleError(uuid.v4()); - const mockRuleError2: Readonly = mockRuleError(uuid.v4()); - - describe('bucketRulesResponse', () => { - test('buckets empty response', () => { - const bucketedResponse = bucketRulesResponse([]); - expect(bucketedResponse).toEqual({ rules: [], errors: [] }); - }); - - test('buckets all error response', () => { - const bucketedResponse = bucketRulesResponse([mockRuleError1, mockRuleError2]); - expect(bucketedResponse).toEqual({ rules: [], errors: [mockRuleError1, mockRuleError2] }); - }); - - test('buckets all success response', () => { - const bucketedResponse = bucketRulesResponse([mockRule1, mockRule2]); - expect(bucketedResponse).toEqual({ rules: [mockRule1, mockRule2], errors: [] }); - }); - - test('buckets mixed success/error response', () => { - const bucketedResponse = bucketRulesResponse([ - mockRule1, - mockRuleError1, - mockRule2, - mockRuleError2, - ]); - expect(bucketedResponse).toEqual({ - rules: [mockRule1, mockRule2], - errors: [mockRuleError1, mockRuleError2], - }); - }); - }); - - describe('showRulesTable', () => { - test('returns false when rulesCustomInstalled and rulesInstalled are null', () => { - const result = showRulesTable({ - rulesCustomInstalled: null, - rulesInstalled: null, - }); - expect(result).toBeFalsy(); - }); - - test('returns false when rulesCustomInstalled and rulesInstalled are 0', () => { - const result = showRulesTable({ - rulesCustomInstalled: 0, - rulesInstalled: 0, - }); - expect(result).toBeFalsy(); - }); - - test('returns false when both rulesCustomInstalled and rulesInstalled checks return false', () => { - const result = showRulesTable({ - rulesCustomInstalled: 0, - rulesInstalled: null, - }); - expect(result).toBeFalsy(); - }); - - test('returns true if rulesCustomInstalled is not null or 0', () => { - const result = showRulesTable({ - rulesCustomInstalled: 5, - rulesInstalled: null, - }); - expect(result).toBeTruthy(); - }); - - test('returns true if rulesInstalled is not null or 0', () => { - const result = showRulesTable({ - rulesCustomInstalled: null, - rulesInstalled: 5, - }); - expect(result).toBeTruthy(); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts deleted file mode 100644 index 0ebeb84d57468c..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/all/helpers.ts +++ /dev/null @@ -1,35 +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 { - BulkRuleResponse, - RuleResponseBuckets, -} from '../../../../containers/detection_engine/rules'; - -/** - * Separates rules/errors from bulk rules API response (create/update/delete) - * - * @param response BulkRuleResponse from bulk rules API - */ -export const bucketRulesResponse = (response: BulkRuleResponse) => - response.reduce( - (acc, cv): RuleResponseBuckets => { - return 'error' in cv - ? { rules: [...acc.rules], errors: [...acc.errors, cv] } - : { rules: [...acc.rules, cv], errors: [...acc.errors] }; - }, - { rules: [], errors: [] } - ); - -export const showRulesTable = ({ - rulesCustomInstalled, - rulesInstalled, -}: { - rulesCustomInstalled: number | null; - rulesInstalled: number | null; -}) => - (rulesCustomInstalled != null && rulesCustomInstalled > 0) || - (rulesInstalled != null && rulesInstalled > 0); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/all/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/index.test.tsx deleted file mode 100644 index 59b3b02ff3587a..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/all/index.test.tsx +++ /dev/null @@ -1,230 +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 { shallow, mount } from 'enzyme'; -import { act } from 'react-dom/test-utils'; - -import { createKibanaContextProviderMock } from '../../../../mock/kibana_react'; -import { TestProviders } from '../../../../mock'; -import { wait } from '../../../../lib/helpers'; -import { AllRules } from './index'; - -jest.mock('./reducer', () => { - return { - allRulesReducer: jest.fn().mockReturnValue(() => ({ - exportRuleIds: [], - filterOptions: { - filter: 'some filter', - sortField: 'some sort field', - sortOrder: 'desc', - }, - loadingRuleIds: [], - loadingRulesAction: null, - pagination: { - page: 1, - perPage: 20, - total: 1, - }, - rules: [ - { - actions: [], - created_at: '2020-02-14T19:49:28.178Z', - created_by: 'elastic', - description: 'jibber jabber', - enabled: false, - false_positives: [], - filters: [], - from: 'now-660s', - id: 'rule-id-1', - immutable: true, - index: ['endgame-*'], - interval: '10m', - language: 'kuery', - max_signals: 100, - name: 'Credential Dumping - Detected - Elastic Endpoint', - output_index: '.siem-signals-default', - query: 'host.name:*', - references: [], - risk_score: 73, - rule_id: '571afc56-5ed9-465d-a2a9-045f099f6e7e', - severity: 'high', - tags: ['Elastic', 'Endpoint'], - threat: [], - throttle: null, - to: 'now', - type: 'query', - updated_at: '2020-02-14T19:49:28.320Z', - updated_by: 'elastic', - version: 1, - }, - ], - selectedRuleIds: [], - })), - }; -}); - -jest.mock('../../../../containers/detection_engine/rules', () => { - return { - useRules: jest.fn().mockReturnValue([ - false, - { - page: 1, - perPage: 20, - total: 1, - data: [ - { - actions: [], - created_at: '2020-02-14T19:49:28.178Z', - created_by: 'elastic', - description: 'jibber jabber', - enabled: false, - false_positives: [], - filters: [], - from: 'now-660s', - id: 'rule-id-1', - immutable: true, - index: ['endgame-*'], - interval: '10m', - language: 'kuery', - max_signals: 100, - name: 'Credential Dumping - Detected - Elastic Endpoint', - output_index: '.siem-signals-default', - query: 'host.name:*', - references: [], - risk_score: 73, - rule_id: '571afc56-5ed9-465d-a2a9-045f099f6e7e', - severity: 'high', - tags: ['Elastic', 'Endpoint'], - threat: [], - throttle: null, - to: 'now', - type: 'query', - updated_at: '2020-02-14T19:49:28.320Z', - updated_by: 'elastic', - version: 1, - }, - ], - }, - ]), - useRulesStatuses: jest.fn().mockReturnValue({ - loading: false, - rulesStatuses: [ - { - current_status: { - alert_id: 'alertId', - bulk_create_time_durations: ['2235.01'], - gap: null, - last_failure_at: null, - last_failure_message: null, - last_look_back_date: new Date().toISOString(), - last_success_at: new Date().toISOString(), - last_success_message: 'it is a success', - search_after_time_durations: ['616.97'], - status: 'succeeded', - status_date: new Date().toISOString(), - }, - failures: [], - id: '12345678987654321', - activate: true, - name: 'Test rule', - }, - ], - }), - }; -}); - -jest.mock('react-router-dom', () => { - const originalModule = jest.requireActual('react-router-dom'); - - return { - ...originalModule, - useHistory: jest.fn(), - }; -}); - -describe('AllRules', () => { - it('renders correctly', () => { - const wrapper = shallow( - - ); - - expect(wrapper.find('[title="All rules"]')).toHaveLength(1); - }); - - it('renders rules tab', async () => { - const KibanaContext = createKibanaContextProviderMock(); - const wrapper = mount( - - - - - - ); - - await act(async () => { - await wait(); - - expect(wrapper.exists('[data-test-subj="monitoring-table"]')).toBeFalsy(); - expect(wrapper.exists('[data-test-subj="rules-table"]')).toBeTruthy(); - }); - }); - - it('renders monitoring tab when monitoring tab clicked', async () => { - const KibanaContext = createKibanaContextProviderMock(); - - const wrapper = mount( - - - - - - ); - const monitoringTab = wrapper.find('[data-test-subj="allRulesTableTab-monitoring"] button'); - monitoringTab.simulate('click'); - - await act(async () => { - wrapper.update(); - await wait(); - - expect(wrapper.exists('[data-test-subj="monitoring-table"]')).toBeTruthy(); - expect(wrapper.exists('[data-test-subj="rules-table"]')).toBeFalsy(); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/all/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/index.tsx deleted file mode 100644 index 18ca4d42bd018d..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/all/index.tsx +++ /dev/null @@ -1,423 +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 { - EuiBasicTable, - EuiContextMenuPanel, - EuiLoadingContent, - EuiSpacer, - EuiTab, - EuiTabs, -} from '@elastic/eui'; -import React, { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'; -import { useHistory } from 'react-router-dom'; -import uuid from 'uuid'; - -import { - useRules, - useRulesStatuses, - CreatePreBuiltRules, - FilterOptions, - Rule, - PaginationOptions, - exportRules, -} from '../../../../containers/detection_engine/rules'; -import { HeaderSection } from '../../../../components/header_section'; -import { - UtilityBar, - UtilityBarAction, - UtilityBarGroup, - UtilityBarSection, - UtilityBarText, -} from '../../../../components/utility_bar'; -import { useStateToaster } from '../../../../components/toasters'; -import { Loader } from '../../../../components/loader'; -import { Panel } from '../../../../components/panel'; -import { PrePackagedRulesPrompt } from '../components/pre_packaged_rules/load_empty_prompt'; -import { GenericDownloader } from '../../../../components/generic_downloader'; -import { AllRulesTables, SortingType } from '../components/all_rules_tables'; -import { getPrePackagedRuleStatus } from '../helpers'; -import * as i18n from '../translations'; -import { EuiBasicTableOnChange } from '../types'; -import { getBatchItems } from './batch_actions'; -import { getColumns, getMonitoringColumns } from './columns'; -import { showRulesTable } from './helpers'; -import { allRulesReducer, State } from './reducer'; -import { RulesTableFilters } from './rules_table_filters/rules_table_filters'; -import { useMlCapabilities } from '../../../../components/ml_popover/hooks/use_ml_capabilities'; -import { hasMlAdminPermissions } from '../../../../components/ml/permissions/has_ml_admin_permissions'; - -const SORT_FIELD = 'enabled'; -const initialState: State = { - exportRuleIds: [], - filterOptions: { - filter: '', - sortField: SORT_FIELD, - sortOrder: 'desc', - }, - loadingRuleIds: [], - loadingRulesAction: null, - pagination: { - page: 1, - perPage: 20, - total: 0, - }, - rules: [], - selectedRuleIds: [], -}; - -interface AllRulesProps { - createPrePackagedRules: CreatePreBuiltRules | null; - hasNoPermissions: boolean; - loading: boolean; - loadingCreatePrePackagedRules: boolean; - refetchPrePackagedRulesStatus: () => void; - rulesCustomInstalled: number | null; - rulesInstalled: number | null; - rulesNotInstalled: number | null; - rulesNotUpdated: number | null; - setRefreshRulesData: (refreshRule: (refreshPrePackagedRule?: boolean) => void) => void; -} - -export enum AllRulesTabs { - rules = 'rules', - monitoring = 'monitoring', -} - -const allRulesTabs = [ - { - id: AllRulesTabs.rules, - name: i18n.RULES_TAB, - disabled: false, - }, - { - id: AllRulesTabs.monitoring, - name: i18n.MONITORING_TAB, - disabled: false, - }, -]; - -/** - * Table Component for displaying all Rules for a given cluster. Provides the ability to filter - * by name, sort by enabled, and perform the following actions: - * * Enable/Disable - * * Duplicate - * * Delete - * * Import/Export - */ -export const AllRules = React.memo( - ({ - createPrePackagedRules, - hasNoPermissions, - loading, - loadingCreatePrePackagedRules, - refetchPrePackagedRulesStatus, - rulesCustomInstalled, - rulesInstalled, - rulesNotInstalled, - rulesNotUpdated, - setRefreshRulesData, - }) => { - const [initLoading, setInitLoading] = useState(true); - const tableRef = useRef(); - const [ - { - exportRuleIds, - filterOptions, - loadingRuleIds, - loadingRulesAction, - pagination, - rules, - selectedRuleIds, - }, - dispatch, - ] = useReducer(allRulesReducer(tableRef), initialState); - const { loading: isLoadingRulesStatuses, rulesStatuses } = useRulesStatuses(rules); - const history = useHistory(); - const [, dispatchToaster] = useStateToaster(); - const mlCapabilities = useMlCapabilities(); - const [allRulesTab, setAllRulesTab] = useState(AllRulesTabs.rules); - - // TODO: Refactor license check + hasMlAdminPermissions to common check - const hasMlPermissions = - mlCapabilities.isPlatinumOrTrialLicense && hasMlAdminPermissions(mlCapabilities); - - const setRules = useCallback((newRules: Rule[], newPagination: Partial) => { - dispatch({ - type: 'setRules', - rules: newRules, - pagination: newPagination, - }); - }, []); - - const [isLoadingRules, , reFetchRulesData] = useRules({ - pagination, - filterOptions, - refetchPrePackagedRulesStatus, - dispatchRulesInReducer: setRules, - }); - - const sorting = useMemo( - (): SortingType => ({ sort: { field: 'enabled', direction: filterOptions.sortOrder } }), - [filterOptions.sortOrder] - ); - - const prePackagedRuleStatus = getPrePackagedRuleStatus( - rulesInstalled, - rulesNotInstalled, - rulesNotUpdated - ); - - const getBatchItemsPopoverContent = useCallback( - (closePopover: () => void) => ( - - ), - [ - dispatch, - dispatchToaster, - hasMlPermissions, - loadingRuleIds, - reFetchRulesData, - rules, - selectedRuleIds, - ] - ); - - const paginationMemo = useMemo( - () => ({ - pageIndex: pagination.page - 1, - pageSize: pagination.perPage, - totalItemCount: pagination.total, - pageSizeOptions: [5, 10, 20, 50, 100, 200, 300], - }), - [pagination] - ); - - const tableOnChangeCallback = useCallback( - ({ page, sort }: EuiBasicTableOnChange) => { - dispatch({ - type: 'updateFilterOptions', - filterOptions: { - sortField: SORT_FIELD, // Only enabled is supported for sorting currently - sortOrder: sort?.direction ?? 'desc', - }, - pagination: { page: page.index + 1, perPage: page.size }, - }); - }, - [dispatch] - ); - - const rulesColumns = useMemo(() => { - return getColumns({ - dispatch, - dispatchToaster, - history, - hasMlPermissions, - hasNoPermissions, - loadingRuleIds: - loadingRulesAction != null && - (loadingRulesAction === 'enable' || loadingRulesAction === 'disable') - ? loadingRuleIds - : [], - reFetchRules: reFetchRulesData, - }); - }, [ - dispatch, - dispatchToaster, - hasMlPermissions, - history, - loadingRuleIds, - loadingRulesAction, - reFetchRulesData, - ]); - - const monitoringColumns = useMemo(() => getMonitoringColumns(), []); - - useEffect(() => { - if (reFetchRulesData != null) { - setRefreshRulesData(reFetchRulesData); - } - }, [reFetchRulesData, setRefreshRulesData]); - - useEffect(() => { - if (initLoading && !loading && !isLoadingRules && !isLoadingRulesStatuses) { - setInitLoading(false); - } - }, [initLoading, loading, isLoadingRules, isLoadingRulesStatuses]); - - const handleCreatePrePackagedRules = useCallback(async () => { - if (createPrePackagedRules != null && reFetchRulesData != null) { - await createPrePackagedRules(); - reFetchRulesData(true); - } - }, [createPrePackagedRules, reFetchRulesData]); - - const euiBasicTableSelectionProps = useMemo( - () => ({ - selectable: (item: Rule) => !loadingRuleIds.includes(item.id), - onSelectionChange: (selected: Rule[]) => - dispatch({ type: 'selectedRuleIds', ids: selected.map(r => r.id) }), - }), - [loadingRuleIds] - ); - - const onFilterChangedCallback = useCallback((newFilterOptions: Partial) => { - dispatch({ - type: 'updateFilterOptions', - filterOptions: { - ...newFilterOptions, - }, - pagination: { page: 1 }, - }); - }, []); - - const isLoadingAnActionOnRule = useMemo(() => { - if ( - loadingRuleIds.length > 0 && - (loadingRulesAction === 'disable' || loadingRulesAction === 'enable') - ) { - return false; - } else if (loadingRuleIds.length > 0) { - return true; - } - return false; - }, [loadingRuleIds, loadingRulesAction]); - - const tabs = useMemo( - () => ( - - {allRulesTabs.map(tab => ( - setAllRulesTab(tab.id)} - isSelected={tab.id === allRulesTab} - disabled={tab.disabled} - key={tab.id} - > - {tab.name} - - ))} - - ), - [allRulesTabs, allRulesTab, setAllRulesTab] - ); - - return ( - <> - { - dispatch({ type: 'loadingRuleIds', ids: [], actionType: null }); - dispatchToaster({ - type: 'addToaster', - toast: { - id: uuid.v4(), - title: i18n.SUCCESSFULLY_EXPORTED_RULES(exportCount), - color: 'success', - iconType: 'check', - }, - }); - }} - exportSelectedData={exportRules} - /> - - {tabs} - - - - <> - - - - - {(loading || isLoadingRules || isLoadingAnActionOnRule || isLoadingRulesStatuses) && - !initLoading && ( - - )} - {rulesCustomInstalled != null && - rulesCustomInstalled === 0 && - prePackagedRuleStatus === 'ruleNotInstalled' && - !initLoading && ( - - )} - {initLoading && ( - - )} - {showRulesTable({ rulesCustomInstalled, rulesInstalled }) && !initLoading && ( - <> - - - - - {i18n.SHOWING_RULES(pagination.total ?? 0)} - - - - - {i18n.SELECTED_RULES(selectedRuleIds.length)} - {!hasNoPermissions && ( - - {i18n.BATCH_ACTIONS} - - )} - reFetchRulesData(true)} - > - {i18n.REFRESH} - - - - - - - )} - - - - ); - } -); - -AllRules.displayName = 'AllRules'; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/all/reducer.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/all/reducer.ts deleted file mode 100644 index bc5297e7628b7f..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/all/reducer.ts +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiBasicTable } from '@elastic/eui'; -import { - FilterOptions, - PaginationOptions, - Rule, -} from '../../../../containers/detection_engine/rules'; - -type LoadingRuleAction = 'duplicate' | 'enable' | 'disable' | 'export' | 'delete' | null; -export interface State { - exportRuleIds: string[]; - filterOptions: FilterOptions; - loadingRuleIds: string[]; - loadingRulesAction: LoadingRuleAction; - pagination: PaginationOptions; - rules: Rule[]; - selectedRuleIds: string[]; -} - -export type Action = - | { type: 'exportRuleIds'; ids: string[] } - | { type: 'loadingRuleIds'; ids: string[]; actionType: LoadingRuleAction } - | { type: 'selectedRuleIds'; ids: string[] } - | { type: 'setRules'; rules: Rule[]; pagination: Partial } - | { type: 'updateRules'; rules: Rule[] } - | { - type: 'updateFilterOptions'; - filterOptions: Partial; - pagination: Partial; - } - | { type: 'failure' }; - -export const allRulesReducer = ( - tableRef: React.MutableRefObject | undefined> -) => (state: State, action: Action): State => { - switch (action.type) { - case 'exportRuleIds': { - return { - ...state, - loadingRuleIds: action.ids, - loadingRulesAction: 'export', - exportRuleIds: action.ids, - }; - } - case 'loadingRuleIds': { - return { - ...state, - loadingRuleIds: action.actionType == null ? [] : [...state.loadingRuleIds, ...action.ids], - loadingRulesAction: action.actionType, - }; - } - case 'selectedRuleIds': { - return { - ...state, - selectedRuleIds: action.ids, - }; - } - case 'setRules': { - if ( - tableRef != null && - tableRef.current != null && - tableRef.current.changeSelection != null - ) { - // for future devs: eui basic table is not giving us a prop to set the value, so - // we are using the ref in setTimeout to reset on the next loop so that we - // do not get a warning telling us we are trying to update during a render - window.setTimeout(() => tableRef?.current?.changeSelection([]), 0); - } - - return { - ...state, - rules: action.rules, - selectedRuleIds: [], - loadingRuleIds: [], - loadingRulesAction: null, - pagination: { - ...state.pagination, - ...action.pagination, - }, - }; - } - case 'updateRules': { - if (state.rules != null) { - const ruleIds = state.rules.map(r => r.id); - const updatedRules = action.rules.reduce((rules, updatedRule) => { - let newRules = rules; - if (ruleIds.includes(updatedRule.id)) { - newRules = newRules.map(r => (updatedRule.id === r.id ? updatedRule : r)); - } else { - newRules = [...newRules, updatedRule]; - } - return newRules; - }, state.rules); - const updatedRuleIds = action.rules.map(r => r.id); - const newLoadingRuleIds = state.loadingRuleIds.filter(id => !updatedRuleIds.includes(id)); - return { - ...state, - rules: updatedRules, - loadingRuleIds: newLoadingRuleIds, - loadingRulesAction: newLoadingRuleIds.length === 0 ? null : state.loadingRulesAction, - }; - } - return state; - } - case 'updateFilterOptions': { - return { - ...state, - filterOptions: { - ...state.filterOptions, - ...action.filterOptions, - }, - pagination: { - ...state.pagination, - ...action.pagination, - }, - }; - } - case 'failure': { - return { - ...state, - rules: [], - }; - } - default: - return state; - } -}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.test.tsx deleted file mode 100644 index eafa89a33f5964..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.test.tsx +++ /dev/null @@ -1,32 +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 { shallow } from 'enzyme'; - -import { AddItem } from './index'; -import { useFormFieldMock } from '../../../../../../public/mock/test_providers'; - -describe('AddItem', () => { - it('renders correctly', () => { - const Component = () => { - const field = useFormFieldMock(); - - return ( - - ); - }; - const wrapper = shallow(); - - expect(wrapper.dive().find('[iconType="plusInCircle"]')).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx deleted file mode 100644 index abbaa6d6192eed..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/add_item_form/index.tsx +++ /dev/null @@ -1,188 +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 { - EuiButtonEmpty, - EuiButtonIcon, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiFieldText, - EuiSpacer, -} from '@elastic/eui'; -import { isEmpty } from 'lodash/fp'; -import React, { ChangeEvent, useCallback, useEffect, useState, useRef } from 'react'; -import styled from 'styled-components'; - -import * as RuleI18n from '../../translations'; -import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../../shared_imports'; - -interface AddItemProps { - addText: string; - field: FieldHook; - dataTestSubj: string; - idAria: string; - isDisabled: boolean; - validate?: (args: unknown) => boolean; -} - -const MyEuiFormRow = styled(EuiFormRow)` - .euiFormRow__labelWrapper { - .euiText { - padding-right: 32px; - } - } -`; - -export const MyAddItemButton = styled(EuiButtonEmpty)` - margin-top: 4px; - - &.euiButtonEmpty--xSmall { - font-size: 12px; - } - - .euiIcon { - width: 12px; - height: 12px; - } -`; - -MyAddItemButton.defaultProps = { - flush: 'left', - iconType: 'plusInCircle', - size: 'xs', -}; - -export const AddItem = ({ - addText, - dataTestSubj, - field, - idAria, - isDisabled, - validate, -}: AddItemProps) => { - const [showValidation, setShowValidation] = useState(false); - const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); - const [haveBeenKeyboardDeleted, setHaveBeenKeyboardDeleted] = useState(-1); - - const inputsRef = useRef([]); - - const removeItem = useCallback( - (index: number) => { - const values = field.value as string[]; - const newValues = [...values.slice(0, index), ...values.slice(index + 1)]; - field.setValue(newValues.length === 0 ? [''] : newValues); - inputsRef.current = [ - ...inputsRef.current.slice(0, index), - ...inputsRef.current.slice(index + 1), - ]; - inputsRef.current = inputsRef.current.map((ref, i) => { - if (i >= index && inputsRef.current[index] != null) { - ref.value = 're-render'; - } - return ref; - }); - }, - [field] - ); - - const addItem = useCallback(() => { - const values = field.value as string[]; - field.setValue([...values, '']); - }, [field]); - - const updateItem = useCallback( - (event: ChangeEvent, index: number) => { - event.persist(); - const values = field.value as string[]; - const value = event.target.value; - field.setValue([...values.slice(0, index), value, ...values.slice(index + 1)]); - }, - [field] - ); - - const handleLastInputRef = useCallback( - (index: number, element: HTMLInputElement | null) => { - if (element != null) { - inputsRef.current = [ - ...inputsRef.current.slice(0, index), - element, - ...inputsRef.current.slice(index + 1), - ]; - } - }, - [inputsRef] - ); - - useEffect(() => { - if ( - haveBeenKeyboardDeleted !== -1 && - !isEmpty(inputsRef.current) && - inputsRef.current[haveBeenKeyboardDeleted] != null - ) { - inputsRef.current[haveBeenKeyboardDeleted].focus(); - setHaveBeenKeyboardDeleted(-1); - } - }, [haveBeenKeyboardDeleted, inputsRef.current]); - - const values = field.value as string[]; - return ( - - <> - {values.map((item, index) => { - const euiFieldProps = { - disabled: isDisabled, - ...(index === values.length - 1 - ? { inputRef: handleLastInputRef.bind(null, index) } - : {}), - ...((inputsRef.current[index] != null && inputsRef.current[index].value !== item) || - inputsRef.current[index] == null - ? { value: item } - : {}), - isInvalid: validate == null ? false : showValidation && validate(item), - }; - return ( -
- - - setShowValidation(true)} - onChange={e => updateItem(e, index)} - fullWidth - {...euiFieldProps} - /> - - - removeItem(index)} - aria-label={RuleI18n.DELETE} - /> - - - - {values.length - 1 !== index && } -
- ); - })} - - - {addText} - - -
- ); -}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.test.tsx deleted file mode 100644 index 8afb8db0c8d5b4..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.test.tsx +++ /dev/null @@ -1,120 +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, { useRef } from 'react'; -import { shallow } from 'enzyme'; - -import { AllRulesTables } from './index'; -import { AllRulesTabs } from '../../all'; - -describe('AllRulesTables', () => { - it('renders correctly', () => { - const Component = () => { - const ref = useRef(); - - return ( - - ); - }; - const wrapper = shallow(); - - expect(wrapper.dive().find('[data-test-subj="rules-table"]')).toHaveLength(1); - }); - - it('renders rules tab when "selectedTab" is "rules"', () => { - const Component = () => { - const ref = useRef(); - - return ( - - ); - }; - const wrapper = shallow(); - - expect(wrapper.dive().find('[data-test-subj="rules-table"]')).toHaveLength(1); - expect(wrapper.dive().find('[data-test-subj="monitoring-table"]')).toHaveLength(0); - }); - - it('renders monitoring tab when "selectedTab" is "monitoring"', () => { - const Component = () => { - const ref = useRef(); - - return ( - - ); - }; - const wrapper = shallow(); - - expect(wrapper.dive().find('[data-test-subj="rules-table"]')).toHaveLength(0); - expect(wrapper.dive().find('[data-test-subj="monitoring-table"]')).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.tsx deleted file mode 100644 index 8ea5606d0082c0..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/all_rules_tables/index.tsx +++ /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 { - EuiBasicTable, - EuiBasicTableColumn, - EuiEmptyPrompt, - Direction, - EuiTableSelectionType, -} from '@elastic/eui'; -import React, { useMemo, memo } from 'react'; -import styled from 'styled-components'; - -import { EuiBasicTableOnChange } from '../../types'; -import * as i18n from '../../translations'; -import { - RulesColumns, - RuleStatusRowItemType, -} from '../../../../../pages/detection_engine/rules/all/columns'; -import { Rule, Rules } from '../../../../../containers/detection_engine/rules'; -import { AllRulesTabs } from '../../all'; - -// EuiBasicTable give me a hardtime with adding the ref attributes so I went the easy way -// after few hours of fight with typescript !!!! I lost :( -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const MyEuiBasicTable = styled(EuiBasicTable as any)`` as any; - -export interface SortingType { - sort: { - field: 'enabled'; - direction: Direction; - }; -} - -interface AllRulesTablesProps { - euiBasicTableSelectionProps: EuiTableSelectionType; - hasNoPermissions: boolean; - monitoringColumns: Array>; - pagination: { - pageIndex: number; - pageSize: number; - totalItemCount: number; - pageSizeOptions: number[]; - }; - rules: Rules; - rulesColumns: RulesColumns[]; - rulesStatuses: RuleStatusRowItemType[]; - sorting: { - sort: { - field: 'enabled'; - direction: Direction; - }; - }; - tableOnChangeCallback: ({ page, sort }: EuiBasicTableOnChange) => void; - tableRef?: React.MutableRefObject; - selectedTab: AllRulesTabs; -} - -export const AllRulesTablesComponent: React.FC = ({ - euiBasicTableSelectionProps, - hasNoPermissions, - monitoringColumns, - pagination, - rules, - rulesColumns, - rulesStatuses, - sorting, - tableOnChangeCallback, - tableRef, - selectedTab, -}) => { - const emptyPrompt = useMemo(() => { - return ( - {i18n.NO_RULES}} titleSize="xs" body={i18n.NO_RULES_BODY} /> - ); - }, []); - - return ( - <> - {selectedTab === AllRulesTabs.rules && ( - - )} - {selectedTab === AllRulesTabs.monitoring && ( - - )} - - ); -}; - -export const AllRulesTables = memo(AllRulesTablesComponent); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.test.tsx deleted file mode 100644 index c0e957d94261fe..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.test.tsx +++ /dev/null @@ -1,24 +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 { shallow } from 'enzyme'; - -import { AnomalyThresholdSlider } from './index'; -import { useFormFieldMock } from '../../../../../mock'; - -describe('AnomalyThresholdSlider', () => { - it('renders correctly', () => { - const Component = () => { - const field = useFormFieldMock(); - - return ; - }; - const wrapper = shallow(); - - expect(wrapper.dive().find('EuiRange')).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.tsx deleted file mode 100644 index 01fddf98b97d83..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/anomaly_threshold_slider/index.tsx +++ /dev/null @@ -1,53 +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, { useCallback } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiRange, EuiFormRow } from '@elastic/eui'; - -import { FieldHook } from '../../../../../shared_imports'; - -interface AnomalyThresholdSliderProps { - describedByIds: string[]; - field: FieldHook; -} -type Event = React.ChangeEvent; -type EventArg = Event | React.MouseEvent; - -export const AnomalyThresholdSlider = ({ - describedByIds = [], - field, -}: AnomalyThresholdSliderProps) => { - const threshold = field.value as number; - const onThresholdChange = useCallback( - (event: EventArg) => { - const thresholdValue = Number((event as Event).target.value); - field.setValue(thresholdValue); - }, - [field] - ); - - return ( - - - - - - - - ); -}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.test.tsx deleted file mode 100644 index 186aeae42246d3..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.test.tsx +++ /dev/null @@ -1,415 +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 { shallow } from 'enzyme'; -import { EuiLoadingSpinner } from '@elastic/eui'; - -import { coreMock } from '../../../../../../../../../src/core/public/mocks'; -import { esFilters, FilterManager } from '../../../../../../../../../src/plugins/data/public'; -import { SeverityBadge } from '../severity_badge'; - -import * as i18n from './translations'; -import { - isNotEmptyArray, - buildQueryBarDescription, - buildThreatDescription, - buildUnorderedListArrayDescription, - buildStringArrayDescription, - buildSeverityDescription, - buildUrlsDescription, - buildNoteDescription, - buildRuleTypeDescription, -} from './helpers'; -import { ListItems } from './types'; - -const setupMock = coreMock.createSetup(); -const uiSettingsMock = (pinnedByDefault: boolean) => (key: string) => { - switch (key) { - case 'filters:pinnedByDefault': - return pinnedByDefault; - default: - throw new Error(`Unexpected uiSettings key in FilterManager mock: ${key}`); - } -}; -setupMock.uiSettings.get.mockImplementation(uiSettingsMock(true)); -const mockFilterManager = new FilterManager(setupMock.uiSettings); - -const mockQueryBar = { - query: 'test query', - filters: [ - { - $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, - }, - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - ], - saved_id: 'test123', -}; - -describe('helpers', () => { - describe('isNotEmptyArray', () => { - test('returns false if empty array', () => { - const result = isNotEmptyArray([]); - expect(result).toBeFalsy(); - }); - - test('returns false if array of empty strings', () => { - const result = isNotEmptyArray(['', '']); - expect(result).toBeFalsy(); - }); - - test('returns true if array of string with space', () => { - const result = isNotEmptyArray([' ']); - expect(result).toBeTruthy(); - }); - - test('returns true if array with at least one non-empty string', () => { - const result = isNotEmptyArray(['', 'abc']); - expect(result).toBeTruthy(); - }); - }); - - describe('buildQueryBarDescription', () => { - test('returns empty array if no filters, query or savedId exist', () => { - const emptyMockQueryBar = { - query: '', - filters: [], - saved_id: '', - }; - const result: ListItems[] = buildQueryBarDescription({ - field: 'queryBar', - filters: emptyMockQueryBar.filters, - filterManager: mockFilterManager, - query: emptyMockQueryBar.query, - savedId: emptyMockQueryBar.saved_id, - }); - expect(result).toEqual([]); - }); - - test('returns expected array of ListItems when filters exists, but no indexPatterns passed in', () => { - const mockQueryBarWithFilters = { - ...mockQueryBar, - query: '', - saved_id: '', - }; - const result: ListItems[] = buildQueryBarDescription({ - field: 'queryBar', - filters: mockQueryBarWithFilters.filters, - filterManager: mockFilterManager, - query: mockQueryBarWithFilters.query, - savedId: mockQueryBarWithFilters.saved_id, - }); - const wrapper = shallow(result[0].description as React.ReactElement); - - expect(result[0].title).toEqual(<>{i18n.FILTERS_LABEL} ); - expect(wrapper.find(EuiLoadingSpinner).exists()).toBeTruthy(); - }); - - test('returns expected array of ListItems when filters AND indexPatterns exist', () => { - const mockQueryBarWithFilters = { - ...mockQueryBar, - query: '', - saved_id: '', - }; - const result: ListItems[] = buildQueryBarDescription({ - field: 'queryBar', - filters: mockQueryBarWithFilters.filters, - filterManager: mockFilterManager, - query: mockQueryBarWithFilters.query, - savedId: mockQueryBarWithFilters.saved_id, - indexPatterns: { fields: [{ name: 'test name', type: 'test type' }], title: 'test title' }, - }); - const wrapper = shallow(result[0].description as React.ReactElement); - const filterLabelComponent = wrapper.find(esFilters.FilterLabel).at(0); - - expect(result[0].title).toEqual(<>{i18n.FILTERS_LABEL} ); - expect(filterLabelComponent.prop('valueLabel')).toEqual('file'); - expect(filterLabelComponent.prop('filter')).toEqual(mockQueryBar.filters[0]); - }); - - test('returns expected array of ListItems when "query.query" exists', () => { - const mockQueryBarWithQuery = { - ...mockQueryBar, - filters: [], - saved_id: '', - }; - const result: ListItems[] = buildQueryBarDescription({ - field: 'queryBar', - filters: mockQueryBarWithQuery.filters, - filterManager: mockFilterManager, - query: mockQueryBarWithQuery.query, - savedId: mockQueryBarWithQuery.saved_id, - }); - expect(result[0].title).toEqual(<>{i18n.QUERY_LABEL} ); - expect(result[0].description).toEqual(<>{mockQueryBarWithQuery.query} ); - }); - - test('returns expected array of ListItems when "savedId" exists', () => { - const mockQueryBarWithSavedId = { - ...mockQueryBar, - query: '', - filters: [], - }; - const result: ListItems[] = buildQueryBarDescription({ - field: 'queryBar', - filters: mockQueryBarWithSavedId.filters, - filterManager: mockFilterManager, - query: mockQueryBarWithSavedId.query, - savedId: mockQueryBarWithSavedId.saved_id, - }); - expect(result[0].title).toEqual(<>{i18n.SAVED_ID_LABEL} ); - expect(result[0].description).toEqual(<>{mockQueryBarWithSavedId.saved_id} ); - }); - }); - - describe('buildThreatDescription', () => { - test('returns empty array if no threats', () => { - const result: ListItems[] = buildThreatDescription({ label: 'Mitre Attack', threat: [] }); - expect(result).toHaveLength(0); - }); - - test('returns empty tactic link if no corresponding tactic id found', () => { - const result: ListItems[] = buildThreatDescription({ - label: 'Mitre Attack', - threat: [ - { - framework: 'MITRE ATTACK', - technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }], - tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA000999' }, - }, - ], - }); - const wrapper = shallow(result[0].description as React.ReactElement); - expect(result[0].title).toEqual('Mitre Attack'); - expect(wrapper.find('[data-test-subj="threatTacticLink"]').text()).toEqual(''); - expect(wrapper.find('[data-test-subj="threatTechniqueLink"]').text()).toEqual( - 'Audio Capture (T1123)' - ); - }); - - test('returns empty technique link if no corresponding technique id found', () => { - const result: ListItems[] = buildThreatDescription({ - label: 'Mitre Attack', - threat: [ - { - framework: 'MITRE ATTACK', - technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123456' }], - tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA0009' }, - }, - ], - }); - const wrapper = shallow(result[0].description as React.ReactElement); - expect(result[0].title).toEqual('Mitre Attack'); - expect(wrapper.find('[data-test-subj="threatTacticLink"]').text()).toEqual( - 'Collection (TA0009)' - ); - expect(wrapper.find('[data-test-subj="threatTechniqueLink"]').text()).toEqual(''); - }); - - test('returns with corresponding tactic and technique link text', () => { - const result: ListItems[] = buildThreatDescription({ - label: 'Mitre Attack', - threat: [ - { - framework: 'MITRE ATTACK', - technique: [{ reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }], - tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA0009' }, - }, - ], - }); - const wrapper = shallow(result[0].description as React.ReactElement); - expect(result[0].title).toEqual('Mitre Attack'); - expect(wrapper.find('[data-test-subj="threatTacticLink"]').text()).toEqual( - 'Collection (TA0009)' - ); - expect(wrapper.find('[data-test-subj="threatTechniqueLink"]').text()).toEqual( - 'Audio Capture (T1123)' - ); - }); - - test('returns corresponding number of tactic and technique links', () => { - const result: ListItems[] = buildThreatDescription({ - label: 'Mitre Attack', - threat: [ - { - framework: 'MITRE ATTACK', - technique: [ - { reference: 'https://test.com', name: 'Audio Capture', id: 'T1123' }, - { reference: 'https://test.com', name: 'Clipboard Data', id: 'T1115' }, - ], - tactic: { reference: 'https://test.com', name: 'Collection', id: 'TA0009' }, - }, - { - framework: 'MITRE ATTACK', - technique: [ - { reference: 'https://test.com', name: 'Automated Collection', id: 'T1119' }, - ], - tactic: { reference: 'https://test.com', name: 'Discovery', id: 'TA0007' }, - }, - ], - }); - const wrapper = shallow(result[0].description as React.ReactElement); - - expect(wrapper.find('[data-test-subj="threatTacticLink"]')).toHaveLength(2); - expect(wrapper.find('[data-test-subj="threatTechniqueLink"]')).toHaveLength(3); - }); - }); - - describe('buildUnorderedListArrayDescription', () => { - test('returns empty array if "values" is empty array', () => { - const result: ListItems[] = buildUnorderedListArrayDescription( - 'Test label', - 'falsePositives', - [] - ); - expect(result).toHaveLength(0); - }); - - test('returns ListItem with corresponding number of valid values items', () => { - const result: ListItems[] = buildUnorderedListArrayDescription( - 'Test label', - 'falsePositives', - ['', 'falsePositive1', 'falsePositive2'] - ); - const wrapper = shallow(result[0].description as React.ReactElement); - - expect(result[0].title).toEqual('Test label'); - expect(wrapper.find('[data-test-subj="unorderedListArrayDescriptionItem"]')).toHaveLength(2); - }); - }); - - describe('buildStringArrayDescription', () => { - test('returns empty array if "values" is empty array', () => { - const result: ListItems[] = buildStringArrayDescription('Test label', 'tags', []); - expect(result).toHaveLength(0); - }); - - test('returns ListItem with corresponding number of valid values items', () => { - const result: ListItems[] = buildStringArrayDescription('Test label', 'tags', [ - '', - 'tag1', - 'tag2', - ]); - const wrapper = shallow(result[0].description as React.ReactElement); - - expect(result[0].title).toEqual('Test label'); - expect(wrapper.find('[data-test-subj="stringArrayDescriptionBadgeItem"]')).toHaveLength(2); - expect( - wrapper - .find('[data-test-subj="stringArrayDescriptionBadgeItem"]') - .first() - .text() - ).toEqual('tag1'); - expect( - wrapper - .find('[data-test-subj="stringArrayDescriptionBadgeItem"]') - .at(1) - .text() - ).toEqual('tag2'); - }); - }); - - describe('buildSeverityDescription', () => { - test('returns ListItem with passed in label and SeverityBadge component', () => { - const result: ListItems[] = buildSeverityDescription('Test label', 'Test description value'); - - expect(result[0].title).toEqual('Test label'); - expect(result[0].description).toEqual(); - }); - }); - - describe('buildUrlsDescription', () => { - test('returns empty array if "values" is empty array', () => { - const result: ListItems[] = buildUrlsDescription('Test label', []); - expect(result).toHaveLength(0); - }); - - test('returns ListItem with corresponding number of valid values items', () => { - const result: ListItems[] = buildUrlsDescription('Test label', [ - 'www.test.com', - 'www.test2.com', - ]); - const wrapper = shallow(result[0].description as React.ReactElement); - - expect(result[0].title).toEqual('Test label'); - expect(wrapper.find('[data-test-subj="urlsDescriptionReferenceLinkItem"]')).toHaveLength(2); - expect( - wrapper - .find('[data-test-subj="urlsDescriptionReferenceLinkItem"]') - .first() - .text() - ).toEqual('www.test.com'); - expect( - wrapper - .find('[data-test-subj="urlsDescriptionReferenceLinkItem"]') - .at(1) - .text() - ).toEqual('www.test2.com'); - }); - }); - - describe('buildNoteDescription', () => { - test('returns ListItem with passed in label and note content', () => { - const noteSample = - 'Cras mattism. [Pellentesque](https://elastic.co). ### Malesuada adipiscing tristique'; - const result: ListItems[] = buildNoteDescription('Test label', noteSample); - const wrapper = shallow(result[0].description as React.ReactElement); - const noteElement = wrapper.find('[data-test-subj="noteDescriptionItem"]').at(0); - - expect(result[0].title).toEqual('Test label'); - expect(noteElement.exists()).toBeTruthy(); - expect(noteElement.text()).toEqual(noteSample); - }); - - test('returns empty array if passed in note is empty string', () => { - const result: ListItems[] = buildNoteDescription('Test label', ''); - - expect(result).toHaveLength(0); - }); - }); - - describe('buildRuleTypeDescription', () => { - it('returns the label for a machine_learning type', () => { - const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'machine_learning'); - - expect(result.title).toEqual('Test label'); - }); - - it('returns a humanized description for a machine_learning type', () => { - const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'machine_learning'); - - expect(result.description).toEqual('Machine Learning'); - }); - - it('returns the label for a query type', () => { - const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'query'); - - expect(result.title).toEqual('Test label'); - }); - - it('returns a humanized description for a query type', () => { - const [result]: ListItems[] = buildRuleTypeDescription('Test label', 'query'); - - expect(result.description).toEqual('Query'); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx deleted file mode 100644 index 1ac371a3f68297..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/helpers.tsx +++ /dev/null @@ -1,294 +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 { - EuiBadge, - EuiLoadingSpinner, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiSpacer, - EuiLink, - EuiText, -} from '@elastic/eui'; - -import { isEmpty } from 'lodash/fp'; -import React from 'react'; -import styled from 'styled-components'; - -import { RuleType } from '../../../../../../common/detection_engine/types'; -import { esFilters } from '../../../../../../../../../src/plugins/data/public'; - -import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_techniques'; - -import * as i18n from './translations'; -import { BuildQueryBarDescription, BuildThreatDescription, ListItems } from './types'; -import { SeverityBadge } from '../severity_badge'; -import ListTreeIcon from './assets/list_tree_icon.svg'; -import { assertUnreachable } from '../../../../../lib/helpers'; - -const NoteDescriptionContainer = styled(EuiFlexItem)` - height: 105px; - overflow-y: hidden; -`; - -export const isNotEmptyArray = (values: string[]) => !isEmpty(values.join('')); - -const EuiBadgeWrap = (styled(EuiBadge)` - .euiBadge__text { - white-space: pre-wrap !important; - } -` as unknown) as typeof EuiBadge; - -export const buildQueryBarDescription = ({ - field, - filters, - filterManager, - query, - savedId, - indexPatterns, -}: BuildQueryBarDescription): ListItems[] => { - let items: ListItems[] = []; - if (!isEmpty(filters)) { - filterManager.setFilters(filters); - items = [ - ...items, - { - title: <>{i18n.FILTERS_LABEL} , - description: ( - - {filterManager.getFilters().map((filter, index) => ( - - - {indexPatterns != null ? ( - - ) : ( - - )} - - - ))} - - ), - }, - ]; - } - if (!isEmpty(query)) { - items = [ - ...items, - { - title: <>{i18n.QUERY_LABEL} , - description: <>{query} , - }, - ]; - } - if (!isEmpty(savedId)) { - items = [ - ...items, - { - title: <>{i18n.SAVED_ID_LABEL} , - description: <>{savedId} , - }, - ]; - } - return items; -}; - -const ThreatEuiFlexGroup = styled(EuiFlexGroup)` - .euiFlexItem { - margin-bottom: 0px; - } -`; - -const TechniqueLinkItem = styled(EuiButtonEmpty)` - .euiIcon { - width: 8px; - height: 8px; - } -`; - -export const buildThreatDescription = ({ label, threat }: BuildThreatDescription): ListItems[] => { - if (threat.length > 0) { - return [ - { - title: label, - description: ( - - {threat.map((singleThreat, index) => { - const tactic = tacticsOptions.find(t => t.id === singleThreat.tactic.id); - return ( - - - {tactic != null ? tactic.text : ''} - - - {singleThreat.technique.map(technique => { - const myTechnique = techniquesOptions.find(t => t.id === technique.id); - return ( - - - {myTechnique != null ? myTechnique.label : ''} - - - ); - })} - - - ); - })} - - - ), - }, - ]; - } - return []; -}; - -export const buildUnorderedListArrayDescription = ( - label: string, - field: string, - values: string[] -): ListItems[] => { - if (isNotEmptyArray(values)) { - return [ - { - title: label, - description: ( - -
    - {values.map(val => - isEmpty(val) ? null : ( -
  • - {val} -
  • - ) - )} -
-
- ), - }, - ]; - } - return []; -}; - -export const buildStringArrayDescription = ( - label: string, - field: string, - values: string[] -): ListItems[] => { - if (isNotEmptyArray(values)) { - return [ - { - title: label, - description: ( - - {values.map((val: string) => - isEmpty(val) ? null : ( - - - {val} - - - ) - )} - - ), - }, - ]; - } - return []; -}; - -export const buildSeverityDescription = (label: string, value: string): ListItems[] => [ - { - title: label, - description: , - }, -]; - -export const buildUrlsDescription = (label: string, values: string[]): ListItems[] => { - if (isNotEmptyArray(values)) { - return [ - { - title: label, - description: ( - -
    - {values - .filter(v => !isEmpty(v)) - .map((val, index) => ( -
  • - - {val} - -
  • - ))} -
-
- ), - }, - ]; - } - return []; -}; - -export const buildNoteDescription = (label: string, note: string): ListItems[] => { - if (note.trim() !== '') { - return [ - { - title: label, - description: ( - -
- {note} -
-
- ), - }, - ]; - } - return []; -}; - -export const buildRuleTypeDescription = (label: string, ruleType: RuleType): ListItems[] => { - switch (ruleType) { - case 'machine_learning': { - return [ - { - title: label, - description: i18n.ML_TYPE_DESCRIPTION, - }, - ]; - } - case 'query': - case 'saved_query': { - return [ - { - title: label, - description: i18n.QUERY_TYPE_DESCRIPTION, - }, - ]; - } - default: - return assertUnreachable(ruleType); - } -}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx deleted file mode 100644 index fdfcfd0fd85fe3..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.test.tsx +++ /dev/null @@ -1,474 +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 { shallow } from 'enzyme'; - -import { - StepRuleDescriptionComponent, - addFilterStateIfNotThere, - buildListItems, - getDescriptionItem, -} from './'; - -import { - esFilters, - Filter, - FilterManager, -} from '../../../../../../../../../src/plugins/data/public'; -import { mockAboutStepRule, mockDefineStepRule } from '../../all/__mocks__/mock'; -import { coreMock } from '../../../../../../../../../src/core/public/mocks'; -import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/translations'; -import * as i18n from './translations'; - -import { schema } from '../step_about_rule/schema'; -import { ListItems } from './types'; -import { AboutStepRule } from '../../types'; - -jest.mock('../../../../../lib/kibana'); - -describe('description_step', () => { - const setupMock = coreMock.createSetup(); - const uiSettingsMock = (pinnedByDefault: boolean) => (key: string) => { - switch (key) { - case 'filters:pinnedByDefault': - return pinnedByDefault; - default: - throw new Error(`Unexpected uiSettings key in FilterManager mock: ${key}`); - } - }; - let mockFilterManager: FilterManager; - let mockAboutStep: AboutStepRule; - - beforeEach(() => { - setupMock.uiSettings.get.mockImplementation(uiSettingsMock(true)); - mockFilterManager = new FilterManager(setupMock.uiSettings); - mockAboutStep = mockAboutStepRule(); - }); - - describe('StepRuleDescriptionComponent', () => { - test('renders correctly against snapshot when columns is "multi"', () => { - const wrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); - expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(2); - }); - - test('renders correctly against snapshot when columns is "single"', () => { - const wrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); - expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(1); - }); - - test('renders correctly against snapshot when columns is "singleSplit', () => { - const wrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); - expect(wrapper.find('[data-test-subj="listItemColumnStepRuleDescription"]')).toHaveLength(1); - expect( - wrapper - .find('[data-test-subj="singleSplitStepRuleDescriptionList"]') - .at(0) - .prop('type') - ).toEqual('column'); - }); - }); - - describe('addFilterStateIfNotThere', () => { - test('it does not change the state if it is global', () => { - const filters: Filter[] = [ - { - $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, - }, - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - { - $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, - }, - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - ]; - const output = addFilterStateIfNotThere(filters); - const expected: Filter[] = [ - { - $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, - }, - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - { - $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, - }, - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - ]; - expect(output).toEqual(expected); - }); - - test('it adds the state if it does not exist as local', () => { - const filters: Filter[] = [ - { - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - { - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - ]; - const output = addFilterStateIfNotThere(filters); - const expected: Filter[] = [ - { - $state: { - store: esFilters.FilterStateStore.APP_STATE, - }, - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - { - $state: { - store: esFilters.FilterStateStore.APP_STATE, - }, - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - ]; - expect(output).toEqual(expected); - }); - }); - - describe('buildListItems', () => { - test('returns expected ListItems array when given valid inputs', () => { - const result: ListItems[] = buildListItems(mockAboutStep, schema, mockFilterManager); - - expect(result.length).toEqual(9); - }); - }); - - describe('getDescriptionItem', () => { - test('returns ListItem with all values enumerated when value[field] is an array', () => { - const result: ListItems[] = getDescriptionItem( - 'tags', - 'Tags label', - mockAboutStep, - mockFilterManager - ); - - expect(result[0].title).toEqual('Tags label'); - expect(typeof result[0].description).toEqual('object'); - }); - - test('returns ListItem with description of value[field] when value[field] is a string', () => { - const result: ListItems[] = getDescriptionItem( - 'description', - 'Description label', - mockAboutStep, - mockFilterManager - ); - - expect(result[0].title).toEqual('Description label'); - expect(result[0].description).toEqual('24/7'); - }); - - test('returns empty array when "value" is a non-existant property in "field"', () => { - const result: ListItems[] = getDescriptionItem( - 'jibberjabber', - 'JibberJabber label', - mockAboutStep, - mockFilterManager - ); - - expect(result.length).toEqual(0); - }); - - describe('queryBar', () => { - test('returns array of ListItems when queryBar exist', () => { - const mockQueryBar = { - isNew: false, - queryBar: { - query: { - query: 'user.name: root or user.name: admin', - language: 'kuery', - }, - filters: null, - saved_id: null, - }, - }; - const result: ListItems[] = getDescriptionItem( - 'queryBar', - 'Query bar label', - mockQueryBar, - mockFilterManager - ); - - expect(result[0].title).toEqual(<>{i18n.QUERY_LABEL} ); - expect(result[0].description).toEqual(<>{mockQueryBar.queryBar.query.query} ); - }); - }); - - describe('threat', () => { - test('returns array of ListItems when threat exist', () => { - const result: ListItems[] = getDescriptionItem( - 'threat', - 'Threat label', - mockAboutStep, - mockFilterManager - ); - - expect(result[0].title).toEqual('Threat label'); - expect(React.isValidElement(result[0].description)).toBeTruthy(); - }); - - test('filters out threats with tactic.name of "none"', () => { - const mockStep = { - ...mockAboutStep, - threat: [ - { - framework: 'mockFramework', - tactic: { - id: '1234', - name: 'none', - reference: 'reference1', - }, - technique: [ - { - id: '456', - name: 'technique1', - reference: 'technique reference', - }, - ], - }, - ], - }; - const result: ListItems[] = getDescriptionItem( - 'threat', - 'Threat label', - mockStep, - mockFilterManager - ); - - expect(result.length).toEqual(0); - }); - }); - - describe('references', () => { - test('returns array of ListItems when references exist', () => { - const result: ListItems[] = getDescriptionItem( - 'references', - 'Reference label', - mockAboutStep, - mockFilterManager - ); - - expect(result[0].title).toEqual('Reference label'); - expect(React.isValidElement(result[0].description)).toBeTruthy(); - }); - }); - - describe('falsePositives', () => { - test('returns array of ListItems when falsePositives exist', () => { - const result: ListItems[] = getDescriptionItem( - 'falsePositives', - 'False positives label', - mockAboutStep, - mockFilterManager - ); - - expect(result[0].title).toEqual('False positives label'); - expect(React.isValidElement(result[0].description)).toBeTruthy(); - }); - }); - - describe('severity', () => { - test('returns array of ListItems when severity exist', () => { - const result: ListItems[] = getDescriptionItem( - 'severity', - 'Severity label', - mockAboutStep, - mockFilterManager - ); - - expect(result[0].title).toEqual('Severity label'); - expect(React.isValidElement(result[0].description)).toBeTruthy(); - }); - }); - - describe('riskScore', () => { - test('returns array of ListItems when riskScore exist', () => { - const result: ListItems[] = getDescriptionItem( - 'riskScore', - 'Risk score label', - mockAboutStep, - mockFilterManager - ); - - expect(result[0].title).toEqual('Risk score label'); - expect(result[0].description).toEqual(21); - }); - }); - - describe('timeline', () => { - test('returns timeline title if one exists', () => { - const mockDefineStep = mockDefineStepRule(); - const result: ListItems[] = getDescriptionItem( - 'timeline', - 'Timeline label', - mockDefineStep, - mockFilterManager - ); - - expect(result[0].title).toEqual('Timeline label'); - expect(result[0].description).toEqual('Titled timeline'); - }); - - test('returns default timeline title if none exists', () => { - const mockStep = { - ...mockDefineStepRule(), - timeline: { - id: '12345', - }, - }; - const result: ListItems[] = getDescriptionItem( - 'timeline', - 'Timeline label', - mockStep, - mockFilterManager - ); - - expect(result[0].title).toEqual('Timeline label'); - expect(result[0].description).toEqual(DEFAULT_TIMELINE_TITLE); - }); - }); - - describe('note', () => { - test('returns default "note" description', () => { - const result: ListItems[] = getDescriptionItem( - 'note', - 'Investigation guide', - mockAboutStep, - mockFilterManager - ); - - expect(result[0].title).toEqual('Investigation guide'); - expect(React.isValidElement(result[0].description)).toBeTruthy(); - }); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx deleted file mode 100644 index 108f2138114122..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/index.tsx +++ /dev/null @@ -1,205 +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 { EuiDescriptionList, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { isEmpty, chunk, get, pick, isNumber } from 'lodash/fp'; -import React, { memo, useState } from 'react'; -import styled from 'styled-components'; - -import { RuleType } from '../../../../../../common/detection_engine/types'; -import { - IIndexPattern, - Filter, - esFilters, - FilterManager, -} from '../../../../../../../../../src/plugins/data/public'; -import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/translations'; -import { useKibana } from '../../../../../lib/kibana'; -import { IMitreEnterpriseAttack } from '../../types'; -import { FieldValueTimeline } from '../pick_timeline'; -import { FormSchema } from '../../../../../shared_imports'; -import { ListItems } from './types'; -import { - buildQueryBarDescription, - buildSeverityDescription, - buildStringArrayDescription, - buildThreatDescription, - buildUnorderedListArrayDescription, - buildUrlsDescription, - buildNoteDescription, - buildRuleTypeDescription, -} from './helpers'; -import { useSiemJobs } from '../../../../../components/ml_popover/hooks/use_siem_jobs'; -import { buildMlJobDescription } from './ml_job_description'; - -const DescriptionListContainer = styled(EuiDescriptionList)` - &.euiDescriptionList--column .euiDescriptionList__title { - width: 30%; - } - &.euiDescriptionList--column .euiDescriptionList__description { - width: 70%; - } -`; - -interface StepRuleDescriptionProps { - columns?: 'multi' | 'single' | 'singleSplit'; - data: unknown; - indexPatterns?: IIndexPattern; - schema: FormSchema; -} - -export const StepRuleDescriptionComponent: React.FC = ({ - data, - columns = 'multi', - indexPatterns, - schema, -}) => { - const kibana = useKibana(); - const [filterManager] = useState(new FilterManager(kibana.services.uiSettings)); - const [, siemJobs] = useSiemJobs(true); - - const keys = Object.keys(schema); - const listItems = keys.reduce((acc: ListItems[], key: string) => { - if (key === 'machineLearningJobId') { - return [ - ...acc, - buildMlJobDescription( - get(key, data) as string, - (get(key, schema) as { label: string }).label, - siemJobs - ), - ]; - } - return [...acc, ...buildListItems(data, pick(key, schema), filterManager, indexPatterns)]; - }, []); - - if (columns === 'multi') { - return ( - - {chunk(Math.ceil(listItems.length / 2), listItems).map((chunkListItems, index) => ( - - - - ))} - - ); - } - - return ( - - - {columns === 'single' ? ( - - ) : ( - - )} - - - ); -}; - -export const StepRuleDescription = memo(StepRuleDescriptionComponent); - -export const buildListItems = ( - data: unknown, - schema: FormSchema, - filterManager: FilterManager, - indexPatterns?: IIndexPattern -): ListItems[] => - Object.keys(schema).reduce( - (acc, field) => [ - ...acc, - ...getDescriptionItem( - field, - get([field, 'label'], schema), - data, - filterManager, - indexPatterns - ), - ], - [] - ); - -export const addFilterStateIfNotThere = (filters: Filter[]): Filter[] => { - return filters.map(filter => { - if (filter.$state == null) { - return { $state: { store: esFilters.FilterStateStore.APP_STATE }, ...filter }; - } else { - return filter; - } - }); -}; - -export const getDescriptionItem = ( - field: string, - label: string, - data: unknown, - filterManager: FilterManager, - indexPatterns?: IIndexPattern -): ListItems[] => { - if (field === 'queryBar') { - const filters = addFilterStateIfNotThere(get('queryBar.filters', data) ?? []); - const query = get('queryBar.query.query', data); - const savedId = get('queryBar.saved_id', data); - return buildQueryBarDescription({ - field, - filters, - filterManager, - query, - savedId, - indexPatterns, - }); - } else if (field === 'threat') { - const threat: IMitreEnterpriseAttack[] = get(field, data).filter( - (singleThreat: IMitreEnterpriseAttack) => singleThreat.tactic.name !== 'none' - ); - return buildThreatDescription({ label, threat }); - } else if (field === 'references') { - const urls: string[] = get(field, data); - return buildUrlsDescription(label, urls); - } else if (field === 'falsePositives') { - const values: string[] = get(field, data); - return buildUnorderedListArrayDescription(label, field, values); - } else if (Array.isArray(get(field, data))) { - const values: string[] = get(field, data); - return buildStringArrayDescription(label, field, values); - } else if (field === 'severity') { - const val: string = get(field, data); - return buildSeverityDescription(label, val); - } else if (field === 'timeline') { - const timeline = get(field, data) as FieldValueTimeline; - return [ - { - title: label, - description: timeline.title ?? DEFAULT_TIMELINE_TITLE, - }, - ]; - } else if (field === 'note') { - const val: string = get(field, data); - return buildNoteDescription(label, val); - } else if (field === 'ruleType') { - const ruleType: RuleType = get(field, data); - return buildRuleTypeDescription(label, ruleType); - } - - const description: string = get(field, data); - if (isNumber(description) || !isEmpty(description)) { - return [ - { - title: label, - description, - }, - ]; - } - return []; -}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/types.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/types.ts deleted file mode 100644 index 564a3c5dc2c018..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/description_step/types.ts +++ /dev/null @@ -1,32 +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 { ReactNode } from 'react'; - -import { - IIndexPattern, - Filter, - FilterManager, -} from '../../../../../../../../../src/plugins/data/public'; -import { IMitreEnterpriseAttack } from '../../types'; - -export interface ListItems { - title: NonNullable; - description: NonNullable; -} - -export interface BuildQueryBarDescription { - field: string; - filters: Filter[]; - filterManager: FilterManager; - query: string; - savedId: string; - indexPatterns?: IIndexPattern; -} - -export interface BuildThreatDescription { - label: string; - threat: IMitreEnterpriseAttack[]; -} diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/mitre/helpers.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/mitre/helpers.ts deleted file mode 100644 index 7a28a16214df63..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/mitre/helpers.ts +++ /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 { isEmpty } from 'lodash/fp'; - -import { IMitreAttack } from '../../types'; - -export const isMitreAttackInvalid = ( - tacticName: string | null | undefined, - technique: IMitreAttack[] | null | undefined -) => { - if (isEmpty(tacticName) || (tacticName !== 'none' && isEmpty(technique))) { - return true; - } - return false; -}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.test.tsx deleted file mode 100644 index 3e8d5426824560..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.test.tsx +++ /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 React from 'react'; -import { shallow } from 'enzyme'; - -import { AddMitreThreat } from './index'; -import { useFormFieldMock } from '../../../../../mock'; - -describe('AddMitreThreat', () => { - it('renders correctly', () => { - const Component = () => { - const field = useFormFieldMock(); - - return ( - - ); - }; - const wrapper = shallow(); - - expect(wrapper.dive().find('[data-test-subj="addMitre"]')).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx deleted file mode 100644 index a2d81e72af40e4..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/mitre/index.tsx +++ /dev/null @@ -1,218 +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 { - EuiButtonIcon, - EuiFormRow, - EuiSuperSelect, - EuiSpacer, - EuiFlexGroup, - EuiFlexItem, - EuiComboBox, - EuiText, -} from '@elastic/eui'; -import { isEmpty, kebabCase, camelCase } from 'lodash/fp'; -import React, { useCallback, useState } from 'react'; -import styled from 'styled-components'; - -import { tacticsOptions, techniquesOptions } from '../../../mitre/mitre_tactics_techniques'; -import * as Rulei18n from '../../translations'; -import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../../shared_imports'; -import { threatDefault } from '../step_about_rule/default_value'; -import { IMitreEnterpriseAttack } from '../../types'; -import { MyAddItemButton } from '../add_item_form'; -import { isMitreAttackInvalid } from './helpers'; -import * as i18n from './translations'; - -const MitreContainer = styled.div` - margin-top: 16px; -`; -const MyEuiSuperSelect = styled(EuiSuperSelect)` - width: 280px; -`; -interface AddItemProps { - field: FieldHook; - dataTestSubj: string; - idAria: string; - isDisabled: boolean; -} - -export const AddMitreThreat = ({ dataTestSubj, field, idAria, isDisabled }: AddItemProps) => { - const [showValidation, setShowValidation] = useState(false); - const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); - - const removeItem = useCallback( - (index: number) => { - const values = field.value as string[]; - const newValues = [...values.slice(0, index), ...values.slice(index + 1)]; - if (isEmpty(newValues)) { - field.setValue(threatDefault); - } else { - field.setValue(newValues); - } - }, - [field] - ); - - const addItem = useCallback(() => { - const values = field.value as IMitreEnterpriseAttack[]; - if (!isEmpty(values[values.length - 1])) { - field.setValue([ - ...values, - { tactic: { id: 'none', name: 'none', reference: 'none' }, technique: [] }, - ]); - } else { - field.setValue([{ tactic: { id: 'none', name: 'none', reference: 'none' }, technique: [] }]); - } - }, [field]); - - const updateTactic = useCallback( - (index: number, value: string) => { - const values = field.value as IMitreEnterpriseAttack[]; - const { id, reference, name } = tacticsOptions.find(t => t.value === value) || { - id: '', - name: '', - reference: '', - }; - field.setValue([ - ...values.slice(0, index), - { - ...values[index], - tactic: { id, reference, name }, - technique: [], - }, - ...values.slice(index + 1), - ]); - }, - [field] - ); - - const updateTechniques = useCallback( - (index: number, selectedOptions: unknown[]) => { - field.setValue([ - ...values.slice(0, index), - { - ...values[index], - technique: selectedOptions, - }, - ...values.slice(index + 1), - ]); - }, - [field] - ); - - const values = field.value as IMitreEnterpriseAttack[]; - - const getSelectTactic = (tacticName: string, index: number, disabled: boolean) => ( - {i18n.TACTIC_PLACEHOLDER}, - value: 'none', - disabled, - }, - ] - : []), - ...tacticsOptions.map(t => ({ - inputDisplay: <>{t.text}, - value: t.value, - disabled, - })), - ]} - aria-label="" - onChange={updateTactic.bind(null, index)} - fullWidth={false} - valueOfSelected={camelCase(tacticName)} - data-test-subj="mitreTactic" - /> - ); - - const getSelectTechniques = (item: IMitreEnterpriseAttack, index: number, disabled: boolean) => { - const invalid = isMitreAttackInvalid(item.tactic.name, item.technique); - const options = techniquesOptions.filter(t => t.tactics.includes(kebabCase(item.tactic.name))); - const selectedOptions = item.technique.map(technic => ({ - ...technic, - label: `${technic.name} (${technic.id})`, // API doesn't allow for label field - })); - - return ( - - - setShowValidation(true)} - /> - {showValidation && invalid && ( - -

{errorMessage}

-
- )} -
- - removeItem(index)} - aria-label={Rulei18n.DELETE} - /> - -
- ); - }; - - return ( - - {values.map((item, index) => ( -
- - - {index === 0 ? ( - - <>{getSelectTactic(item.tactic.name, index, isDisabled)} - - ) : ( - getSelectTactic(item.tactic.name, index, isDisabled) - )} - - - {index === 0 ? ( - - <>{getSelectTechniques(item, index, isDisabled)} - - ) : ( - getSelectTechniques(item, index, isDisabled) - )} - - - {values.length - 1 !== index && } -
- ))} - - {i18n.ADD_MITRE_ATTACK} - -
- ); -}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.test.tsx deleted file mode 100644 index dea27d8d045365..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.test.tsx +++ /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 React from 'react'; -import { shallow } from 'enzyme'; - -import { MlJobSelect } from './index'; -import { useSiemJobs } from '../../../../../components/ml_popover/hooks/use_siem_jobs'; -import { useFormFieldMock } from '../../../../../mock'; -jest.mock('../../../../../components/ml_popover/hooks/use_siem_jobs'); -jest.mock('../../../../../lib/kibana'); - -describe('MlJobSelect', () => { - beforeAll(() => { - (useSiemJobs as jest.Mock).mockReturnValue([false, []]); - }); - - it('renders correctly', () => { - const Component = () => { - const field = useFormFieldMock(); - - return ; - }; - const wrapper = shallow(); - - expect(wrapper.dive().find('[data-test-subj="mlJobSelect"]')).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.tsx deleted file mode 100644 index 4fb9faaea711c0..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/ml_job_select/index.tsx +++ /dev/null @@ -1,140 +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, { useCallback, useMemo } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiIcon, - EuiLink, - EuiSuperSelect, - EuiText, -} from '@elastic/eui'; - -import styled from 'styled-components'; -import { isJobStarted } from '../../../../../../common/detection_engine/ml_helpers'; -import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../../shared_imports'; -import { useSiemJobs } from '../../../../../components/ml_popover/hooks/use_siem_jobs'; -import { useKibana } from '../../../../../lib/kibana'; -import { - ML_JOB_SELECT_PLACEHOLDER_TEXT, - ENABLE_ML_JOB_WARNING, -} from '../step_define_rule/translations'; - -const HelpTextWarningContainer = styled.div` - margin-top: 10px; -`; - -const MlJobSelectEuiFlexGroup = styled(EuiFlexGroup)` - margin-bottom: 5px; -`; - -const HelpText: React.FC<{ href: string; showEnableWarning: boolean }> = ({ - href, - showEnableWarning = false, -}) => ( - <> - - - - ), - }} - /> - {showEnableWarning && ( - - - - {ENABLE_ML_JOB_WARNING} - - - )} - -); - -const JobDisplay: React.FC<{ title: string; description: string }> = ({ title, description }) => ( - <> - {title} - -

{description}

-
- -); - -interface MlJobSelectProps { - describedByIds: string[]; - field: FieldHook; -} - -export const MlJobSelect: React.FC = ({ describedByIds = [], field }) => { - const jobId = field.value as string; - const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); - const [isLoading, siemJobs] = useSiemJobs(false); - const mlUrl = useKibana().services.application.getUrlForApp('ml'); - const handleJobChange = useCallback( - (machineLearningJobId: string) => { - field.setValue(machineLearningJobId); - }, - [field] - ); - const placeholderOption = { - value: 'placeholder', - inputDisplay: ML_JOB_SELECT_PLACEHOLDER_TEXT, - dropdownDisplay: ML_JOB_SELECT_PLACEHOLDER_TEXT, - disabled: true, - }; - - const jobOptions = siemJobs.map(job => ({ - value: job.id, - inputDisplay: job.id, - dropdownDisplay: , - })); - - const options = [placeholderOption, ...jobOptions]; - - const isJobRunning = useMemo(() => { - // If the selected job is not found in the list, it means the placeholder is selected - // and so we don't want to show the warning, thus isJobRunning will be true when 'job == null' - const job = siemJobs.find(j => j.id === jobId); - return job == null || isJobStarted(job.jobState, job.datafeedState); - }, [siemJobs, jobId]); - - return ( - - - } - isInvalid={isInvalid} - error={errorMessage} - data-test-subj="mlJobSelect" - describedByIds={describedByIds} - > - - - - - - - - - ); -}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/next_step/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/next_step/index.tsx deleted file mode 100644 index 11332e7af92662..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/next_step/index.tsx +++ /dev/null @@ -1,32 +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 { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton } from '@elastic/eui'; -import * as RuleI18n from '../../translations'; - -interface NextStepProps { - onClick: () => Promise; - isDisabled: boolean; - dataTestSubj?: string; -} - -export const NextStep = React.memo( - ({ onClick, isDisabled, dataTestSubj = 'nextStep-continue' }) => ( - <> - - - - - {RuleI18n.CONTINUE} - - - - - ) -); - -NextStep.displayName = 'NextStep'; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/optional_field_label/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/optional_field_label/index.tsx deleted file mode 100644 index 0dab87b0a3b744..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/optional_field_label/index.tsx +++ /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 { EuiText } from '@elastic/eui'; -import React from 'react'; - -import * as RuleI18n from '../../translations'; - -export const OptionalFieldLabel = ( - - {RuleI18n.OPTIONAL_FIELD} - -); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.test.tsx deleted file mode 100644 index fefc9697176c4c..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.test.tsx +++ /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 React from 'react'; -import { shallow } from 'enzyme'; - -import { PickTimeline } from './index'; -import { useFormFieldMock } from '../../../../../mock'; - -describe('PickTimeline', () => { - it('renders correctly', () => { - const Component = () => { - const field = useFormFieldMock(); - - return ( - - ); - }; - const wrapper = shallow(); - - expect(wrapper.dive().find('[data-test-subj="pick-timeline"]')).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.tsx deleted file mode 100644 index 27d668dc6166c4..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/pick_timeline/index.tsx +++ /dev/null @@ -1,74 +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 { EuiFormRow } from '@elastic/eui'; -import React, { useCallback, useEffect, useState } from 'react'; - -import { SearchTimelineSuperSelect } from '../../../../../components/timeline/search_super_select'; -import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../../shared_imports'; - -export interface FieldValueTimeline { - id: string | null; - title: string | null; -} - -interface QueryBarDefineRuleProps { - dataTestSubj: string; - field: FieldHook; - idAria: string; - isDisabled: boolean; -} - -export const PickTimeline = ({ - dataTestSubj, - field, - idAria, - isDisabled = false, -}: QueryBarDefineRuleProps) => { - const [timelineId, setTimelineId] = useState(null); - const [timelineTitle, setTimelineTitle] = useState(null); - - const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); - - useEffect(() => { - const { id, title } = field.value as FieldValueTimeline; - if (timelineTitle !== title && timelineId !== id) { - setTimelineId(id); - setTimelineTitle(title); - } - }, [field.value]); - - const handleOnTimelineChange = useCallback( - (title: string, id: string | null) => { - if (id === null) { - field.setValue({ id, title: null }); - } else if (timelineTitle !== title && timelineId !== id) { - field.setValue({ id, title }); - } - }, - [field] - ); - - return ( - - - - ); -}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.test.tsx deleted file mode 100644 index cdd06ad58bb4b2..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.test.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 { shallow } from 'enzyme'; - -import { QueryBarDefineRule } from './index'; -import { useFormFieldMock } from '../../../../../mock'; - -jest.mock('../../../../../lib/kibana'); - -describe('QueryBarDefineRule', () => { - it('renders correctly', () => { - const Component = () => { - const field = useFormFieldMock(); - - return ( - - ); - }; - const wrapper = shallow(); - - expect(wrapper.dive().find('[data-test-subj="query-bar-define-rule"]')).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx deleted file mode 100644 index b92d98a4afb13b..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/query_bar/index.tsx +++ /dev/null @@ -1,285 +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 { EuiFormRow, EuiMutationObserver } from '@elastic/eui'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import { Subscription } from 'rxjs'; -import styled from 'styled-components'; -import deepEqual from 'fast-deep-equal'; - -import { - Filter, - IIndexPattern, - Query, - FilterManager, - SavedQuery, - SavedQueryTimeFilter, -} from '../../../../../../../../../src/plugins/data/public'; - -import { BrowserFields } from '../../../../../containers/source'; -import { OpenTimelineModal } from '../../../../../components/open_timeline/open_timeline_modal'; -import { ActionTimelineToShow } from '../../../../../components/open_timeline/types'; -import { QueryBar } from '../../../../../components/query_bar'; -import { buildGlobalQuery } from '../../../../../components/timeline/helpers'; -import { getDataProviderFilter } from '../../../../../components/timeline/query_bar'; -import { convertKueryToElasticSearchQuery } from '../../../../../lib/keury'; -import { useKibana } from '../../../../../lib/kibana'; -import { TimelineModel } from '../../../../../store/timeline/model'; -import { useSavedQueryServices } from '../../../../../utils/saved_query_services'; -import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../../shared_imports'; -import * as i18n from './translations'; - -export interface FieldValueQueryBar { - filters: Filter[]; - query: Query; - saved_id?: string; -} -interface QueryBarDefineRuleProps { - browserFields: BrowserFields; - dataTestSubj: string; - field: FieldHook; - idAria: string; - isLoading: boolean; - indexPattern: IIndexPattern; - onCloseTimelineSearch: () => void; - openTimelineSearch: boolean; - resizeParentContainer?: (height: number) => void; -} - -const StyledEuiFormRow = styled(EuiFormRow)` - .kbnTypeahead__items { - max-height: 45vh !important; - } - .globalQueryBar { - padding: 4px 0px 0px 0px; - .kbnQueryBar { - & > div:first-child { - margin: 0px 0px 0px 4px; - } - } - } -`; - -// TODO need to add disabled in the SearchBar - -export const QueryBarDefineRule = ({ - browserFields, - dataTestSubj, - field, - idAria, - indexPattern, - isLoading = false, - onCloseTimelineSearch, - openTimelineSearch = false, - resizeParentContainer, -}: QueryBarDefineRuleProps) => { - const [originalHeight, setOriginalHeight] = useState(-1); - const [loadingTimeline, setLoadingTimeline] = useState(false); - const [savedQuery, setSavedQuery] = useState(null); - const [queryDraft, setQueryDraft] = useState({ query: '', language: 'kuery' }); - const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); - - const kibana = useKibana(); - const [filterManager] = useState(new FilterManager(kibana.services.uiSettings)); - - const savedQueryServices = useSavedQueryServices(); - - useEffect(() => { - let isSubscribed = true; - const subscriptions = new Subscription(); - filterManager.setFilters([]); - - subscriptions.add( - filterManager.getUpdates$().subscribe({ - next: () => { - if (isSubscribed) { - const newFilters = filterManager.getFilters(); - const { filters } = field.value as FieldValueQueryBar; - - if (!deepEqual(filters, newFilters)) { - field.setValue({ ...(field.value as FieldValueQueryBar), filters: newFilters }); - } - } - }, - }) - ); - - return () => { - isSubscribed = false; - subscriptions.unsubscribe(); - }; - }, [field.value]); - - useEffect(() => { - let isSubscribed = true; - async function updateFilterQueryFromValue() { - const { filters, query, saved_id: savedId } = field.value as FieldValueQueryBar; - if (!deepEqual(query, queryDraft)) { - setQueryDraft(query); - } - if (!deepEqual(filters, filterManager.getFilters())) { - filterManager.setFilters(filters); - } - if ( - (savedId != null && savedQuery != null && savedId !== savedQuery.id) || - (savedId != null && savedQuery == null) - ) { - try { - const mySavedQuery = await savedQueryServices.getSavedQuery(savedId); - if (isSubscribed && mySavedQuery != null) { - setSavedQuery(mySavedQuery); - } - } catch { - setSavedQuery(null); - } - } else if (savedId == null && savedQuery != null) { - setSavedQuery(null); - } - } - updateFilterQueryFromValue(); - return () => { - isSubscribed = false; - }; - }, [field.value]); - - const onSubmitQuery = useCallback( - (newQuery: Query, timefilter?: SavedQueryTimeFilter) => { - const { query } = field.value as FieldValueQueryBar; - if (!deepEqual(query, newQuery)) { - field.setValue({ ...(field.value as FieldValueQueryBar), query: newQuery }); - } - }, - [field] - ); - - const onChangedQuery = useCallback( - (newQuery: Query) => { - const { query } = field.value as FieldValueQueryBar; - if (!deepEqual(query, newQuery)) { - field.setValue({ ...(field.value as FieldValueQueryBar), query: newQuery }); - } - }, - [field] - ); - - const onSavedQuery = useCallback( - (newSavedQuery: SavedQuery | null) => { - if (newSavedQuery != null) { - const { saved_id: savedId } = field.value as FieldValueQueryBar; - if (newSavedQuery.id !== savedId) { - setSavedQuery(newSavedQuery); - field.setValue({ - filters: newSavedQuery.attributes.filters, - query: newSavedQuery.attributes.query, - saved_id: newSavedQuery.id, - }); - } - } - }, - [field.value] - ); - - const onCloseTimelineModal = useCallback(() => { - setLoadingTimeline(true); - onCloseTimelineSearch(); - }, [onCloseTimelineSearch]); - - const onOpenTimeline = useCallback( - (timeline: TimelineModel) => { - setLoadingTimeline(false); - const newQuery = { - query: timeline.kqlQuery.filterQuery?.kuery?.expression ?? '', - language: timeline.kqlQuery.filterQuery?.kuery?.kind ?? 'kuery', - }; - const dataProvidersDsl = - timeline.dataProviders != null && timeline.dataProviders.length > 0 - ? convertKueryToElasticSearchQuery( - buildGlobalQuery(timeline.dataProviders, browserFields), - indexPattern - ) - : ''; - const newFilters = timeline.filters ?? []; - field.setValue({ - filters: - dataProvidersDsl !== '' - ? [...newFilters, getDataProviderFilter(dataProvidersDsl)] - : newFilters, - query: newQuery, - saved_id: '', - }); - }, - [browserFields, field, indexPattern] - ); - - const onMutation = (event: unknown, observer: unknown) => { - if (resizeParentContainer != null) { - const suggestionContainer = document.getElementById('kbnTypeahead__items'); - if (suggestionContainer != null) { - const box = suggestionContainer.getBoundingClientRect(); - const accordionContainer = document.getElementById('define-rule'); - if (accordionContainer != null) { - const accordionBox = accordionContainer.getBoundingClientRect(); - if (originalHeight === -1 || accordionBox.height < originalHeight + box.height) { - resizeParentContainer(originalHeight + box.height - 100); - } - if (originalHeight === -1) { - setOriginalHeight(accordionBox.height); - } - } - } else { - resizeParentContainer(-1); - } - } - }; - - const actionTimelineToHide = useMemo(() => ['duplicate'], []); - - return ( - <> - - - {mutationRef => ( -
- -
- )} -
-
- {openTimelineSearch ? ( - - ) : null} - - ); -}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.test.tsx deleted file mode 100644 index 13ae3ee3d3b7de..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.test.tsx +++ /dev/null @@ -1,42 +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 { shallow } from 'enzyme'; - -import { RuleActionsField } from './index'; -import { useKibana } from '../../../../../lib/kibana'; -import { useFormFieldMock } from '../../../../../mock'; -jest.mock('../../../../../lib/kibana'); - -describe('RuleActionsField', () => { - it('should not render ActionForm is no actions are supported', () => { - (useKibana as jest.Mock).mockReturnValue({ - services: { - triggers_actions_ui: { - actionTypeRegistry: {}, - }, - application: { - capabilities: { - actions: { - delete: true, - save: true, - show: true, - }, - }, - }, - }, - }); - const Component = () => { - const field = useFormFieldMock(); - - return ; - }; - const wrapper = shallow(); - - expect(wrapper.dive().find('ActionForm')).toHaveLength(0); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.tsx deleted file mode 100644 index d53cf52cc67b94..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_field/index.tsx +++ /dev/null @@ -1,87 +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, { useCallback, useEffect, useState } from 'react'; -import deepMerge from 'deepmerge'; - -import { NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS } from '../../../../../../common/constants'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { loadActionTypes } from '../../../../../../../triggers_actions_ui/public/application/lib/action_connector_api'; -import { SelectField } from '../../../../../shared_imports'; -import { ActionForm, ActionType } from '../../../../../../../triggers_actions_ui/public'; -import { AlertAction } from '../../../../../../../alerting/common'; -import { useKibana } from '../../../../../lib/kibana'; - -type ThrottleSelectField = typeof SelectField; - -const DEFAULT_ACTION_GROUP_ID = 'default'; -const DEFAULT_ACTION_MESSAGE = - 'Rule {{context.rule.name}} generated {{state.signals_count}} signals'; - -export const RuleActionsField: ThrottleSelectField = ({ field, messageVariables }) => { - const [supportedActionTypes, setSupportedActionTypes] = useState(); - const { - http, - triggers_actions_ui: { actionTypeRegistry }, - notifications, - docLinks, - application: { capabilities }, - } = useKibana().services; - - const setActionIdByIndex = useCallback( - (id: string, index: number) => { - const updatedActions = [...(field.value as Array>)]; - updatedActions[index] = deepMerge(updatedActions[index], { id }); - field.setValue(updatedActions); - }, - [field] - ); - - const setAlertProperty = useCallback( - (updatedActions: AlertAction[]) => field.setValue(updatedActions), - [field] - ); - - const setActionParamsProperty = useCallback( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (key: string, value: any, index: number) => { - const updatedActions = [...(field.value as AlertAction[])]; - updatedActions[index].params[key] = value; - field.setValue(updatedActions); - }, - [field] - ); - - useEffect(() => { - (async function() { - const actionTypes = await loadActionTypes({ http }); - const supportedTypes = actionTypes.filter(actionType => - NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS.includes(actionType.id) - ); - setSupportedActionTypes(supportedTypes); - })(); - }, []); - - if (!supportedActionTypes) return <>; - - return ( - - ); -}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.test.tsx deleted file mode 100644 index b54938e6a3cf18..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.test.tsx +++ /dev/null @@ -1,295 +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 { shallow, mount } from 'enzyme'; -import React from 'react'; - -import { deleteRulesAction, duplicateRulesAction } from '../../all/actions'; -import { RuleActionsOverflow } from './index'; -import { mockRule } from '../../all/__mocks__/mock'; - -jest.mock('react-router-dom', () => ({ - useHistory: () => ({ - push: jest.fn(), - }), -})); - -jest.mock('../../all/actions', () => ({ - deleteRulesAction: jest.fn(), - duplicateRulesAction: jest.fn(), -})); - -describe('RuleActionsOverflow', () => { - describe('snapshots', () => { - test('renders correctly against snapshot', () => { - const wrapper = shallow( - - ); - expect(wrapper).toMatchSnapshot(); - }); - }); - - describe('rules details menu panel', () => { - test('there is at least one item when there is a rule within the rules-details-menu-panel', () => { - const wrapper = mount( - - ); - wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); - wrapper.update(); - const items: unknown[] = wrapper - .find('[data-test-subj="rules-details-menu-panel"]') - .first() - .prop('items'); - - expect(items.length).toBeGreaterThan(0); - }); - - test('items are empty when there is a null rule within the rules-details-menu-panel', () => { - const wrapper = mount(); - wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); - wrapper.update(); - expect( - wrapper - .find('[data-test-subj="rules-details-menu-panel"]') - .first() - .prop('items') - ).toEqual([]); - }); - - test('items are empty when there is an undefined rule within the rules-details-menu-panel', () => { - const wrapper = mount(); - wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); - wrapper.update(); - expect( - wrapper - .find('[data-test-subj="rules-details-menu-panel"]') - .first() - .prop('items') - ).toEqual([]); - }); - - test('it opens the popover when rules-details-popover-button-icon is clicked', () => { - const wrapper = mount( - - ); - wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); - wrapper.update(); - expect( - wrapper - .find('[data-test-subj="rules-details-popover"]') - .first() - .prop('isOpen') - ).toEqual(true); - }); - }); - - describe('rules details pop over button icon', () => { - test('it does not open the popover when rules-details-popover-button-icon is clicked when the user does not have permission', () => { - const wrapper = mount( - - ); - wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); - wrapper.update(); - expect( - wrapper - .find('[data-test-subj="rules-details-popover"]') - .first() - .prop('isOpen') - ).toEqual(false); - }); - }); - - describe('rules details duplicate rule', () => { - test('it does not open the popover when rules-details-popover-button-icon is clicked and the user does not have permission', () => { - const rule = mockRule('id'); - const wrapper = mount(); - wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); - wrapper.update(); - expect(wrapper.find('[data-test-subj="rules-details-delete-rule"] button').exists()).toEqual( - false - ); - }); - - test('it opens the popover when rules-details-popover-button-icon is clicked', () => { - const wrapper = mount( - - ); - wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); - wrapper.update(); - expect( - wrapper - .find('[data-test-subj="rules-details-popover"]') - .first() - .prop('isOpen') - ).toEqual(true); - }); - - test('it closes the popover when rules-details-duplicate-rule is clicked', () => { - const wrapper = mount( - - ); - wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); - wrapper.update(); - wrapper.find('[data-test-subj="rules-details-duplicate-rule"] button').simulate('click'); - wrapper.update(); - expect( - wrapper - .find('[data-test-subj="rules-details-popover"]') - .first() - .prop('isOpen') - ).toEqual(false); - }); - - test('it calls duplicateRulesAction when rules-details-duplicate-rule is clicked', () => { - const wrapper = mount( - - ); - wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); - wrapper.update(); - wrapper.find('[data-test-subj="rules-details-duplicate-rule"] button').simulate('click'); - wrapper.update(); - expect(duplicateRulesAction).toHaveBeenCalled(); - }); - - test('it calls duplicateRulesAction with the rule and rule.id when rules-details-duplicate-rule is clicked', () => { - const rule = mockRule('id'); - const wrapper = mount(); - wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); - wrapper.update(); - wrapper.find('[data-test-subj="rules-details-duplicate-rule"] button').simulate('click'); - wrapper.update(); - expect(duplicateRulesAction).toHaveBeenCalledWith( - [rule], - [rule.id], - expect.anything(), - expect.anything() - ); - }); - }); - - describe('rules details export rule', () => { - test('it does not open the popover when rules-details-popover-button-icon is clicked and the user does not have permission', () => { - const rule = mockRule('id'); - const wrapper = mount(); - wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); - wrapper.update(); - expect(wrapper.find('[data-test-subj="rules-details-export-rule"] button').exists()).toEqual( - false - ); - }); - - test('it closes the popover when rules-details-export-rule is clicked', () => { - const wrapper = mount( - - ); - wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); - wrapper.update(); - wrapper.find('[data-test-subj="rules-details-export-rule"] button').simulate('click'); - wrapper.update(); - expect( - wrapper - .find('[data-test-subj="rules-details-popover"]') - .first() - .prop('isOpen') - ).toEqual(false); - }); - - test('it sets the rule.rule_id on the generic downloader when rules-details-export-rule is clicked', () => { - const rule = mockRule('id'); - const wrapper = mount(); - wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); - wrapper.update(); - wrapper.find('[data-test-subj="rules-details-export-rule"] button').simulate('click'); - wrapper.update(); - expect( - wrapper.find('[data-test-subj="rules-details-generic-downloader"]').prop('ids') - ).toEqual([rule.rule_id]); - }); - - test('it does not close the pop over on rules-details-export-rule when the rule is an immutable rule and the user does a click', () => { - const rule = mockRule('id'); - rule.immutable = true; - const wrapper = mount(); - wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); - wrapper.update(); - wrapper.find('[data-test-subj="rules-details-export-rule"] button').simulate('click'); - wrapper.update(); - expect( - wrapper - .find('[data-test-subj="rules-details-popover"]') - .first() - .prop('isOpen') - ).toEqual(true); - }); - - test('it does not set the rule.rule_id on rules-details-export-rule when the rule is an immutable rule', () => { - const rule = mockRule('id'); - rule.immutable = true; - const wrapper = mount(); - wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); - wrapper.update(); - wrapper.find('[data-test-subj="rules-details-export-rule"] button').simulate('click'); - wrapper.update(); - expect( - wrapper.find('[data-test-subj="rules-details-generic-downloader"]').prop('ids') - ).toEqual([]); - }); - }); - - describe('rules details delete rule', () => { - test('it does not open the popover when rules-details-popover-button-icon is clicked and the user does not have permission', () => { - const rule = mockRule('id'); - const wrapper = mount(); - wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); - wrapper.update(); - expect(wrapper.find('[data-test-subj="rules-details-delete-rule"] button').exists()).toEqual( - false - ); - }); - - test('it closes the popover when rules-details-delete-rule is clicked', () => { - const wrapper = mount( - - ); - wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); - wrapper.update(); - wrapper.find('[data-test-subj="rules-details-delete-rule"] button').simulate('click'); - wrapper.update(); - expect( - wrapper - .find('[data-test-subj="rules-details-popover"]') - .first() - .prop('isOpen') - ).toEqual(false); - }); - - test('it calls deleteRulesAction when rules-details-delete-rule is clicked', () => { - const wrapper = mount( - - ); - wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); - wrapper.update(); - wrapper.find('[data-test-subj="rules-details-delete-rule"] button').simulate('click'); - wrapper.update(); - expect(deleteRulesAction).toHaveBeenCalled(); - }); - - test('it calls deleteRulesAction with the rule.id when rules-details-delete-rule is clicked', () => { - const rule = mockRule('id'); - const wrapper = mount(); - wrapper.find('[data-test-subj="rules-details-popover-button-icon"] button').simulate('click'); - wrapper.update(); - wrapper.find('[data-test-subj="rules-details-delete-rule"] button').simulate('click'); - wrapper.update(); - expect(deleteRulesAction).toHaveBeenCalledWith( - [rule.id], - expect.anything(), - expect.anything(), - expect.anything() - ); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.tsx deleted file mode 100644 index a7ce0c85ffdcf7..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_actions_overflow/index.tsx +++ /dev/null @@ -1,155 +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 { - EuiButtonIcon, - EuiContextMenuItem, - EuiContextMenuPanel, - EuiPopover, - EuiToolTip, -} from '@elastic/eui'; -import React, { useCallback, useMemo, useState } from 'react'; -import styled from 'styled-components'; - -import { noop } from 'lodash/fp'; -import { useHistory } from 'react-router-dom'; -import { Rule, exportRules } from '../../../../../containers/detection_engine/rules'; -import * as i18n from './translations'; -import * as i18nActions from '../../../rules/translations'; -import { displaySuccessToast, useStateToaster } from '../../../../../components/toasters'; -import { deleteRulesAction, duplicateRulesAction } from '../../all/actions'; -import { GenericDownloader } from '../../../../../components/generic_downloader'; -import { DETECTION_ENGINE_PAGE_NAME } from '../../../../../components/link_to/redirect_to_detection_engine'; - -const MyEuiButtonIcon = styled(EuiButtonIcon)` - &.euiButtonIcon { - svg { - transform: rotate(90deg); - } - border: 1px solid  ${({ theme }) => theme.euiColorPrimary}; - width: 40px; - height: 40px; - } -`; - -interface RuleActionsOverflowComponentProps { - rule: Rule | null; - userHasNoPermissions: boolean; -} - -/** - * Overflow Actions for a Rule - */ -const RuleActionsOverflowComponent = ({ - rule, - userHasNoPermissions, -}: RuleActionsOverflowComponentProps) => { - const [isPopoverOpen, setIsPopoverOpen] = useState(false); - const [rulesToExport, setRulesToExport] = useState([]); - const history = useHistory(); - const [, dispatchToaster] = useStateToaster(); - - const onRuleDeletedCallback = useCallback(() => { - history.push(`/${DETECTION_ENGINE_PAGE_NAME}/rules`); - }, [history]); - - const actions = useMemo( - () => - rule != null - ? [ - { - setIsPopoverOpen(false); - await duplicateRulesAction([rule], [rule.id], noop, dispatchToaster); - }} - > - {i18nActions.DUPLICATE_RULE} - , - { - setIsPopoverOpen(false); - setRulesToExport([rule.rule_id]); - }} - > - {i18nActions.EXPORT_RULE} - , - { - setIsPopoverOpen(false); - await deleteRulesAction([rule.id], noop, dispatchToaster, onRuleDeletedCallback); - }} - > - {i18nActions.DELETE_RULE} - , - ] - : [], - [rule, userHasNoPermissions] - ); - - const handlePopoverOpen = useCallback(() => { - setIsPopoverOpen(!isPopoverOpen); - }, [setIsPopoverOpen, isPopoverOpen]); - - const button = useMemo( - () => ( - - - - ), - [handlePopoverOpen, userHasNoPermissions] - ); - - return ( - <> - setIsPopoverOpen(false)} - id="ruleActionsOverflow" - isOpen={isPopoverOpen} - data-test-subj="rules-details-popover" - ownFocus={true} - panelPaddingSize="none" - > - - - { - displaySuccessToast( - i18nActions.SUCCESSFULLY_EXPORTED_RULES(exportCount), - dispatchToaster - ); - }} - /> - - ); -}; - -export const RuleActionsOverflow = React.memo(RuleActionsOverflowComponent); - -RuleActionsOverflow.displayName = 'RuleActionsOverflow'; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.ts deleted file mode 100644 index 263f602251ea77..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_status/helpers.ts +++ /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 { RuleStatusType } from '../../../../../containers/detection_engine/rules'; - -export const getStatusColor = (status: RuleStatusType | string | null) => - status == null - ? 'subdued' - : status === 'succeeded' - ? 'success' - : status === 'failed' - ? 'danger' - : status === 'executing' || status === 'going to run' - ? 'warning' - : 'subdued'; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.tsx deleted file mode 100644 index ac457d7345c29e..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_status/index.tsx +++ /dev/null @@ -1,99 +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 { - EuiButtonIcon, - EuiFlexGroup, - EuiFlexItem, - EuiHealth, - EuiLoadingSpinner, - EuiText, -} from '@elastic/eui'; -import React, { memo, useCallback, useEffect, useState } from 'react'; -import deepEqual from 'fast-deep-equal'; - -import { useRuleStatus, RuleInfoStatus } from '../../../../../containers/detection_engine/rules'; -import { FormattedDate } from '../../../../../components/formatted_date'; -import { getEmptyTagValue } from '../../../../../components/empty_value'; -import { getStatusColor } from './helpers'; -import * as i18n from './translations'; - -interface RuleStatusProps { - ruleId: string | null; - ruleEnabled?: boolean | null; -} - -const RuleStatusComponent: React.FC = ({ ruleId, ruleEnabled }) => { - const [loading, ruleStatus, fetchRuleStatus] = useRuleStatus(ruleId); - const [myRuleEnabled, setMyRuleEnabled] = useState(ruleEnabled ?? null); - const [currentStatus, setCurrentStatus] = useState( - ruleStatus?.current_status ?? null - ); - - useEffect(() => { - if (myRuleEnabled !== ruleEnabled && fetchRuleStatus != null && ruleId != null) { - fetchRuleStatus(ruleId); - if (myRuleEnabled !== ruleEnabled) { - setMyRuleEnabled(ruleEnabled ?? null); - } - } - }, [fetchRuleStatus, myRuleEnabled, ruleId, ruleEnabled, setMyRuleEnabled]); - - useEffect(() => { - if (!deepEqual(currentStatus, ruleStatus?.current_status)) { - setCurrentStatus(ruleStatus?.current_status ?? null); - } - }, [currentStatus, ruleStatus, setCurrentStatus]); - - const handleRefresh = useCallback(() => { - if (fetchRuleStatus != null && ruleId != null) { - fetchRuleStatus(ruleId); - } - }, [fetchRuleStatus, ruleId]); - - return ( - - - {i18n.STATUS} - {':'} - - {loading && ( - - - - )} - {!loading && ( - <> - - - {currentStatus?.status ?? getEmptyTagValue()} - - - {currentStatus?.status_date != null && currentStatus?.status != null && ( - <> - - <>{i18n.STATUS_AT} - - - - - - )} - - - - - )} - - ); -}; - -export const RuleStatus = memo(RuleStatusComponent); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx deleted file mode 100644 index 44845ea68d954a..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/rule_switch/index.tsx +++ /dev/null @@ -1,134 +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 { - EuiFlexGroup, - EuiFlexItem, - EuiLoadingSpinner, - EuiSwitch, - EuiSwitchEvent, -} from '@elastic/eui'; -import { isEmpty } from 'lodash/fp'; -import styled from 'styled-components'; -import React, { useCallback, useState, useEffect } from 'react'; - -import * as i18n from '../../translations'; -import { enableRules } from '../../../../../containers/detection_engine/rules'; -import { enableRulesAction } from '../../all/actions'; -import { Action } from '../../all/reducer'; -import { useStateToaster, displayErrorToast } from '../../../../../components/toasters'; -import { bucketRulesResponse } from '../../all/helpers'; - -const StaticSwitch = styled(EuiSwitch)` - .euiSwitch__thumb, - .euiSwitch__icon { - transition: none; - } -`; - -StaticSwitch.displayName = 'StaticSwitch'; - -export interface RuleSwitchProps { - dispatch?: React.Dispatch; - id: string; - enabled: boolean; - isDisabled?: boolean; - isLoading?: boolean; - optionLabel?: string; - onChange?: (enabled: boolean) => void; -} - -/** - * Basic switch component for displaying loader when enabled/disabled - */ -export const RuleSwitchComponent = ({ - dispatch, - id, - isDisabled, - isLoading, - enabled, - optionLabel, - onChange, -}: RuleSwitchProps) => { - const [myIsLoading, setMyIsLoading] = useState(false); - const [myEnabled, setMyEnabled] = useState(enabled ?? false); - const [, dispatchToaster] = useStateToaster(); - - const onRuleStateChange = useCallback( - async (event: EuiSwitchEvent) => { - setMyIsLoading(true); - if (dispatch != null) { - await enableRulesAction([id], event.target.checked!, dispatch, dispatchToaster); - } else { - try { - const enabling = event.target.checked!; - const response = await enableRules({ - ids: [id], - enabled: enabling, - }); - const { rules, errors } = bucketRulesResponse(response); - - if (errors.length > 0) { - setMyIsLoading(false); - const title = enabling - ? i18n.BATCH_ACTION_ACTIVATE_SELECTED_ERROR(1) - : i18n.BATCH_ACTION_DEACTIVATE_SELECTED_ERROR(1); - displayErrorToast( - title, - errors.map(e => e.error.message), - dispatchToaster - ); - } else { - const [rule] = rules; - setMyEnabled(rule.enabled); - if (onChange != null) { - onChange(rule.enabled); - } - } - } catch { - setMyIsLoading(false); - } - } - setMyIsLoading(false); - }, - [dispatch, id] - ); - - useEffect(() => { - if (myEnabled !== enabled) { - setMyEnabled(enabled); - } - }, [enabled]); - - useEffect(() => { - if (myIsLoading !== isLoading) { - setMyIsLoading(isLoading ?? false); - } - }, [isLoading]); - - return ( - - - {myIsLoading ? ( - - ) : ( - - )} - - - ); -}; - -export const RuleSwitch = React.memo(RuleSwitchComponent); - -RuleSwitch.displayName = 'RuleSwitch'; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.test.tsx deleted file mode 100644 index 3829af02ca4f17..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.test.tsx +++ /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 React from 'react'; -import { shallow } from 'enzyme'; - -import { ScheduleItem } from './index'; -import { useFormFieldMock } from '../../../../../mock'; - -describe('ScheduleItem', () => { - it('renders correctly', () => { - const Component = () => { - const field = useFormFieldMock(); - - return ( - - ); - }; - const wrapper = shallow(); - - expect(wrapper.dive().find('[data-test-subj="schedule-item"]')).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx deleted file mode 100644 index 1b7d17016f83c3..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/schedule_item_form/index.tsx +++ /dev/null @@ -1,163 +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 { - EuiFlexGroup, - EuiFlexItem, - EuiFieldNumber, - EuiFormRow, - EuiSelect, - EuiFormControlLayout, -} from '@elastic/eui'; -import { isEmpty } from 'lodash/fp'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; -import styled from 'styled-components'; - -import { FieldHook, getFieldValidityAndErrorMessage } from '../../../../../shared_imports'; - -import * as I18n from './translations'; - -interface ScheduleItemProps { - field: FieldHook; - dataTestSubj: string; - idAria: string; - isDisabled: boolean; - minimumValue?: number; -} - -const timeTypeOptions = [ - { value: 's', text: I18n.SECONDS }, - { value: 'm', text: I18n.MINUTES }, - { value: 'h', text: I18n.HOURS }, -]; - -// move optional label to the end of input -const StyledLabelAppend = styled(EuiFlexItem)` - &.euiFlexItem.euiFlexItem--flexGrowZero { - margin-left: 31px; - } -`; - -const StyledEuiFormRow = styled(EuiFormRow)` - max-width: none; - - .euiFormControlLayout { - max-width: 200px !important; - } - - .euiFormControlLayout__childrenWrapper > *:first-child { - box-shadow: none; - height: 38px; - } - - .euiFormControlLayout:not(:first-child) { - border-left: 1px solid ${({ theme }) => theme.eui.euiColorLightShade}; - } -`; - -const MyEuiSelect = styled(EuiSelect)` - width: auto; -`; - -export const ScheduleItem = ({ - dataTestSubj, - field, - idAria, - isDisabled, - minimumValue = 0, -}: ScheduleItemProps) => { - const [timeType, setTimeType] = useState('s'); - const [timeVal, setTimeVal] = useState(0); - const { isInvalid, errorMessage } = getFieldValidityAndErrorMessage(field); - - const onChangeTimeType = useCallback( - e => { - setTimeType(e.target.value); - field.setValue(`${timeVal}${e.target.value}`); - }, - [timeVal] - ); - - const onChangeTimeVal = useCallback( - e => { - const sanitizedValue: number = parseInt(e.target.value, 10); - setTimeVal(sanitizedValue); - field.setValue(`${sanitizedValue}${timeType}`); - }, - [timeType] - ); - - useEffect(() => { - if (field.value !== `${timeVal}${timeType}`) { - const filterTimeVal = (field.value as string).match(/\d+/g); - const filterTimeType = (field.value as string).match(/[a-zA-Z]+/g); - if ( - !isEmpty(filterTimeVal) && - filterTimeVal != null && - !isNaN(Number(filterTimeVal[0])) && - Number(filterTimeVal[0]) !== Number(timeVal) - ) { - setTimeVal(Number(filterTimeVal[0])); - } - if ( - !isEmpty(filterTimeType) && - filterTimeType != null && - ['s', 'm', 'h'].includes(filterTimeType[0]) && - filterTimeType[0] !== timeType - ) { - setTimeType(filterTimeType[0]); - } - } - }, [field.value]); - - // EUI missing some props - const rest = { disabled: isDisabled }; - const label = useMemo( - () => ( - - - {field.label} - - - {field.labelAppend} - - - ), - [field.label, field.labelAppend] - ); - - return ( - - - } - > - - - - ); -}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.test.tsx deleted file mode 100644 index 3d832d61abb283..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.test.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { shallow } from 'enzyme'; - -import { SelectRuleType } from './index'; -import { useFormFieldMock } from '../../../../../mock'; -jest.mock('../../../../../lib/kibana'); - -describe('SelectRuleType', () => { - it('renders correctly', () => { - const Component = () => { - const field = useFormFieldMock(); - - return ; - }; - const wrapper = shallow(); - - expect(wrapper.dive().find('[data-test-subj="selectRuleType"]')).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.tsx deleted file mode 100644 index 6f3d299da8d452..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/select_rule_type/index.tsx +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useCallback } from 'react'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiCard, - EuiFlexGrid, - EuiFlexItem, - EuiFormRow, - EuiIcon, - EuiLink, - EuiText, -} from '@elastic/eui'; - -import { isMlRule } from '../../../../../../common/detection_engine/ml_helpers'; -import { RuleType } from '../../../../../../common/detection_engine/types'; -import { FieldHook } from '../../../../../shared_imports'; -import { useKibana } from '../../../../../lib/kibana'; -import * as i18n from './translations'; - -const MlCardDescription = ({ - subscriptionUrl, - hasValidLicense = false, -}: { - subscriptionUrl: string; - hasValidLicense?: boolean; -}) => ( - - {hasValidLicense ? ( - i18n.ML_TYPE_DESCRIPTION - ) : ( - - - - ), - }} - /> - )} - -); - -interface SelectRuleTypeProps { - describedByIds?: string[]; - field: FieldHook; - hasValidLicense?: boolean; - isMlAdmin?: boolean; - isReadOnly?: boolean; -} - -export const SelectRuleType: React.FC = ({ - describedByIds = [], - field, - isReadOnly = false, - hasValidLicense = false, - isMlAdmin = false, -}) => { - const ruleType = field.value as RuleType; - const setType = useCallback( - (type: RuleType) => { - field.setValue(type); - }, - [field] - ); - const setMl = useCallback(() => setType('machine_learning'), [setType]); - const setQuery = useCallback(() => setType('query'), [setType]); - const mlCardDisabled = isReadOnly || !hasValidLicense || !isMlAdmin; - const licensingUrl = useKibana().services.application.getUrlForApp('kibana', { - path: '#/management/elasticsearch/license_management', - }); - - return ( - - - - } - selectable={{ - isDisabled: isReadOnly, - onClick: setQuery, - isSelected: !isMlRule(ruleType), - }} - /> - - - - } - icon={} - isDisabled={mlCardDisabled} - selectable={{ - isDisabled: mlCardDisabled, - onClick: setMl, - isSelected: isMlRule(ruleType), - }} - /> - - - - ); -}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.test.tsx deleted file mode 100644 index 89b8a56e790541..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.test.tsx +++ /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 React from 'react'; -import { shallow } from 'enzyme'; - -import { TestProviders } from '../../../../../mock'; -import { RuleStatusIcon } from './index'; -jest.mock('../../../../../lib/kibana'); - -describe('RuleStatusIcon', () => { - it('renders correctly', () => { - const wrapper = shallow(, { - wrappingComponent: TestProviders, - }); - - expect(wrapper.find('EuiAvatar')).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.tsx deleted file mode 100644 index 3ec5bf1a12eb0f..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/status_icon/index.tsx +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiAvatar, EuiIcon } from '@elastic/eui'; -import React, { memo } from 'react'; -import styled from 'styled-components'; - -import { useEuiTheme } from '../../../../../lib/theme/use_eui_theme'; -import { RuleStatusType } from '../../types'; - -export interface RuleStatusIconProps { - name: string; - type: RuleStatusType; -} - -const RuleStatusIconStyled = styled.div` - position: relative; - svg { - position: absolute; - top: 8px; - left: 9px; - } -`; - -const RuleStatusIconComponent: React.FC = ({ name, type }) => { - const theme = useEuiTheme(); - const color = type === 'passive' ? theme.euiColorLightestShade : theme.euiColorPrimary; - return ( - - - {type === 'valid' ? : null} - - ); -}; - -export const RuleStatusIcon = memo(RuleStatusIconComponent); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.test.tsx deleted file mode 100644 index 3c28e697789acf..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.test.tsx +++ /dev/null @@ -1,166 +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 { mount, shallow } from 'enzyme'; -import { ThemeProvider } from 'styled-components'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_light.json'; - -import { StepAboutRule } from './'; -import { mockAboutStepRule } from '../../all/__mocks__/mock'; -import { StepRuleDescription } from '../description_step'; -import { stepAboutDefaultValue } from './default_value'; - -const theme = () => ({ eui: euiDarkVars, darkMode: true }); - -/* eslint-disable no-console */ -// Silence until enzyme fixed to use ReactTestUtils.act() -const originalError = console.error; -beforeAll(() => { - console.error = jest.fn(); -}); -afterAll(() => { - console.error = originalError; -}); -/* eslint-enable no-console */ - -describe('StepAboutRuleComponent', () => { - test('it renders StepRuleDescription if isReadOnlyView is true and "name" property exists', () => { - const wrapper = shallow( - - ); - - expect(wrapper.find(StepRuleDescription).exists()).toBeTruthy(); - }); - - test('it prevents user from clicking continue if no "description" defined', () => { - const wrapper = mount( - - - - ); - - const nameInput = wrapper - .find('input[aria-describedby="detectionEngineStepAboutRuleName"]') - .at(0); - nameInput.simulate('change', { target: { value: 'Test name text' } }); - - const descriptionInput = wrapper - .find('textarea[aria-describedby="detectionEngineStepAboutRuleDescription"]') - .at(0); - const nextButton = wrapper.find('button[data-test-subj="about-continue"]').at(0); - nextButton.simulate('click'); - - expect( - wrapper - .find('input[aria-describedby="detectionEngineStepAboutRuleName"]') - .at(0) - .props().value - ).toEqual('Test name text'); - expect(descriptionInput.props().value).toEqual(''); - expect( - wrapper - .find('EuiFormRow[data-test-subj="detectionEngineStepAboutRuleDescription"] label') - .at(0) - .hasClass('euiFormLabel-isInvalid') - ).toBeTruthy(); - expect( - wrapper - .find('EuiFormRow[data-test-subj="detectionEngineStepAboutRuleDescription"] EuiTextArea') - .at(0) - .prop('isInvalid') - ).toBeTruthy(); - }); - - test('it prevents user from clicking continue if no "name" defined', () => { - const wrapper = mount( - - - - ); - - const descriptionInput = wrapper - .find('textarea[aria-describedby="detectionEngineStepAboutRuleDescription"]') - .at(0); - descriptionInput.simulate('change', { target: { value: 'Test description text' } }); - - const nameInput = wrapper - .find('input[aria-describedby="detectionEngineStepAboutRuleName"]') - .at(0); - const nextButton = wrapper.find('button[data-test-subj="about-continue"]').at(0); - nextButton.simulate('click'); - - expect( - wrapper - .find('textarea[aria-describedby="detectionEngineStepAboutRuleDescription"]') - .at(0) - .props().value - ).toEqual('Test description text'); - expect(nameInput.props().value).toEqual(''); - expect( - wrapper - .find('EuiFormRow[data-test-subj="detectionEngineStepAboutRuleName"] label') - .at(0) - .hasClass('euiFormLabel-isInvalid') - ).toBeTruthy(); - expect( - wrapper - .find('EuiFormRow[data-test-subj="detectionEngineStepAboutRuleName"] EuiFieldText') - .at(0) - .prop('isInvalid') - ).toBeTruthy(); - }); - - test('it allows user to click continue if "name" and "description" are defined', () => { - const wrapper = mount( - - - - ); - - const descriptionInput = wrapper - .find('textarea[aria-describedby="detectionEngineStepAboutRuleDescription"]') - .at(0); - descriptionInput.simulate('change', { target: { value: 'Test description text' } }); - - const nameInput = wrapper - .find('input[aria-describedby="detectionEngineStepAboutRuleName"]') - .at(0); - nameInput.simulate('change', { target: { value: 'Test name text' } }); - - const nextButton = wrapper.find('button[data-test-subj="about-continue"]').at(0); - nextButton.simulate('click'); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx deleted file mode 100644 index eaf543780d7774..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/index.tsx +++ /dev/null @@ -1,279 +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 { EuiAccordion, EuiFlexItem, EuiSpacer, EuiButtonEmpty } from '@elastic/eui'; -import React, { FC, memo, useCallback, useEffect, useState } from 'react'; -import styled from 'styled-components'; -import deepEqual from 'fast-deep-equal'; - -import { setFieldValue } from '../../helpers'; -import { RuleStepProps, RuleStep, AboutStepRule } from '../../types'; -import { AddItem } from '../add_item_form'; -import { StepRuleDescription } from '../description_step'; -import { AddMitreThreat } from '../mitre'; -import { - Field, - Form, - FormDataProvider, - getUseField, - UseField, - useForm, -} from '../../../../../shared_imports'; - -import { defaultRiskScoreBySeverity, severityOptions, SeverityValue } from './data'; -import { stepAboutDefaultValue } from './default_value'; -import { isUrlInvalid } from './helpers'; -import { schema } from './schema'; -import * as I18n from './translations'; -import { StepContentWrapper } from '../step_content_wrapper'; -import { NextStep } from '../next_step'; -import { MarkdownEditorForm } from '../../../../../components/markdown_editor/form'; - -const CommonUseField = getUseField({ component: Field }); - -interface StepAboutRuleProps extends RuleStepProps { - defaultValues?: AboutStepRule | null; -} - -const ThreeQuartersContainer = styled.div` - max-width: 740px; -`; - -ThreeQuartersContainer.displayName = 'ThreeQuartersContainer'; - -const TagContainer = styled.div` - margin-top: 16px; -`; - -TagContainer.displayName = 'TagContainer'; - -const AdvancedSettingsAccordion = styled(EuiAccordion)` - .euiAccordion__iconWrapper { - display: none; - } - - .euiAccordion__childWrapper { - transition-duration: 1ms; /* hack to fire Step accordion to set proper content's height */ - } - - &.euiAccordion-isOpen .euiButtonEmpty__content > svg { - transform: rotate(90deg); - } -`; - -const AdvancedSettingsAccordionButton = ( - - {I18n.ADVANCED_SETTINGS} - -); - -const StepAboutRuleComponent: FC = ({ - addPadding = false, - defaultValues, - descriptionColumns = 'singleSplit', - isReadOnlyView, - isUpdateView = false, - isLoading, - setForm, - setStepData, -}) => { - const [myStepData, setMyStepData] = useState(stepAboutDefaultValue); - - const { form } = useForm({ - defaultValue: myStepData, - options: { stripEmptyFields: false }, - schema, - }); - - const onSubmit = useCallback(async () => { - if (setStepData) { - setStepData(RuleStep.aboutRule, null, false); - const { isValid, data } = await form.submit(); - if (isValid) { - setStepData(RuleStep.aboutRule, data, isValid); - setMyStepData({ ...data, isNew: false } as AboutStepRule); - } - } - }, [form]); - - useEffect(() => { - const { isNew, ...initDefaultValue } = myStepData; - if (defaultValues != null && !deepEqual(initDefaultValue, defaultValues)) { - const myDefaultValues = { - ...defaultValues, - isNew: false, - }; - setMyStepData(myDefaultValues); - setFieldValue(form, schema, myDefaultValues); - } - }, [defaultValues]); - - useEffect(() => { - if (setForm != null) { - setForm(RuleStep.aboutRule, form); - } - }, [form]); - - return isReadOnlyView && myStepData.name != null ? ( - - - - ) : ( - <> - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {({ severity }) => { - const newRiskScore = defaultRiskScoreBySeverity[severity as SeverityValue]; - const severityField = form.getFields().severity; - const riskScoreField = form.getFields().riskScore; - if ( - severityField.value !== severity && - newRiskScore != null && - riskScoreField.value !== newRiskScore - ) { - riskScoreField.setValue(newRiskScore); - } - return null; - }} - - -
- - {!isUpdateView && ( - - )} - - ); -}; - -export const StepAboutRule = memo(StepAboutRuleComponent); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx deleted file mode 100644 index 7c088c068c9b25..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule/schema.tsx +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -import { IMitreEnterpriseAttack } from '../../types'; -import { - FIELD_TYPES, - fieldValidators, - FormSchema, - ValidationFunc, - ERROR_CODE, -} from '../../../../../shared_imports'; -import { isMitreAttackInvalid } from '../mitre/helpers'; -import { OptionalFieldLabel } from '../optional_field_label'; -import { isUrlInvalid } from './helpers'; -import * as I18n from './translations'; - -const { emptyField } = fieldValidators; - -export const schema: FormSchema = { - name: { - type: FIELD_TYPES.TEXT, - label: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.fieldNameLabel', { - defaultMessage: 'Name', - }), - validations: [ - { - validator: emptyField( - i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepAboutRule.nameFieldRequiredError', - { - defaultMessage: 'A name is required.', - } - ) - ), - }, - ], - }, - description: { - type: FIELD_TYPES.TEXTAREA, - label: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldDescriptionLabel', - { - defaultMessage: 'Description', - } - ), - validations: [ - { - validator: emptyField( - i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepAboutRule.descriptionFieldRequiredError', - { - defaultMessage: 'A description is required.', - } - ) - ), - }, - ], - }, - severity: { - type: FIELD_TYPES.SUPER_SELECT, - label: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldSeverityLabel', - { - defaultMessage: 'Severity', - } - ), - validations: [ - { - validator: emptyField( - i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepAboutRule.severityFieldRequiredError', - { - defaultMessage: 'A severity is required.', - } - ) - ), - }, - ], - }, - riskScore: { - type: FIELD_TYPES.RANGE, - label: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldRiskScoreLabel', - { - defaultMessage: 'Risk score', - } - ), - }, - references: { - label: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldReferenceUrlsLabel', - { - defaultMessage: 'Reference URLs', - } - ), - labelAppend: OptionalFieldLabel, - validations: [ - { - validator: ( - ...args: Parameters - ): ReturnType> | undefined => { - const [{ value, path }] = args; - let hasError = false; - (value as string[]).forEach(url => { - if (isUrlInvalid(url)) { - hasError = true; - } - }); - return hasError - ? { - code: 'ERR_FIELD_FORMAT', - path, - message: I18n.URL_FORMAT_INVALID, - } - : undefined; - }, - }, - ], - }, - falsePositives: { - label: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldFalsePositiveLabel', - { - defaultMessage: 'False positive examples', - } - ), - labelAppend: OptionalFieldLabel, - }, - threat: { - label: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldMitreThreatLabel', - { - defaultMessage: 'MITRE ATT&CK\\u2122', - } - ), - labelAppend: OptionalFieldLabel, - validations: [ - { - validator: ( - ...args: Parameters - ): ReturnType> | undefined => { - const [{ value, path }] = args; - let hasError = false; - (value as IMitreEnterpriseAttack[]).forEach(v => { - if (isMitreAttackInvalid(v.tactic.name, v.technique)) { - hasError = true; - } - }); - return hasError - ? { - code: 'ERR_FIELD_MISSING', - path, - message: I18n.CUSTOM_MITRE_ATTACK_TECHNIQUES_REQUIRED, - } - : undefined; - }, - }, - ], - }, - tags: { - type: FIELD_TYPES.COMBO_BOX, - label: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTagsLabel', { - defaultMessage: 'Tags', - }), - helpText: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTagsHelpText', - { - defaultMessage: - 'Type one or more custom identifying tags for this rule. Press enter after each tag to begin a new one.', - } - ), - labelAppend: OptionalFieldLabel, - }, - note: { - type: FIELD_TYPES.TEXTAREA, - label: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.guideLabel', { - defaultMessage: 'Investigation guide', - }), - helpText: i18n.translate('xpack.siem.detectionEngine.createRule.stepAboutRule.guideHelpText', { - defaultMessage: - 'Provide helpful information for analysts that are performing a signal investigation. This guide will appear on both the rule details page and in timelines created from signals generated by this rule.', - }), - labelAppend: OptionalFieldLabel, - }, -}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.test.tsx deleted file mode 100644 index 76a3c590a62a6d..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.test.tsx +++ /dev/null @@ -1,170 +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 { mount, shallow } from 'enzyme'; -import { EuiProgress, EuiButtonGroup } from '@elastic/eui'; -import { ThemeProvider } from 'styled-components'; -import euiDarkVars from '@elastic/eui/dist/eui_theme_light.json'; - -import { StepAboutRuleToggleDetails } from './'; -import { mockAboutStepRule } from '../../all/__mocks__/mock'; -import { HeaderSection } from '../../../../../components/header_section'; -import { StepAboutRule } from '../step_about_rule/'; -import { AboutStepRule } from '../../types'; - -jest.mock('../../../../../lib/kibana'); - -const theme = () => ({ eui: euiDarkVars, darkMode: true }); - -describe('StepAboutRuleToggleDetails', () => { - let mockRule: AboutStepRule; - - beforeEach(() => { - mockRule = mockAboutStepRule(); - }); - - test('it renders loading component when "loading" is true', () => { - const wrapper = shallow( - - ); - - expect(wrapper.find(EuiProgress).exists()).toBeTruthy(); - expect(wrapper.find(HeaderSection).exists()).toBeTruthy(); - }); - - test('it does not render details if stepDataDetails is null', () => { - const wrapper = shallow( - - ); - - expect(wrapper.find(StepAboutRule).exists()).toBeFalsy(); - }); - - test('it does not render details if stepData is null', () => { - const wrapper = shallow( - - ); - - expect(wrapper.find(StepAboutRule).exists()).toBeFalsy(); - }); - - describe('note value is empty string', () => { - test('it does not render toggle buttons', () => { - const mockAboutStepWithoutNote = { - ...mockRule, - note: '', - }; - const wrapper = shallow( - - ); - - expect(wrapper.find('[data-test-subj="stepAboutDetailsToggle"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="stepAboutDetailsNoteContent"]').exists()).toBeFalsy(); - expect(wrapper.find('[data-test-subj="stepAboutDetailsContent"]').exists()).toBeTruthy(); - }); - }); - - describe('note value does exist', () => { - test('it renders toggle buttons, defaulted to "details"', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find(EuiButtonGroup).exists()).toBeTruthy(); - expect( - wrapper - .find('EuiButtonToggle[id="details"]') - .at(0) - .prop('isSelected') - ).toBeTruthy(); - expect( - wrapper - .find('EuiButtonToggle[id="notes"]') - .at(0) - .prop('isSelected') - ).toBeFalsy(); - }); - - test('it allows users to toggle between "details" and "note"', () => { - const wrapper = mount( - - - - ); - - expect(wrapper.find('EuiButtonGroup[idSelected="details"]').exists()).toBeTruthy(); - expect(wrapper.find('EuiButtonGroup[idSelected="notes"]').exists()).toBeFalsy(); - - wrapper - .find('input[title="Investigation guide"]') - .at(0) - .simulate('change', { target: { value: 'notes' } }); - - expect(wrapper.find('EuiButtonGroup[idSelected="details"]').exists()).toBeFalsy(); - expect(wrapper.find('EuiButtonGroup[idSelected="notes"]').exists()).toBeTruthy(); - }); - - test('it displays notes markdown when user toggles to "notes"', () => { - const wrapper = mount( - - - - ); - - wrapper - .find('input[title="Investigation guide"]') - .at(0) - .simulate('change', { target: { value: 'notes' } }); - - expect(wrapper.find('EuiButtonGroup[idSelected="notes"]').exists()).toBeTruthy(); - expect(wrapper.find('Markdown h1').text()).toEqual('this is some markdown documentation'); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.tsx deleted file mode 100644 index 5d9803214fa0a6..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_about_rule_details/index.tsx +++ /dev/null @@ -1,150 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - EuiPanel, - EuiProgress, - EuiButtonGroup, - EuiButtonGroupOption, - EuiSpacer, - EuiFlexItem, - EuiText, - EuiFlexGroup, - EuiResizeObserver, -} from '@elastic/eui'; -import React, { memo, useCallback, useState } from 'react'; -import styled from 'styled-components'; -import { isEmpty } from 'lodash/fp'; - -import { HeaderSection } from '../../../../../components/header_section'; -import { Markdown } from '../../../../../components/markdown'; -import { AboutStepRule, AboutStepRuleDetails } from '../../types'; -import * as i18n from './translations'; -import { StepAboutRule } from '../step_about_rule/'; - -const MyPanel = styled(EuiPanel)` - position: relative; -`; - -const FlexGroupFullHeight = styled(EuiFlexGroup)` - height: 100%; -`; - -const VerticalOverflowContainer = styled.div((props: { maxHeight: number }) => ({ - 'max-height': `${props.maxHeight}px`, - 'overflow-y': 'hidden', -})); - -const VerticalOverflowContent = styled.div((props: { maxHeight: number }) => ({ - 'max-height': `${props.maxHeight}px`, -})); - -const AboutContent = styled.div` - height: 100%; -`; - -const toggleOptions: EuiButtonGroupOption[] = [ - { - id: 'details', - label: i18n.ABOUT_PANEL_DETAILS_TAB, - }, - { - id: 'notes', - label: i18n.ABOUT_PANEL_NOTES_TAB, - }, -]; - -interface StepPanelProps { - stepData: AboutStepRule | null; - stepDataDetails: AboutStepRuleDetails | null; - loading: boolean; -} - -const StepAboutRuleToggleDetailsComponent: React.FC = ({ - stepData, - stepDataDetails, - loading, -}) => { - const [selectedToggleOption, setToggleOption] = useState('details'); - const [aboutPanelHeight, setAboutPanelHeight] = useState(0); - - const onResize = useCallback( - (e: { height: number; width: number }) => { - setAboutPanelHeight(e.height); - }, - [setAboutPanelHeight] - ); - - return ( - - {loading && ( - <> - - - - )} - {stepData != null && stepDataDetails != null && ( - - - - {!isEmpty(stepDataDetails.note) && stepDataDetails.note.trim() !== '' && ( - { - setToggleOption(val); - }} - data-test-subj="stepAboutDetailsToggle" - /> - )} - - - - {selectedToggleOption === 'details' ? ( - - {resizeRef => ( - - - - - {stepDataDetails.description} - - - - - - - )} - - ) : ( - - - - - - )} - - - )} - - ); -}; - -export const StepAboutRuleToggleDetails = memo(StepAboutRuleToggleDetailsComponent); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.test.tsx deleted file mode 100644 index ebef6348d477ea..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.test.tsx +++ /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. - */ - -import React from 'react'; -import { shallow } from 'enzyme'; - -import { StepDefineRule } from './index'; - -jest.mock('../../../../../lib/kibana'); - -describe('StepDefineRule', () => { - it('renders correctly', () => { - const wrapper = shallow(); - - expect(wrapper.find('Form[data-test-subj="stepDefineRule"]')).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx deleted file mode 100644 index b6887badc56be5..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/index.tsx +++ /dev/null @@ -1,276 +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 { EuiButtonEmpty, EuiFormRow } from '@elastic/eui'; -import React, { FC, memo, useCallback, useState, useEffect } from 'react'; -import styled from 'styled-components'; -import deepEqual from 'fast-deep-equal'; - -import { DEFAULT_INDEX_KEY } from '../../../../../../common/constants'; -import { isMlRule } from '../../../../../../common/detection_engine/ml_helpers'; -import { IIndexPattern } from '../../../../../../../../../src/plugins/data/public'; -import { useFetchIndexPatterns } from '../../../../../containers/detection_engine/rules'; -import { DEFAULT_TIMELINE_TITLE } from '../../../../../components/timeline/translations'; -import { useMlCapabilities } from '../../../../../components/ml_popover/hooks/use_ml_capabilities'; -import { useUiSetting$ } from '../../../../../lib/kibana'; -import { setFieldValue } from '../../helpers'; -import { DefineStepRule, RuleStep, RuleStepProps } from '../../types'; -import { StepRuleDescription } from '../description_step'; -import { QueryBarDefineRule } from '../query_bar'; -import { SelectRuleType } from '../select_rule_type'; -import { AnomalyThresholdSlider } from '../anomaly_threshold_slider'; -import { MlJobSelect } from '../ml_job_select'; -import { PickTimeline } from '../pick_timeline'; -import { StepContentWrapper } from '../step_content_wrapper'; -import { NextStep } from '../next_step'; -import { - Field, - Form, - FormDataProvider, - getUseField, - UseField, - useForm, - FormSchema, -} from '../../../../../shared_imports'; -import { schema } from './schema'; -import * as i18n from './translations'; -import { filterRuleFieldsForType, RuleFields } from '../../create/helpers'; -import { hasMlAdminPermissions } from '../../../../../components/ml/permissions/has_ml_admin_permissions'; - -const CommonUseField = getUseField({ component: Field }); - -interface StepDefineRuleProps extends RuleStepProps { - defaultValues?: DefineStepRule | null; -} - -const stepDefineDefaultValue: DefineStepRule = { - anomalyThreshold: 50, - index: [], - isNew: true, - machineLearningJobId: '', - ruleType: 'query', - queryBar: { - query: { query: '', language: 'kuery' }, - filters: [], - saved_id: undefined, - }, - timeline: { - id: null, - title: DEFAULT_TIMELINE_TITLE, - }, -}; - -const MyLabelButton = styled(EuiButtonEmpty)` - height: 18px; - font-size: 12px; - - .euiIcon { - width: 14px; - height: 14px; - } -`; - -MyLabelButton.defaultProps = { - flush: 'right', -}; - -const StepDefineRuleComponent: FC = ({ - addPadding = false, - defaultValues, - descriptionColumns = 'singleSplit', - isReadOnlyView, - isLoading, - isUpdateView = false, - setForm, - setStepData, -}) => { - const mlCapabilities = useMlCapabilities(); - const [openTimelineSearch, setOpenTimelineSearch] = useState(false); - const [indexModified, setIndexModified] = useState(false); - const [localIsMlRule, setIsMlRule] = useState(false); - const [indicesConfig] = useUiSetting$(DEFAULT_INDEX_KEY); - const [myStepData, setMyStepData] = useState({ - ...stepDefineDefaultValue, - index: indicesConfig ?? [], - }); - const [ - { browserFields, indexPatterns: indexPatternQueryBar, isLoading: indexPatternLoadingQueryBar }, - ] = useFetchIndexPatterns(myStepData.index); - - const { form } = useForm({ - defaultValue: myStepData, - options: { stripEmptyFields: false }, - schema, - }); - const clearErrors = useCallback(() => form.reset({ resetValues: false }), [form]); - - const onSubmit = useCallback(async () => { - if (setStepData) { - setStepData(RuleStep.defineRule, null, false); - const { isValid, data } = await form.submit(); - if (isValid && setStepData) { - setStepData(RuleStep.defineRule, data, isValid); - setMyStepData({ ...data, isNew: false } as DefineStepRule); - } - } - }, [form]); - - useEffect(() => { - const { isNew, ...values } = myStepData; - if (defaultValues != null && !deepEqual(values, defaultValues)) { - const newValues = { ...values, ...defaultValues, isNew: false }; - setMyStepData(newValues); - setFieldValue(form, schema, newValues); - } - }, [defaultValues, setMyStepData, setFieldValue]); - - useEffect(() => { - if (setForm != null) { - setForm(RuleStep.defineRule, form); - } - }, [form]); - - const handleResetIndices = useCallback(() => { - const indexField = form.getFields().index; - indexField.setValue(indicesConfig); - }, [form, indicesConfig]); - - const handleOpenTimelineSearch = useCallback(() => { - setOpenTimelineSearch(true); - }, []); - - const handleCloseTimelineSearch = useCallback(() => { - setOpenTimelineSearch(false); - }, []); - - return isReadOnlyView ? ( - - - - ) : ( - <> - -
- - - <> - - {i18n.RESET_DEFAULT_INDEX} - - ) : null, - }} - componentProps={{ - idAria: 'detectionEngineStepDefineRuleIndices', - 'data-test-subj': 'detectionEngineStepDefineRuleIndices', - euiFieldProps: { - fullWidth: true, - isDisabled: isLoading, - placeholder: '', - }, - }} - /> - - {i18n.IMPORT_TIMELINE_QUERY} - - ), - }} - component={QueryBarDefineRule} - componentProps={{ - browserFields, - idAria: 'detectionEngineStepDefineRuleQueryBar', - indexPattern: indexPatternQueryBar, - isDisabled: isLoading, - isLoading: indexPatternLoadingQueryBar, - dataTestSubj: 'detectionEngineStepDefineRuleQueryBar', - openTimelineSearch, - onCloseTimelineSearch: handleCloseTimelineSearch, - }} - /> - - - - <> - - - - - - - {({ index, ruleType }) => { - if (index != null) { - if (deepEqual(index, indicesConfig) && indexModified) { - setIndexModified(false); - } else if (!deepEqual(index, indicesConfig) && !indexModified) { - setIndexModified(true); - } - } - - if (isMlRule(ruleType) && !localIsMlRule) { - setIsMlRule(true); - clearErrors(); - } else if (!isMlRule(ruleType) && localIsMlRule) { - setIsMlRule(false); - clearErrors(); - } - - return null; - }} - - -
- - {!isUpdateView && ( - - )} - - ); -}; - -export const StepDefineRule = memo(StepDefineRuleComponent); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx deleted file mode 100644 index 8915c5f0a224f9..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_define_rule/schema.tsx +++ /dev/null @@ -1,176 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { EuiText } from '@elastic/eui'; -import { isEmpty } from 'lodash/fp'; -import React from 'react'; - -import { isMlRule } from '../../../../../../common/detection_engine/ml_helpers'; -import { esKuery } from '../../../../../../../../../src/plugins/data/public'; -import { FieldValueQueryBar } from '../query_bar'; -import { - ERROR_CODE, - FIELD_TYPES, - fieldValidators, - FormSchema, - ValidationFunc, -} from '../../../../../shared_imports'; -import { CUSTOM_QUERY_REQUIRED, INVALID_CUSTOM_QUERY, INDEX_HELPER_TEXT } from './translations'; - -export const schema: FormSchema = { - index: { - type: FIELD_TYPES.COMBO_BOX, - label: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepAboutRule.fiedIndexPatternsLabel', - { - defaultMessage: 'Index patterns', - } - ), - helpText: {INDEX_HELPER_TEXT}, - validations: [ - { - validator: ( - ...args: Parameters - ): ReturnType> | undefined => { - const [{ formData }] = args; - const needsValidation = !isMlRule(formData.ruleType); - - if (!needsValidation) { - return; - } - - return fieldValidators.emptyField( - i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepDefineRule.outputIndiceNameFieldRequiredError', - { - defaultMessage: 'A minimum of one index pattern is required.', - } - ) - )(...args); - }, - }, - ], - }, - queryBar: { - label: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldQuerBarLabel', - { - defaultMessage: 'Custom query', - } - ), - validations: [ - { - validator: ( - ...args: Parameters - ): ReturnType> | undefined => { - const [{ value, path, formData }] = args; - const { query, filters } = value as FieldValueQueryBar; - const needsValidation = !isMlRule(formData.ruleType); - if (!needsValidation) { - return; - } - - return isEmpty(query.query as string) && isEmpty(filters) - ? { - code: 'ERR_FIELD_MISSING', - path, - message: CUSTOM_QUERY_REQUIRED, - } - : undefined; - }, - }, - { - validator: ( - ...args: Parameters - ): ReturnType> | undefined => { - const [{ value, path, formData }] = args; - const { query } = value as FieldValueQueryBar; - const needsValidation = !isMlRule(formData.ruleType); - if (!needsValidation) { - return; - } - - if (!isEmpty(query.query as string) && query.language === 'kuery') { - try { - esKuery.fromKueryExpression(query.query); - } catch (err) { - return { - code: 'ERR_FIELD_FORMAT', - path, - message: INVALID_CUSTOM_QUERY, - }; - } - } - }, - }, - ], - }, - ruleType: { - label: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldRuleTypeLabel', - { - defaultMessage: 'Rule type', - } - ), - validations: [], - }, - anomalyThreshold: { - label: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldAnomalyThresholdLabel', - { - defaultMessage: 'Anomaly score threshold', - } - ), - validations: [], - }, - machineLearningJobId: { - label: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepDefineRule.fieldMachineLearningJobIdLabel', - { - defaultMessage: 'Machine Learning job', - } - ), - validations: [ - { - validator: ( - ...args: Parameters - ): ReturnType> | undefined => { - const [{ formData }] = args; - const needsValidation = isMlRule(formData.ruleType); - - if (!needsValidation) { - return; - } - - return fieldValidators.emptyField( - i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepDefineRule.machineLearningJobIdRequired', - { - defaultMessage: 'A Machine Learning job is required.', - } - ) - )(...args); - }, - }, - ], - }, - timeline: { - label: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTimelineTemplateLabel', - { - defaultMessage: 'Timeline template', - } - ), - helpText: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepAboutRule.fieldTimelineTemplateHelpText', - { - defaultMessage: - 'Select an existing timeline to use as a template when investigating generated signals.', - } - ), - }, -}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.tsx deleted file mode 100644 index 1923ed09252dda..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_panel/index.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiPanel, EuiProgress } from '@elastic/eui'; -import React, { memo } from 'react'; -import styled from 'styled-components'; - -import { HeaderSection } from '../../../../../components/header_section'; - -interface StepPanelProps { - children: React.ReactNode; - loading: boolean; - title: string; -} - -const MyPanel = styled(EuiPanel)` - position: relative; -`; - -MyPanel.displayName = 'MyPanel'; - -const StepPanelComponent: React.FC = ({ children, loading, title }) => ( - - {loading && } - - {children} - -); - -export const StepPanel = memo(StepPanelComponent); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.test.tsx deleted file mode 100644 index 69d118ba9f28eb..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.test.tsx +++ /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 React from 'react'; -import { shallow } from 'enzyme'; - -import { StepRuleActions } from './index'; - -jest.mock('../../../../../lib/kibana'); - -describe('StepRuleActions', () => { - it('renders correctly', () => { - const wrapper = shallow( - - ); - - expect(wrapper.find('[data-test-subj="stepRuleActions"]')).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.tsx deleted file mode 100644 index aec315938b6aef..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/index.tsx +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiHorizontalRule, EuiFlexGroup, EuiFlexItem, EuiButton, EuiSpacer } from '@elastic/eui'; -import React, { FC, memo, useCallback, useEffect, useMemo, useState } from 'react'; -import deepEqual from 'fast-deep-equal'; - -import { setFieldValue } from '../../helpers'; -import { RuleStep, RuleStepProps, ActionsStepRule } from '../../types'; -import { StepRuleDescription } from '../description_step'; -import { Form, UseField, useForm } from '../../../../../shared_imports'; -import { StepContentWrapper } from '../step_content_wrapper'; -import { ThrottleSelectField, THROTTLE_OPTIONS } from '../throttle_select_field'; -import { RuleActionsField } from '../rule_actions_field'; -import { useKibana } from '../../../../../lib/kibana'; -import { schema } from './schema'; -import * as I18n from './translations'; - -interface StepRuleActionsProps extends RuleStepProps { - defaultValues?: ActionsStepRule | null; - actionMessageParams: string[]; -} - -const stepActionsDefaultValue = { - enabled: true, - isNew: true, - actions: [], - kibanaSiemAppUrl: '', - throttle: THROTTLE_OPTIONS[0].value, -}; - -const GhostFormField = () => <>; - -const StepRuleActionsComponent: FC = ({ - addPadding = false, - defaultValues, - isReadOnlyView, - isLoading, - isUpdateView = false, - setStepData, - setForm, - actionMessageParams, -}) => { - const [myStepData, setMyStepData] = useState(stepActionsDefaultValue); - const { - services: { application }, - } = useKibana(); - - const { form } = useForm({ - defaultValue: myStepData, - options: { stripEmptyFields: false }, - schema, - }); - - const kibanaAbsoluteUrl = useMemo(() => application.getUrlForApp('siem', { absolute: true }), [ - application, - ]); - - const onSubmit = useCallback( - async (enabled: boolean) => { - if (setStepData) { - setStepData(RuleStep.ruleActions, null, false); - const { isValid: newIsValid, data } = await form.submit(); - if (newIsValid) { - setStepData(RuleStep.ruleActions, { ...data, enabled }, newIsValid); - setMyStepData({ ...data, isNew: false } as ActionsStepRule); - } - } - }, - [form] - ); - - useEffect(() => { - const { isNew, ...initDefaultValue } = myStepData; - if (defaultValues != null && !deepEqual(initDefaultValue, defaultValues)) { - const myDefaultValues = { - ...defaultValues, - isNew: false, - }; - setMyStepData(myDefaultValues); - setFieldValue(form, schema, myDefaultValues); - } - }, [defaultValues]); - - useEffect(() => { - if (setForm != null) { - setForm(RuleStep.ruleActions, form); - } - }, [form]); - - const updateThrottle = useCallback(throttle => setMyStepData({ ...myStepData, throttle }), [ - myStepData, - setMyStepData, - ]); - - const throttleFieldComponentProps = useMemo( - () => ({ - idAria: 'detectionEngineStepRuleActionsThrottle', - isDisabled: isLoading, - dataTestSubj: 'detectionEngineStepRuleActionsThrottle', - hasNoInitialSelection: false, - handleChange: updateThrottle, - euiFieldProps: { - options: THROTTLE_OPTIONS, - }, - }), - [isLoading, updateThrottle] - ); - - return isReadOnlyView && myStepData != null ? ( - - - - ) : ( - <> - -
- - {myStepData.throttle !== stepActionsDefaultValue.throttle && ( - <> - - - - - )} - - -
- - {!isUpdateView && ( - <> - - - - - {I18n.COMPLETE_WITHOUT_ACTIVATING} - - - - - {I18n.COMPLETE_WITH_ACTIVATING} - - - - - )} - - ); -}; - -export const StepRuleActions = memo(StepRuleActionsComponent); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/schema.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/schema.tsx deleted file mode 100644 index 1b27d0e0fcc0ed..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_rule_actions/schema.tsx +++ /dev/null @@ -1,32 +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. - */ - -/* istanbul ignore file */ - -import { i18n } from '@kbn/i18n'; - -import { FormSchema } from '../../../../../shared_imports'; - -export const schema: FormSchema = { - actions: {}, - enabled: {}, - kibanaSiemAppUrl: {}, - throttle: { - label: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepRuleActions.fieldThrottleLabel', - { - defaultMessage: 'Actions frequency', - } - ), - helpText: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepRuleActions.fieldThrottleHelpText', - { - defaultMessage: - 'Select when automated actions should be performed if a rule evaluates as true.', - } - ), - }, -}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.test.tsx deleted file mode 100644 index 98de933590d606..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.test.tsx +++ /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 React from 'react'; -import { shallow, mount } from 'enzyme'; - -import { TestProviders } from '../../../../../mock'; -import { StepScheduleRule } from './index'; - -describe('StepScheduleRule', () => { - it('renders correctly', () => { - const wrapper = mount(, { - wrappingComponent: TestProviders, - }); - - expect(wrapper.find('Form[data-test-subj="stepScheduleRule"]')).toHaveLength(1); - }); - - it('renders correctly if isReadOnlyView', () => { - const wrapper = shallow(); - - expect(wrapper.find('StepContentWrapper')).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx deleted file mode 100644 index de9abcefdea2e6..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/index.tsx +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { FC, memo, useCallback, useEffect, useState } from 'react'; -import deepEqual from 'fast-deep-equal'; -import styled from 'styled-components'; - -import { setFieldValue } from '../../helpers'; -import { RuleStep, RuleStepProps, ScheduleStepRule } from '../../types'; -import { StepRuleDescription } from '../description_step'; -import { ScheduleItem } from '../schedule_item_form'; -import { Form, UseField, useForm } from '../../../../../shared_imports'; -import { StepContentWrapper } from '../step_content_wrapper'; -import { NextStep } from '../next_step'; -import { schema } from './schema'; - -interface StepScheduleRuleProps extends RuleStepProps { - defaultValues?: ScheduleStepRule | null; -} - -const RestrictedWidthContainer = styled.div` - max-width: 300px; -`; - -const stepScheduleDefaultValue = { - interval: '5m', - isNew: true, - from: '1m', -}; - -const StepScheduleRuleComponent: FC = ({ - addPadding = false, - defaultValues, - descriptionColumns = 'singleSplit', - isReadOnlyView, - isLoading, - isUpdateView = false, - setStepData, - setForm, -}) => { - const [myStepData, setMyStepData] = useState(stepScheduleDefaultValue); - - const { form } = useForm({ - defaultValue: myStepData, - options: { stripEmptyFields: false }, - schema, - }); - - const onSubmit = useCallback(async () => { - if (setStepData) { - setStepData(RuleStep.scheduleRule, null, false); - const { isValid: newIsValid, data } = await form.submit(); - if (newIsValid) { - setStepData(RuleStep.scheduleRule, { ...data }, newIsValid); - setMyStepData({ ...data, isNew: false } as ScheduleStepRule); - } - } - }, [form]); - - useEffect(() => { - const { isNew, ...initDefaultValue } = myStepData; - if (defaultValues != null && !deepEqual(initDefaultValue, defaultValues)) { - const myDefaultValues = { - ...defaultValues, - isNew: false, - }; - setMyStepData(myDefaultValues); - setFieldValue(form, schema, myDefaultValues); - } - }, [defaultValues]); - - useEffect(() => { - if (setForm != null) { - setForm(RuleStep.scheduleRule, form); - } - }, [form]); - - return isReadOnlyView && myStepData != null ? ( - - - - ) : ( - <> - -
- - - - - - -
-
- - {!isUpdateView && ( - - )} - - ); -}; - -export const StepScheduleRule = memo(StepScheduleRuleComponent); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx deleted file mode 100644 index e79aec2be6e153..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/step_schedule_rule/schema.tsx +++ /dev/null @@ -1,45 +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. - */ - -/* istanbul ignore file */ - -import { i18n } from '@kbn/i18n'; - -import { OptionalFieldLabel } from '../optional_field_label'; -import { FormSchema } from '../../../../../shared_imports'; - -export const schema: FormSchema = { - interval: { - label: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldIntervalLabel', - { - defaultMessage: 'Runs every', - } - ), - helpText: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldIntervalHelpText', - { - defaultMessage: - 'Rules run periodically and detect signals within the specified time frame.', - } - ), - }, - from: { - label: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldAdditionalLookBackLabel', - { - defaultMessage: 'Additional look-back time', - } - ), - labelAppend: OptionalFieldLabel, - helpText: i18n.translate( - 'xpack.siem.detectionEngine.createRule.stepScheduleRule.fieldAdditionalLookBackHelpText', - { - defaultMessage: 'Adds time to the look-back period to prevent missed signals.', - } - ), - }, -}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.test.tsx deleted file mode 100644 index 0ab19b671494ef..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.test.tsx +++ /dev/null @@ -1,24 +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 { shallow } from 'enzyme'; - -import { ThrottleSelectField } from './index'; -import { useFormFieldMock } from '../../../../../mock'; - -describe('ThrottleSelectField', () => { - it('renders correctly', () => { - const Component = () => { - const field = useFormFieldMock(); - - return ; - }; - const wrapper = shallow(); - - expect(wrapper.dive().find('SelectField')).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.tsx deleted file mode 100644 index 0cf15c41a0f913..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/components/throttle_select_field/index.tsx +++ /dev/null @@ -1,36 +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, { useCallback } from 'react'; - -import { - NOTIFICATION_THROTTLE_RULE, - NOTIFICATION_THROTTLE_NO_ACTIONS, -} from '../../../../../../common/constants'; -import { SelectField } from '../../../../../shared_imports'; - -export const THROTTLE_OPTIONS = [ - { value: NOTIFICATION_THROTTLE_NO_ACTIONS, text: 'Perform no actions' }, - { value: NOTIFICATION_THROTTLE_RULE, text: 'On each rule execution' }, - { value: '1h', text: 'Hourly' }, - { value: '1d', text: 'Daily' }, - { value: '7d', text: 'Weekly' }, -]; - -type ThrottleSelectField = typeof SelectField; - -export const ThrottleSelectField: ThrottleSelectField = props => { - const onChange = useCallback( - e => { - const throttle = e.target.value; - props.field.setValue(throttle); - props.handleChange(throttle); - }, - [props.field.setValue, props.handleChange] - ); - const newEuiFieldProps = { ...props.euiFieldProps, onChange }; - return ; -}; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts deleted file mode 100644 index 8d793f39afa997..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/create/helpers.test.ts +++ /dev/null @@ -1,730 +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 { NewRule } from '../../../../containers/detection_engine/rules'; -import { - DefineStepRuleJson, - ScheduleStepRuleJson, - AboutStepRuleJson, - ActionsStepRuleJson, - AboutStepRule, - ActionsStepRule, - ScheduleStepRule, - DefineStepRule, -} from '../types'; -import { - getTimeTypeValue, - formatDefineStepData, - formatScheduleStepData, - formatAboutStepData, - formatActionsStepData, - formatRule, - filterRuleFieldsForType, -} from './helpers'; -import { - mockDefineStepRule, - mockQueryBar, - mockScheduleStepRule, - mockAboutStepRule, - mockActionsStepRule, -} from '../all/__mocks__/mock'; - -describe('helpers', () => { - describe('getTimeTypeValue', () => { - test('returns timeObj with value 0 if no time value found', () => { - const result = getTimeTypeValue('m'); - - expect(result).toEqual({ unit: 'm', value: 0 }); - }); - - test('returns timeObj with unit set to empty string if no expected time type found', () => { - const result = getTimeTypeValue('5l'); - - expect(result).toEqual({ unit: '', value: 5 }); - }); - - test('returns timeObj with unit of s and value 5 when time is 5s ', () => { - const result = getTimeTypeValue('5s'); - - expect(result).toEqual({ unit: 's', value: 5 }); - }); - - test('returns timeObj with unit of m and value 5 when time is 5m ', () => { - const result = getTimeTypeValue('5m'); - - expect(result).toEqual({ unit: 'm', value: 5 }); - }); - - test('returns timeObj with unit of h and value 5 when time is 5h ', () => { - const result = getTimeTypeValue('5h'); - - expect(result).toEqual({ unit: 'h', value: 5 }); - }); - - test('returns timeObj with value of 5 when time is float like 5.6m ', () => { - const result = getTimeTypeValue('5m'); - - expect(result).toEqual({ unit: 'm', value: 5 }); - }); - - test('returns timeObj with value of 0 and unit of "" if random string passed in', () => { - const result = getTimeTypeValue('random'); - - expect(result).toEqual({ unit: '', value: 0 }); - }); - }); - - describe('formatDefineStepData', () => { - let mockData: DefineStepRule; - - beforeEach(() => { - mockData = mockDefineStepRule(); - }); - - test('returns formatted object as DefineStepRuleJson', () => { - const result: DefineStepRuleJson = formatDefineStepData(mockData); - const expected = { - language: 'kuery', - filters: mockQueryBar.filters, - query: 'test query', - saved_id: 'test123', - index: ['filebeat-'], - type: 'saved_query', - timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', - timeline_title: 'Titled timeline', - }; - - expect(result).toEqual(expected); - }); - - test('returns formatted object with no saved_id if no savedId provided', () => { - const mockStepData = { - ...mockData, - queryBar: { - ...mockData.queryBar, - saved_id: '', - }, - }; - const result: DefineStepRuleJson = formatDefineStepData(mockStepData); - const expected = { - language: 'kuery', - filters: mockQueryBar.filters, - query: 'test query', - index: ['filebeat-'], - saved_id: '', - type: 'query', - timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', - timeline_title: 'Titled timeline', - }; - - expect(result).toEqual(expected); - }); - - test('returns formatted object without timeline_id and timeline_title if timeline.id is null', () => { - const mockStepData = { - ...mockData, - }; - delete mockStepData.timeline.id; - - const result: DefineStepRuleJson = formatDefineStepData(mockStepData); - - const expected = { - language: 'kuery', - filters: mockQueryBar.filters, - query: 'test query', - index: ['filebeat-'], - saved_id: 'test123', - type: 'saved_query', - }; - - expect(result).toEqual(expected); - }); - - test('returns formatted object with timeline_id and timeline_title if timeline.id is "', () => { - const mockStepData = { - ...mockData, - timeline: { - ...mockData.timeline, - id: '', - }, - }; - const result: DefineStepRuleJson = formatDefineStepData(mockStepData); - - const expected = { - language: 'kuery', - filters: mockQueryBar.filters, - query: 'test query', - index: ['filebeat-'], - saved_id: 'test123', - type: 'saved_query', - timeline_id: '', - timeline_title: 'Titled timeline', - }; - - expect(result).toEqual(expected); - }); - - test('returns formatted object without timeline_id and timeline_title if timeline.title is null', () => { - const mockStepData = { - ...mockData, - timeline: { - ...mockData.timeline, - id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', - }, - }; - delete mockStepData.timeline.title; - const result: DefineStepRuleJson = formatDefineStepData(mockStepData); - - const expected = { - language: 'kuery', - filters: mockQueryBar.filters, - query: 'test query', - index: ['filebeat-'], - saved_id: 'test123', - type: 'saved_query', - }; - - expect(result).toEqual(expected); - }); - - test('returns formatted object with timeline_id and timeline_title if timeline.title is "', () => { - const mockStepData = { - ...mockData, - timeline: { - ...mockData.timeline, - title: '', - }, - }; - const result: DefineStepRuleJson = formatDefineStepData(mockStepData); - - const expected = { - language: 'kuery', - filters: mockQueryBar.filters, - query: 'test query', - index: ['filebeat-'], - saved_id: 'test123', - type: 'saved_query', - timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', - timeline_title: '', - }; - - expect(result).toEqual(expected); - }); - - test('returns ML fields if type is machine_learning', () => { - const mockStepData: DefineStepRule = { - ...mockData, - ruleType: 'machine_learning', - anomalyThreshold: 44, - machineLearningJobId: 'some_jobert_id', - }; - const result: DefineStepRuleJson = formatDefineStepData(mockStepData); - - const expected = { - type: 'machine_learning', - anomaly_threshold: 44, - machine_learning_job_id: 'some_jobert_id', - timeline_id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', - timeline_title: 'Titled timeline', - }; - - expect(result).toEqual(expected); - }); - }); - - describe('formatScheduleStepData', () => { - let mockData: ScheduleStepRule; - - beforeEach(() => { - mockData = mockScheduleStepRule(); - }); - - test('returns formatted object as ScheduleStepRuleJson', () => { - const result: ScheduleStepRuleJson = formatScheduleStepData(mockData); - const expected = { - from: 'now-660s', - to: 'now', - interval: '5m', - meta: { - from: '6m', - }, - }; - - expect(result).toEqual(expected); - }); - - test('returns formatted object with "to" as "now" if "to" not supplied', () => { - const mockStepData = { - ...mockData, - }; - delete mockStepData.to; - const result: ScheduleStepRuleJson = formatScheduleStepData(mockStepData); - const expected = { - from: 'now-660s', - to: 'now', - interval: '5m', - meta: { - from: '6m', - }, - }; - - expect(result).toEqual(expected); - }); - - test('returns formatted object with "to" as "now" if "to" random string', () => { - const mockStepData = { - ...mockData, - to: 'random', - }; - const result: ScheduleStepRuleJson = formatScheduleStepData(mockStepData); - const expected = { - from: 'now-660s', - to: 'now', - interval: '5m', - meta: { - from: '6m', - }, - }; - - expect(result).toEqual(expected); - }); - - test('returns formatted object if "from" random string', () => { - const mockStepData = { - ...mockData, - from: 'random', - }; - const result: ScheduleStepRuleJson = formatScheduleStepData(mockStepData); - const expected = { - from: 'now-300s', - to: 'now', - interval: '5m', - meta: { - from: 'random', - }, - }; - - expect(result).toEqual(expected); - }); - - test('returns formatted object if "interval" random string', () => { - const mockStepData = { - ...mockData, - interval: 'random', - }; - const result: ScheduleStepRuleJson = formatScheduleStepData(mockStepData); - const expected = { - from: 'now-360s', - to: 'now', - interval: 'random', - meta: { - from: '6m', - }, - }; - - expect(result).toEqual(expected); - }); - }); - - describe('formatAboutStepData', () => { - let mockData: AboutStepRule; - - beforeEach(() => { - mockData = mockAboutStepRule(); - }); - - test('returns formatted object as AboutStepRuleJson', () => { - const result: AboutStepRuleJson = formatAboutStepData(mockData); - const expected = { - description: '24/7', - false_positives: ['test'], - name: 'Query with rule-id', - note: '# this is some markdown documentation', - references: ['www.test.co'], - risk_score: 21, - severity: 'low', - tags: ['tag1', 'tag2'], - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: '1234', - name: 'tactic1', - reference: 'reference1', - }, - technique: [ - { - id: '456', - name: 'technique1', - reference: 'technique reference', - }, - ], - }, - ], - }; - - expect(result).toEqual(expected); - }); - - test('returns formatted object with empty falsePositive and references filtered out', () => { - const mockStepData = { - ...mockData, - falsePositives: ['', 'test', ''], - references: ['www.test.co', ''], - }; - const result: AboutStepRuleJson = formatAboutStepData(mockStepData); - const expected = { - description: '24/7', - false_positives: ['test'], - name: 'Query with rule-id', - note: '# this is some markdown documentation', - references: ['www.test.co'], - risk_score: 21, - severity: 'low', - tags: ['tag1', 'tag2'], - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: '1234', - name: 'tactic1', - reference: 'reference1', - }, - technique: [ - { - id: '456', - name: 'technique1', - reference: 'technique reference', - }, - ], - }, - ], - }; - - expect(result).toEqual(expected); - }); - - test('returns formatted object without note if note is empty string', () => { - const mockStepData = { - ...mockData, - note: '', - }; - const result: AboutStepRuleJson = formatAboutStepData(mockStepData); - const expected = { - description: '24/7', - false_positives: ['test'], - name: 'Query with rule-id', - references: ['www.test.co'], - risk_score: 21, - severity: 'low', - tags: ['tag1', 'tag2'], - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { - id: '1234', - name: 'tactic1', - reference: 'reference1', - }, - technique: [ - { - id: '456', - name: 'technique1', - reference: 'technique reference', - }, - ], - }, - ], - }; - - expect(result).toEqual(expected); - }); - - test('returns formatted object with threats filtered out where tactic.name is "none"', () => { - const mockStepData = { - ...mockData, - threat: [ - { - framework: 'mockFramework', - tactic: { - id: '1234', - name: 'tactic1', - reference: 'reference1', - }, - technique: [ - { - id: '456', - name: 'technique1', - reference: 'technique reference', - }, - ], - }, - { - framework: 'mockFramework', - tactic: { - id: '1234', - name: 'none', - reference: 'reference1', - }, - technique: [ - { - id: '456', - name: 'technique1', - reference: 'technique reference', - }, - ], - }, - ], - }; - const result: AboutStepRuleJson = formatAboutStepData(mockStepData); - const expected = { - description: '24/7', - false_positives: ['test'], - name: 'Query with rule-id', - note: '# this is some markdown documentation', - references: ['www.test.co'], - risk_score: 21, - severity: 'low', - tags: ['tag1', 'tag2'], - threat: [ - { - framework: 'MITRE ATT&CK', - tactic: { id: '1234', name: 'tactic1', reference: 'reference1' }, - technique: [{ id: '456', name: 'technique1', reference: 'technique reference' }], - }, - ], - }; - - expect(result).toEqual(expected); - }); - }); - - describe('formatActionsStepData', () => { - let mockData: ActionsStepRule; - - beforeEach(() => { - mockData = mockActionsStepRule(); - }); - - test('returns formatted object as ActionsStepRuleJson', () => { - const result: ActionsStepRuleJson = formatActionsStepData(mockData); - const expected = { - actions: [], - enabled: false, - meta: { - kibana_siem_app_url: 'http://localhost:5601/app/siem', - }, - throttle: 'no_actions', - }; - - expect(result).toEqual(expected); - }); - - test('returns proper throttle value for no_actions', () => { - const mockStepData = { - ...mockData, - throttle: 'no_actions', - }; - const result: ActionsStepRuleJson = formatActionsStepData(mockStepData); - const expected = { - actions: [], - enabled: false, - meta: { - kibana_siem_app_url: mockStepData.kibanaSiemAppUrl, - }, - throttle: 'no_actions', - }; - - expect(result).toEqual(expected); - }); - - test('returns proper throttle value for rule', () => { - const mockStepData = { - ...mockData, - throttle: 'rule', - actions: [ - { - group: 'default', - id: 'id', - actionTypeId: 'actionTypeId', - params: {}, - }, - ], - }; - const result: ActionsStepRuleJson = formatActionsStepData(mockStepData); - const expected = { - actions: [ - { - group: mockStepData.actions[0].group, - id: mockStepData.actions[0].id, - action_type_id: mockStepData.actions[0].actionTypeId, - params: mockStepData.actions[0].params, - }, - ], - enabled: false, - meta: { - kibana_siem_app_url: mockStepData.kibanaSiemAppUrl, - }, - throttle: 'rule', - }; - - expect(result).toEqual(expected); - }); - - test('returns proper throttle value for interval', () => { - const mockStepData = { - ...mockData, - throttle: '1d', - actions: [ - { - group: 'default', - id: 'id', - actionTypeId: 'actionTypeId', - params: {}, - }, - ], - }; - const result: ActionsStepRuleJson = formatActionsStepData(mockStepData); - const expected = { - actions: [ - { - group: mockStepData.actions[0].group, - id: mockStepData.actions[0].id, - action_type_id: mockStepData.actions[0].actionTypeId, - params: mockStepData.actions[0].params, - }, - ], - enabled: false, - meta: { - kibana_siem_app_url: mockStepData.kibanaSiemAppUrl, - }, - throttle: mockStepData.throttle, - }; - - expect(result).toEqual(expected); - }); - - test('returns actions with action_type_id', () => { - const mockAction = { - group: 'default', - id: '99403909-ca9b-49ba-9d7a-7e5320e68d05', - params: { message: 'ML Rule generated {{state.signals_count}} signals' }, - actionTypeId: '.slack', - }; - - const mockStepData = { - ...mockData, - actions: [mockAction], - }; - const result: ActionsStepRuleJson = formatActionsStepData(mockStepData); - const expected = { - actions: [ - { - group: mockAction.group, - id: mockAction.id, - params: mockAction.params, - action_type_id: mockAction.actionTypeId, - }, - ], - enabled: false, - meta: { - kibana_siem_app_url: mockStepData.kibanaSiemAppUrl, - }, - throttle: 'no_actions', - }; - - expect(result).toEqual(expected); - }); - }); - - describe('formatRule', () => { - let mockAbout: AboutStepRule; - let mockDefine: DefineStepRule; - let mockSchedule: ScheduleStepRule; - let mockActions: ActionsStepRule; - - beforeEach(() => { - mockAbout = mockAboutStepRule(); - mockDefine = mockDefineStepRule(); - mockSchedule = mockScheduleStepRule(); - mockActions = mockActionsStepRule(); - }); - - test('returns NewRule with type of saved_query when saved_id exists', () => { - const result: NewRule = formatRule(mockDefine, mockAbout, mockSchedule, mockActions); - - expect(result.type).toEqual('saved_query'); - }); - - test('returns NewRule with type of query when saved_id does not exist', () => { - const mockDefineStepRuleWithoutSavedId = { - ...mockDefine, - queryBar: { - ...mockDefine.queryBar, - saved_id: '', - }, - }; - const result: NewRule = formatRule( - mockDefineStepRuleWithoutSavedId, - mockAbout, - mockSchedule, - mockActions - ); - - expect(result.type).toEqual('query'); - }); - - test('returns NewRule without id if ruleId does not exist', () => { - const result: NewRule = formatRule(mockDefine, mockAbout, mockSchedule, mockActions); - - expect(result.id).toBeUndefined(); - }); - }); - - describe('filterRuleFieldsForType', () => { - let fields: DefineStepRule; - - beforeEach(() => { - fields = mockDefineStepRule(); - }); - - it('removes query fields if the type is machine learning', () => { - const result = filterRuleFieldsForType(fields, 'machine_learning'); - expect(result).not.toHaveProperty('index'); - expect(result).not.toHaveProperty('queryBar'); - }); - - it('leaves ML fields if the type is machine learning', () => { - const result = filterRuleFieldsForType(fields, 'machine_learning'); - expect(result).toHaveProperty('anomalyThreshold'); - expect(result).toHaveProperty('machineLearningJobId'); - }); - - it('leaves arbitrary fields if the type is machine learning', () => { - const result = filterRuleFieldsForType(fields, 'machine_learning'); - expect(result).toHaveProperty('timeline'); - expect(result).toHaveProperty('ruleType'); - }); - - it('removes ML fields if the type is not machine learning', () => { - const result = filterRuleFieldsForType(fields, 'query'); - expect(result).not.toHaveProperty('anomalyThreshold'); - expect(result).not.toHaveProperty('machineLearningJobId'); - }); - - it('leaves query fields if the type is query', () => { - const result = filterRuleFieldsForType(fields, 'query'); - expect(result).toHaveProperty('index'); - expect(result).toHaveProperty('queryBar'); - }); - - it('leaves arbitrary fields if the type is query', () => { - const result = filterRuleFieldsForType(fields, 'query'); - expect(result).toHaveProperty('timeline'); - expect(result).toHaveProperty('ruleType'); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts deleted file mode 100644 index 7ad116c313361d..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/create/helpers.ts +++ /dev/null @@ -1,174 +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 { has, isEmpty } from 'lodash/fp'; -import moment from 'moment'; -import deepmerge from 'deepmerge'; - -import { NOTIFICATION_THROTTLE_NO_ACTIONS } from '../../../../../common/constants'; -import { transformAlertToRuleAction } from '../../../../../common/detection_engine/transform_actions'; -import { RuleType } from '../../../../../common/detection_engine/types'; -import { isMlRule } from '../../../../../common/detection_engine/ml_helpers'; -import { NewRule } from '../../../../containers/detection_engine/rules'; - -import { - AboutStepRule, - DefineStepRule, - ScheduleStepRule, - ActionsStepRule, - DefineStepRuleJson, - ScheduleStepRuleJson, - AboutStepRuleJson, - ActionsStepRuleJson, -} from '../types'; - -export const getTimeTypeValue = (time: string): { unit: string; value: number } => { - const timeObj = { - unit: '', - value: 0, - }; - const filterTimeVal = (time as string).match(/\d+/g); - const filterTimeType = (time as string).match(/[a-zA-Z]+/g); - if (!isEmpty(filterTimeVal) && filterTimeVal != null && !isNaN(Number(filterTimeVal[0]))) { - timeObj.value = Number(filterTimeVal[0]); - } - if ( - !isEmpty(filterTimeType) && - filterTimeType != null && - ['s', 'm', 'h'].includes(filterTimeType[0]) - ) { - timeObj.unit = filterTimeType[0]; - } - return timeObj; -}; - -export interface RuleFields { - anomalyThreshold: unknown; - machineLearningJobId: unknown; - queryBar: unknown; - index: unknown; - ruleType: unknown; -} -type QueryRuleFields = Omit; -type MlRuleFields = Omit; - -const isMlFields = (fields: QueryRuleFields | MlRuleFields): fields is MlRuleFields => - has('anomalyThreshold', fields); - -export const filterRuleFieldsForType = (fields: T, type: RuleType) => { - if (isMlRule(type)) { - const { index, queryBar, ...mlRuleFields } = fields; - return mlRuleFields; - } else { - const { anomalyThreshold, machineLearningJobId, ...queryRuleFields } = fields; - return queryRuleFields; - } -}; - -export const formatDefineStepData = (defineStepData: DefineStepRule): DefineStepRuleJson => { - const ruleFields = filterRuleFieldsForType(defineStepData, defineStepData.ruleType); - const { ruleType, timeline } = ruleFields; - const baseFields = { - type: ruleType, - ...(timeline.id != null && - timeline.title != null && { - timeline_id: timeline.id, - timeline_title: timeline.title, - }), - }; - - const typeFields = isMlFields(ruleFields) - ? { - anomaly_threshold: ruleFields.anomalyThreshold, - machine_learning_job_id: ruleFields.machineLearningJobId, - } - : { - index: ruleFields.index, - filters: ruleFields.queryBar?.filters, - language: ruleFields.queryBar?.query?.language, - query: ruleFields.queryBar?.query?.query as string, - saved_id: ruleFields.queryBar?.saved_id, - ...(ruleType === 'query' && - ruleFields.queryBar?.saved_id && { type: 'saved_query' as RuleType }), - }; - - return { - ...baseFields, - ...typeFields, - }; -}; - -export const formatScheduleStepData = (scheduleData: ScheduleStepRule): ScheduleStepRuleJson => { - const { isNew, ...formatScheduleData } = scheduleData; - if (!isEmpty(formatScheduleData.interval) && !isEmpty(formatScheduleData.from)) { - const { unit: intervalUnit, value: intervalValue } = getTimeTypeValue( - formatScheduleData.interval - ); - const { unit: fromUnit, value: fromValue } = getTimeTypeValue(formatScheduleData.from); - const duration = moment.duration(intervalValue, intervalUnit as 's' | 'm' | 'h'); - duration.add(fromValue, fromUnit as 's' | 'm' | 'h'); - formatScheduleData.from = `now-${duration.asSeconds()}s`; - formatScheduleData.to = 'now'; - } - return { - ...formatScheduleData, - meta: { - from: scheduleData.from, - }, - }; -}; - -export const formatAboutStepData = (aboutStepData: AboutStepRule): AboutStepRuleJson => { - const { falsePositives, references, riskScore, threat, isNew, note, ...rest } = aboutStepData; - return { - false_positives: falsePositives.filter(item => !isEmpty(item)), - references: references.filter(item => !isEmpty(item)), - risk_score: riskScore, - threat: threat - .filter(singleThreat => singleThreat.tactic.name !== 'none') - .map(singleThreat => ({ - ...singleThreat, - framework: 'MITRE ATT&CK', - technique: singleThreat.technique.map(technique => { - const { id, name, reference } = technique; - return { id, name, reference }; - }), - })), - ...(!isEmpty(note) ? { note } : {}), - ...rest, - }; -}; - -export const formatActionsStepData = (actionsStepData: ActionsStepRule): ActionsStepRuleJson => { - const { - actions = [], - enabled, - kibanaSiemAppUrl, - throttle = NOTIFICATION_THROTTLE_NO_ACTIONS, - } = actionsStepData; - - return { - actions: actions.map(transformAlertToRuleAction), - enabled, - throttle: actions.length ? throttle : NOTIFICATION_THROTTLE_NO_ACTIONS, - meta: { - kibana_siem_app_url: kibanaSiemAppUrl, - }, - }; -}; - -export const formatRule = ( - defineStepData: DefineStepRule, - aboutStepData: AboutStepRule, - scheduleData: ScheduleStepRule, - actionsData: ActionsStepRule -): NewRule => - deepmerge.all([ - formatDefineStepData(defineStepData), - formatAboutStepData(aboutStepData), - formatScheduleStepData(scheduleData), - formatActionsStepData(actionsData), - ]) as NewRule; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/create/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/create/index.test.tsx deleted file mode 100644 index db32be652d0f76..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/create/index.test.tsx +++ /dev/null @@ -1,23 +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 { shallow } from 'enzyme'; - -import { TestProviders } from '../../../../mock'; -import { CreateRulePage } from './index'; -import { useUserInfo } from '../../components/user_info'; - -jest.mock('../../components/user_info'); - -describe('CreateRulePage', () => { - it('renders correctly', () => { - (useUserInfo as jest.Mock).mockReturnValue({}); - const wrapper = shallow(, { wrappingComponent: TestProviders }); - - expect(wrapper.find('[title="Create new rule"]')).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/create/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/create/index.tsx deleted file mode 100644 index 2686bb47925b6c..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/create/index.tsx +++ /dev/null @@ -1,428 +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 { EuiButtonEmpty, EuiAccordion, EuiHorizontalRule, EuiPanel, EuiSpacer } from '@elastic/eui'; -import React, { useCallback, useRef, useState, useMemo } from 'react'; -import { Redirect } from 'react-router-dom'; -import styled, { StyledComponent } from 'styled-components'; - -import { usePersistRule } from '../../../../containers/detection_engine/rules'; - -import { DETECTION_ENGINE_PAGE_NAME } from '../../../../components/link_to/redirect_to_detection_engine'; -import { WrapperPage } from '../../../../components/wrapper_page'; -import { displaySuccessToast, useStateToaster } from '../../../../components/toasters'; -import { SpyRoute } from '../../../../utils/route/spy_routes'; -import { useUserInfo } from '../../components/user_info'; -import { AccordionTitle } from '../components/accordion_title'; -import { FormData, FormHook } from '../../../../shared_imports'; -import { StepAboutRule } from '../components/step_about_rule'; -import { StepDefineRule } from '../components/step_define_rule'; -import { StepScheduleRule } from '../components/step_schedule_rule'; -import { StepRuleActions } from '../components/step_rule_actions'; -import { DetectionEngineHeaderPage } from '../../components/detection_engine_header_page'; -import * as RuleI18n from '../translations'; -import { redirectToDetections, getActionMessageParams, userHasNoPermissions } from '../helpers'; -import { - AboutStepRule, - DefineStepRule, - RuleStep, - RuleStepData, - ScheduleStepRule, - ActionsStepRule, -} from '../types'; -import { formatRule } from './helpers'; -import * as i18n from './translations'; - -const stepsRuleOrder = [ - RuleStep.defineRule, - RuleStep.aboutRule, - RuleStep.scheduleRule, - RuleStep.ruleActions, -]; - -const MyEuiPanel = styled(EuiPanel)<{ - zindex?: number; -}>` - position: relative; - z-index: ${props => props.zindex}; /* ugly fix to allow searchBar to overflow the EuiPanel */ - - > .euiAccordion > .euiAccordion__triggerWrapper { - .euiAccordion__button { - cursor: default !important; - &:hover { - text-decoration: none !important; - } - } - - .euiAccordion__iconWrapper { - display: none; - } - } -`; - -MyEuiPanel.displayName = 'MyEuiPanel'; - -const StepDefineRuleAccordion: StyledComponent< - typeof EuiAccordion, - any, // eslint-disable-line - { ref: React.MutableRefObject }, - never -> = styled(EuiAccordion)` - .euiAccordion__childWrapper { - overflow: visible; - } -`; - -StepDefineRuleAccordion.displayName = 'StepDefineRuleAccordion'; - -const CreateRulePageComponent: React.FC = () => { - const { - loading, - isSignalIndexExists, - isAuthenticated, - hasEncryptionKey, - canUserCRUD, - } = useUserInfo(); - const [, dispatchToaster] = useStateToaster(); - const [openAccordionId, setOpenAccordionId] = useState(RuleStep.defineRule); - const defineRuleRef = useRef(null); - const aboutRuleRef = useRef(null); - const scheduleRuleRef = useRef(null); - const ruleActionsRef = useRef(null); - const stepsForm = useRef | null>>({ - [RuleStep.defineRule]: null, - [RuleStep.aboutRule]: null, - [RuleStep.scheduleRule]: null, - [RuleStep.ruleActions]: null, - }); - const stepsData = useRef>({ - [RuleStep.defineRule]: { isValid: false, data: {} }, - [RuleStep.aboutRule]: { isValid: false, data: {} }, - [RuleStep.scheduleRule]: { isValid: false, data: {} }, - [RuleStep.ruleActions]: { isValid: false, data: {} }, - }); - const [isStepRuleInReadOnlyView, setIsStepRuleInEditView] = useState>({ - [RuleStep.defineRule]: false, - [RuleStep.aboutRule]: false, - [RuleStep.scheduleRule]: false, - [RuleStep.ruleActions]: false, - }); - const [{ isLoading, isSaved }, setRule] = usePersistRule(); - const actionMessageParams = useMemo( - () => - getActionMessageParams((stepsData.current['define-rule'].data as DefineStepRule).ruleType), - [stepsData.current['define-rule'].data] - ); - - const setStepData = useCallback( - (step: RuleStep, data: unknown, isValid: boolean) => { - stepsData.current[step] = { ...stepsData.current[step], data, isValid }; - if (isValid) { - const stepRuleIdx = stepsRuleOrder.findIndex(item => step === item); - if ([0, 1, 2].includes(stepRuleIdx)) { - if (isStepRuleInReadOnlyView[stepsRuleOrder[stepRuleIdx + 1]]) { - setOpenAccordionId(stepsRuleOrder[stepRuleIdx + 1]); - setIsStepRuleInEditView({ - ...isStepRuleInReadOnlyView, - [step]: true, - [stepsRuleOrder[stepRuleIdx + 1]]: false, - }); - } else if (openAccordionId !== stepsRuleOrder[stepRuleIdx + 1]) { - setIsStepRuleInEditView({ - ...isStepRuleInReadOnlyView, - [step]: true, - }); - openCloseAccordion(stepsRuleOrder[stepRuleIdx + 1]); - setOpenAccordionId(stepsRuleOrder[stepRuleIdx + 1]); - } - } else if ( - stepRuleIdx === 3 && - stepsData.current[RuleStep.defineRule].isValid && - stepsData.current[RuleStep.aboutRule].isValid && - stepsData.current[RuleStep.scheduleRule].isValid - ) { - setRule( - formatRule( - stepsData.current[RuleStep.defineRule].data as DefineStepRule, - stepsData.current[RuleStep.aboutRule].data as AboutStepRule, - stepsData.current[RuleStep.scheduleRule].data as ScheduleStepRule, - stepsData.current[RuleStep.ruleActions].data as ActionsStepRule - ) - ); - } - } - }, - [isStepRuleInReadOnlyView, openAccordionId, stepsData.current, setRule] - ); - - const setStepsForm = useCallback((step: RuleStep, form: FormHook) => { - stepsForm.current[step] = form; - }, []); - - const getAccordionType = useCallback( - (accordionId: RuleStep) => { - if (accordionId === openAccordionId) { - return 'active'; - } else if (stepsData.current[accordionId].isValid) { - return 'valid'; - } - return 'passive'; - }, - [openAccordionId, stepsData.current] - ); - - const defineRuleButton = ( - - ); - - const aboutRuleButton = ( - - ); - - const scheduleRuleButton = ( - - ); - - const ruleActionsButton = ( - - ); - - const openCloseAccordion = (accordionId: RuleStep | null) => { - if (accordionId != null) { - if (accordionId === RuleStep.defineRule && defineRuleRef.current != null) { - defineRuleRef.current.onToggle(); - } else if (accordionId === RuleStep.aboutRule && aboutRuleRef.current != null) { - aboutRuleRef.current.onToggle(); - } else if (accordionId === RuleStep.scheduleRule && scheduleRuleRef.current != null) { - scheduleRuleRef.current.onToggle(); - } else if (accordionId === RuleStep.ruleActions && ruleActionsRef.current != null) { - ruleActionsRef.current.onToggle(); - } - } - }; - - // eslint-disable-next-line react-hooks/rules-of-hooks - const manageAccordions = useCallback( - (id: RuleStep, isOpen: boolean) => { - const activeRuleIdx = stepsRuleOrder.findIndex(step => step === openAccordionId); - const stepRuleIdx = stepsRuleOrder.findIndex(step => step === id); - - if ((id === openAccordionId || stepRuleIdx < activeRuleIdx) && !isOpen) { - openCloseAccordion(id); - } else if (stepRuleIdx >= activeRuleIdx) { - if ( - openAccordionId !== id && - !stepsData.current[openAccordionId].isValid && - !isStepRuleInReadOnlyView[id] && - isOpen - ) { - openCloseAccordion(id); - } - } - }, - [isStepRuleInReadOnlyView, openAccordionId, stepsData] - ); - - // eslint-disable-next-line react-hooks/rules-of-hooks - const manageIsEditable = useCallback( - async (id: RuleStep) => { - const activeForm = await stepsForm.current[openAccordionId]?.submit(); - if (activeForm != null && activeForm?.isValid) { - stepsData.current[openAccordionId] = { - ...stepsData.current[openAccordionId], - data: activeForm.data, - isValid: activeForm.isValid, - }; - setOpenAccordionId(id); - setIsStepRuleInEditView({ - ...isStepRuleInReadOnlyView, - [openAccordionId]: true, - [id]: false, - }); - } - }, - [isStepRuleInReadOnlyView, openAccordionId] - ); - - if (isSaved) { - const ruleName = (stepsData.current[RuleStep.aboutRule].data as AboutStepRule).name; - displaySuccessToast(i18n.SUCCESSFULLY_CREATED_RULES(ruleName), dispatchToaster); - return ; - } - - if (redirectToDetections(isSignalIndexExists, isAuthenticated, hasEncryptionKey)) { - return ; - } else if (userHasNoPermissions(canUserCRUD)) { - return ; - } - - return ( - <> - - - - - {i18n.EDIT_RULE} - - ) - } - > - - - - - - - - {i18n.EDIT_RULE} - - ) - } - > - - - - - - - - {i18n.EDIT_RULE} - - ) - } - > - - - - - - - - {i18n.EDIT_RULE} - - ) - } - > - - - - - - - - - ); -}; - -export const CreateRulePage = React.memo(CreateRulePageComponent); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/details/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/details/index.test.tsx deleted file mode 100644 index 19c6f39a9bc7ef..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/details/index.test.tsx +++ /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. - */ - -import React from 'react'; -import { shallow } from 'enzyme'; - -import '../../../../mock/match_media'; -import { TestProviders } from '../../../../mock'; -import { RuleDetailsPageComponent } from './index'; -import { setAbsoluteRangeDatePicker } from '../../../../store/inputs/actions'; -import { useUserInfo } from '../../components/user_info'; -import { useParams } from 'react-router-dom'; - -jest.mock('../../components/user_info'); -jest.mock('react-router-dom', () => { - const originalModule = jest.requireActual('react-router-dom'); - - return { - ...originalModule, - useParams: jest.fn(), - }; -}); - -describe('RuleDetailsPageComponent', () => { - beforeAll(() => { - (useUserInfo as jest.Mock).mockReturnValue({}); - (useParams as jest.Mock).mockReturnValue({}); - }); - - it('renders correctly', () => { - const wrapper = shallow( - , - { - wrappingComponent: TestProviders, - } - ); - - expect(wrapper.find('WithSource')).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/details/index.tsx deleted file mode 100644 index 3e45c892e23ddf..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/details/index.tsx +++ /dev/null @@ -1,429 +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. - */ - -/* eslint-disable react-hooks/rules-of-hooks */ - -import { - EuiButton, - EuiLoadingSpinner, - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiTab, - EuiTabs, - EuiToolTip, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { FC, memo, useCallback, useMemo, useState } from 'react'; -import { Redirect, useParams } from 'react-router-dom'; -import { StickyContainer } from 'react-sticky'; -import { connect, ConnectedProps } from 'react-redux'; - -import { UpdateDateRange } from '../../../../components/charts/common'; -import { FiltersGlobal } from '../../../../components/filters_global'; -import { FormattedDate } from '../../../../components/formatted_date'; -import { - getEditRuleUrl, - getRulesUrl, - DETECTION_ENGINE_PAGE_NAME, -} from '../../../../components/link_to/redirect_to_detection_engine'; -import { SiemSearchBar } from '../../../../components/search_bar'; -import { WrapperPage } from '../../../../components/wrapper_page'; -import { useRule } from '../../../../containers/detection_engine/rules'; - -import { - indicesExistOrDataTemporarilyUnavailable, - WithSource, -} from '../../../../containers/source'; -import { SpyRoute } from '../../../../utils/route/spy_routes'; - -import { StepAboutRuleToggleDetails } from '../components/step_about_rule_details/'; -import { DetectionEngineHeaderPage } from '../../components/detection_engine_header_page'; -import { SignalsHistogramPanel } from '../../components/signals_histogram_panel'; -import { SignalsTable } from '../../components/signals'; -import { useUserInfo } from '../../components/user_info'; -import { DetectionEngineEmptyPage } from '../../detection_engine_empty_page'; -import { useSignalInfo } from '../../components/signals_info'; -import { StepDefineRule } from '../components/step_define_rule'; -import { StepScheduleRule } from '../components/step_schedule_rule'; -import { buildSignalsRuleIdFilter } from '../../components/signals/default_config'; -import { NoWriteSignalsCallOut } from '../../components/no_write_signals_callout'; -import * as detectionI18n from '../../translations'; -import { ReadOnlyCallOut } from '../components/read_only_callout'; -import { RuleSwitch } from '../components/rule_switch'; -import { StepPanel } from '../components/step_panel'; -import { getStepsData, redirectToDetections, userHasNoPermissions } from '../helpers'; -import * as ruleI18n from '../translations'; -import * as i18n from './translations'; -import { GlobalTime } from '../../../../containers/global_time'; -import { signalsHistogramOptions } from '../../components/signals_histogram_panel/config'; -import { inputsSelectors } from '../../../../store/inputs'; -import { State } from '../../../../store'; -import { InputsRange } from '../../../../store/inputs/model'; -import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../../../store/inputs/actions'; -import { RuleActionsOverflow } from '../components/rule_actions_overflow'; -import { RuleStatusFailedCallOut } from './status_failed_callout'; -import { FailureHistory } from './failure_history'; -import { RuleStatus } from '../components/rule_status'; -import { useMlCapabilities } from '../../../../components/ml_popover/hooks/use_ml_capabilities'; -import { hasMlAdminPermissions } from '../../../../components/ml/permissions/has_ml_admin_permissions'; - -enum RuleDetailTabs { - signals = 'signals', - failures = 'failures', -} - -const ruleDetailTabs = [ - { - id: RuleDetailTabs.signals, - name: detectionI18n.SIGNAL, - disabled: false, - }, - { - id: RuleDetailTabs.failures, - name: i18n.FAILURE_HISTORY_TAB, - disabled: false, - }, -]; - -export const RuleDetailsPageComponent: FC = ({ - filters, - query, - setAbsoluteRangeDatePicker, -}) => { - const { - loading, - isSignalIndexExists, - isAuthenticated, - hasEncryptionKey, - canUserCRUD, - hasIndexWrite, - signalIndexName, - } = useUserInfo(); - const { detailName: ruleId } = useParams(); - const [isLoading, rule] = useRule(ruleId); - // This is used to re-trigger api rule status when user de/activate rule - const [ruleEnabled, setRuleEnabled] = useState(null); - const [ruleDetailTab, setRuleDetailTab] = useState(RuleDetailTabs.signals); - const { aboutRuleData, modifiedAboutRuleDetailsData, defineRuleData, scheduleRuleData } = - rule != null - ? getStepsData({ rule, detailsView: true }) - : { - aboutRuleData: null, - modifiedAboutRuleDetailsData: null, - defineRuleData: null, - scheduleRuleData: null, - }; - const [lastSignals] = useSignalInfo({ ruleId }); - const mlCapabilities = useMlCapabilities(); - - // TODO: Refactor license check + hasMlAdminPermissions to common check - const hasMlPermissions = - mlCapabilities.isPlatinumOrTrialLicense && hasMlAdminPermissions(mlCapabilities); - - const title = isLoading === true || rule === null ? : rule.name; - const subTitle = useMemo( - () => - isLoading === true || rule === null ? ( - - ) : ( - [ - - ), - }} - />, - rule?.updated_by != null ? ( - - ), - }} - /> - ) : ( - '' - ), - ] - ), - [isLoading, rule] - ); - - const signalDefaultFilters = useMemo( - () => (ruleId != null ? buildSignalsRuleIdFilter(ruleId) : []), - [ruleId] - ); - - const signalMergedFilters = useMemo(() => [...signalDefaultFilters, ...filters], [ - signalDefaultFilters, - filters, - ]); - - const tabs = useMemo( - () => ( - - {ruleDetailTabs.map(tab => ( - setRuleDetailTab(tab.id)} - isSelected={tab.id === ruleDetailTab} - disabled={tab.disabled} - key={tab.id} - > - {tab.name} - - ))} - - ), - [ruleDetailTabs, ruleDetailTab, setRuleDetailTab] - ); - const ruleError = useMemo( - () => - rule?.status === 'failed' && - ruleDetailTab === RuleDetailTabs.signals && - rule?.last_failure_at != null ? ( - - ) : null, - [rule, ruleDetailTab] - ); - - const indexToAdd = useMemo(() => (signalIndexName == null ? [] : [signalIndexName]), [ - signalIndexName, - ]); - - const updateDateRangeCallback = useCallback( - ({ x }) => { - if (!x) { - return; - } - const [min, max] = x; - setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); - }, - [setAbsoluteRangeDatePicker] - ); - - const handleOnChangeEnabledRule = useCallback( - (enabled: boolean) => { - if (ruleEnabled == null || enabled !== ruleEnabled) { - setRuleEnabled(enabled); - } - }, - [ruleEnabled, setRuleEnabled] - ); - - if (redirectToDetections(isSignalIndexExists, isAuthenticated, hasEncryptionKey)) { - return ; - } - - return ( - <> - {hasIndexWrite != null && !hasIndexWrite && } - {userHasNoPermissions(canUserCRUD) && } - - {({ indicesExist, indexPattern }) => { - return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - {({ to, from, deleteQuery, setQuery }) => ( - - - - - - - - {detectionI18n.LAST_SIGNAL} - {': '} - {lastSignals} - , - ] - : []), - , - ]} - title={title} - > - - - - - - - - - - - - {ruleI18n.EDIT_RULE_SETTINGS} - - - - - - - - - - {ruleError} - - - - - - - - - - - {defineRuleData != null && ( - - )} - - - - - - {scheduleRuleData != null && ( - - )} - - - - - - - {tabs} - - {ruleDetailTab === RuleDetailTabs.signals && ( - <> - - - {ruleId != null && ( - - )} - - )} - {ruleDetailTab === RuleDetailTabs.failures && } - - - )} - - ) : ( - - - - - - ); - }} - - - - - ); -}; - -const makeMapStateToProps = () => { - const getGlobalInputs = inputsSelectors.globalSelector(); - return (state: State) => { - const globalInputs: InputsRange = getGlobalInputs(state); - const { query, filters } = globalInputs; - - return { - query, - filters, - }; - }; -}; - -const mapDispatchToProps = { - setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker, -}; - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const RuleDetailsPage = connector(memo(RuleDetailsPageComponent)); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/edit/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/edit/index.test.tsx deleted file mode 100644 index d22bc12abf9fa1..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/edit/index.test.tsx +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { shallow } from 'enzyme'; - -import { TestProviders } from '../../../../mock'; -import { EditRulePage } from './index'; -import { useUserInfo } from '../../components/user_info'; -import { useParams } from 'react-router-dom'; - -jest.mock('../../components/user_info'); -jest.mock('react-router-dom', () => { - const originalModule = jest.requireActual('react-router-dom'); - - return { - ...originalModule, - useParams: jest.fn(), - }; -}); - -describe('EditRulePage', () => { - it('renders correctly', () => { - (useUserInfo as jest.Mock).mockReturnValue({}); - (useParams as jest.Mock).mockReturnValue({}); - const wrapper = shallow(, { wrappingComponent: TestProviders }); - - expect(wrapper.find('[title="Edit rule settings"]')).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx deleted file mode 100644 index c42e7b902cd5c5..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/edit/index.tsx +++ /dev/null @@ -1,431 +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. - */ - -/* eslint-disable react-hooks/rules-of-hooks */ - -import { - EuiButton, - EuiCallOut, - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiTabbedContent, - EuiTabbedContentTab, -} from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React, { FC, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { Redirect, useParams } from 'react-router-dom'; - -import { useRule, usePersistRule } from '../../../../containers/detection_engine/rules'; -import { WrapperPage } from '../../../../components/wrapper_page'; -import { DETECTION_ENGINE_PAGE_NAME } from '../../../../components/link_to/redirect_to_detection_engine'; -import { displaySuccessToast, useStateToaster } from '../../../../components/toasters'; -import { SpyRoute } from '../../../../utils/route/spy_routes'; -import { useUserInfo } from '../../components/user_info'; -import { DetectionEngineHeaderPage } from '../../components/detection_engine_header_page'; -import { FormHook, FormData } from '../../../../shared_imports'; -import { StepPanel } from '../components/step_panel'; -import { StepAboutRule } from '../components/step_about_rule'; -import { StepDefineRule } from '../components/step_define_rule'; -import { StepScheduleRule } from '../components/step_schedule_rule'; -import { StepRuleActions } from '../components/step_rule_actions'; -import { formatRule } from '../create/helpers'; -import { - getStepsData, - redirectToDetections, - getActionMessageParams, - userHasNoPermissions, -} from '../helpers'; -import * as ruleI18n from '../translations'; -import { - RuleStep, - DefineStepRule, - AboutStepRule, - ScheduleStepRule, - ActionsStepRule, -} from '../types'; -import * as i18n from './translations'; - -interface StepRuleForm { - isValid: boolean; -} -interface AboutStepRuleForm extends StepRuleForm { - data: AboutStepRule | null; -} -interface DefineStepRuleForm extends StepRuleForm { - data: DefineStepRule | null; -} -interface ScheduleStepRuleForm extends StepRuleForm { - data: ScheduleStepRule | null; -} - -interface ActionsStepRuleForm extends StepRuleForm { - data: ActionsStepRule | null; -} - -const EditRulePageComponent: FC = () => { - const [, dispatchToaster] = useStateToaster(); - const { - loading: initLoading, - isSignalIndexExists, - isAuthenticated, - hasEncryptionKey, - canUserCRUD, - } = useUserInfo(); - const { detailName: ruleId } = useParams(); - const [loading, rule] = useRule(ruleId); - - const [initForm, setInitForm] = useState(false); - const [myAboutRuleForm, setMyAboutRuleForm] = useState({ - data: null, - isValid: false, - }); - const [myDefineRuleForm, setMyDefineRuleForm] = useState({ - data: null, - isValid: false, - }); - const [myScheduleRuleForm, setMyScheduleRuleForm] = useState({ - data: null, - isValid: false, - }); - const [myActionsRuleForm, setMyActionsRuleForm] = useState({ - data: null, - isValid: false, - }); - const [selectedTab, setSelectedTab] = useState(); - const stepsForm = useRef | null>>({ - [RuleStep.defineRule]: null, - [RuleStep.aboutRule]: null, - [RuleStep.scheduleRule]: null, - [RuleStep.ruleActions]: null, - }); - const [{ isLoading, isSaved }, setRule] = usePersistRule(); - const [tabHasError, setTabHasError] = useState([]); - const actionMessageParams = useMemo(() => getActionMessageParams(rule?.type), [rule]); - const setStepsForm = useCallback( - (step: RuleStep, form: FormHook) => { - stepsForm.current[step] = form; - if (initForm && step === (selectedTab?.id as RuleStep) && form.isSubmitted === false) { - setInitForm(false); - form.submit(); - } - }, - [initForm, selectedTab] - ); - const tabs = useMemo( - () => [ - { - id: RuleStep.defineRule, - name: ruleI18n.DEFINITION, - disabled: rule?.immutable, - content: ( - <> - - - {myDefineRuleForm.data != null && ( - - )} - - - - ), - }, - { - id: RuleStep.aboutRule, - name: ruleI18n.ABOUT, - disabled: rule?.immutable, - content: ( - <> - - - {myAboutRuleForm.data != null && ( - - )} - - - - ), - }, - { - id: RuleStep.scheduleRule, - name: ruleI18n.SCHEDULE, - disabled: rule?.immutable, - content: ( - <> - - - {myScheduleRuleForm.data != null && ( - - )} - - - - ), - }, - { - id: RuleStep.ruleActions, - name: ruleI18n.ACTIONS, - content: ( - <> - - - {myActionsRuleForm.data != null && ( - - )} - - - - ), - }, - ], - [ - rule, - loading, - initLoading, - isLoading, - myAboutRuleForm, - myDefineRuleForm, - myScheduleRuleForm, - myActionsRuleForm, - setStepsForm, - stepsForm, - actionMessageParams, - ] - ); - - const onSubmit = useCallback(async () => { - const activeFormId = selectedTab?.id as RuleStep; - const activeForm = await stepsForm.current[activeFormId]?.submit(); - - const invalidForms = [ - RuleStep.aboutRule, - RuleStep.defineRule, - RuleStep.scheduleRule, - RuleStep.ruleActions, - ].reduce((acc, step) => { - if ( - (step === activeFormId && activeForm != null && !activeForm?.isValid) || - (step === RuleStep.aboutRule && !myAboutRuleForm.isValid) || - (step === RuleStep.defineRule && !myDefineRuleForm.isValid) || - (step === RuleStep.scheduleRule && !myScheduleRuleForm.isValid) || - (step === RuleStep.ruleActions && !myActionsRuleForm.isValid) - ) { - return [...acc, step]; - } - return acc; - }, []); - - if (invalidForms.length === 0 && activeForm != null) { - setTabHasError([]); - setRule({ - ...formatRule( - (activeFormId === RuleStep.defineRule - ? activeForm.data - : myDefineRuleForm.data) as DefineStepRule, - (activeFormId === RuleStep.aboutRule - ? activeForm.data - : myAboutRuleForm.data) as AboutStepRule, - (activeFormId === RuleStep.scheduleRule - ? activeForm.data - : myScheduleRuleForm.data) as ScheduleStepRule, - (activeFormId === RuleStep.ruleActions - ? activeForm.data - : myActionsRuleForm.data) as ActionsStepRule - ), - ...(ruleId ? { id: ruleId } : {}), - }); - } else { - setTabHasError(invalidForms); - } - }, [ - stepsForm, - myAboutRuleForm, - myDefineRuleForm, - myScheduleRuleForm, - myActionsRuleForm, - selectedTab, - ruleId, - ]); - - useEffect(() => { - if (rule != null) { - const { aboutRuleData, defineRuleData, scheduleRuleData, ruleActionsData } = getStepsData({ - rule, - }); - setMyAboutRuleForm({ data: aboutRuleData, isValid: true }); - setMyDefineRuleForm({ data: defineRuleData, isValid: true }); - setMyScheduleRuleForm({ data: scheduleRuleData, isValid: true }); - setMyActionsRuleForm({ data: ruleActionsData, isValid: true }); - } - }, [rule]); - - const onTabClick = useCallback( - async (tab: EuiTabbedContentTab) => { - if (selectedTab != null) { - const ruleStep = selectedTab.id as RuleStep; - const respForm = await stepsForm.current[ruleStep]?.submit(); - - if (respForm != null) { - if (ruleStep === RuleStep.aboutRule) { - setMyAboutRuleForm({ - data: respForm.data as AboutStepRule, - isValid: respForm.isValid, - }); - } else if (ruleStep === RuleStep.defineRule) { - setMyDefineRuleForm({ - data: respForm.data as DefineStepRule, - isValid: respForm.isValid, - }); - } else if (ruleStep === RuleStep.scheduleRule) { - setMyScheduleRuleForm({ - data: respForm.data as ScheduleStepRule, - isValid: respForm.isValid, - }); - } else if (ruleStep === RuleStep.ruleActions) { - setMyActionsRuleForm({ - data: respForm.data as ActionsStepRule, - isValid: respForm.isValid, - }); - } - } - } - setInitForm(true); - setSelectedTab(tab); - }, - [selectedTab, stepsForm.current] - ); - - useEffect(() => { - if (rule != null) { - const { aboutRuleData, defineRuleData, scheduleRuleData, ruleActionsData } = getStepsData({ - rule, - }); - setMyAboutRuleForm({ data: aboutRuleData, isValid: true }); - setMyDefineRuleForm({ data: defineRuleData, isValid: true }); - setMyScheduleRuleForm({ data: scheduleRuleData, isValid: true }); - setMyActionsRuleForm({ data: ruleActionsData, isValid: true }); - } - }, [rule]); - - useEffect(() => { - const tabIndex = rule?.immutable ? 3 : 0; - setSelectedTab(tabs[tabIndex]); - }, [rule]); - - if (isSaved) { - displaySuccessToast(i18n.SUCCESSFULLY_SAVED_RULE(rule?.name ?? ''), dispatchToaster); - return ; - } - - if (redirectToDetections(isSignalIndexExists, isAuthenticated, hasEncryptionKey)) { - return ; - } else if (userHasNoPermissions(canUserCRUD)) { - return ; - } - - return ( - <> - - - {tabHasError.length > 0 && ( - - { - if (t === RuleStep.aboutRule) { - return ruleI18n.ABOUT; - } else if (t === RuleStep.defineRule) { - return ruleI18n.DEFINITION; - } else if (t === RuleStep.scheduleRule) { - return ruleI18n.SCHEDULE; - } else if (t === RuleStep.ruleActions) { - return ruleI18n.RULE_ACTIONS; - } - return t; - }) - .join(', '), - }} - /> - - )} - - t.id === selectedTab?.id)} - onTabClick={onTabClick} - tabs={tabs} - /> - - - - - - - {i18n.CANCEL} - - - - - - {i18n.SAVE_CHANGES} - - - - - - - - ); -}; - -export const EditRulePage = memo(EditRulePageComponent); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx deleted file mode 100644 index f2a04a87ced278..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/helpers.test.tsx +++ /dev/null @@ -1,378 +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 { - GetStepsData, - getDefineStepsData, - getScheduleStepsData, - getStepsData, - getAboutStepsData, - getActionsStepsData, - getHumanizedDuration, - getModifiedAboutDetailsData, - determineDetailsValue, - userHasNoPermissions, -} from './helpers'; -import { mockRuleWithEverything, mockRule } from './all/__mocks__/mock'; -import { esFilters } from '../../../../../../../src/plugins/data/public'; -import { Rule } from '../../../containers/detection_engine/rules'; -import { - AboutStepRule, - AboutStepRuleDetails, - DefineStepRule, - ScheduleStepRule, - ActionsStepRule, -} from './types'; - -describe('rule helpers', () => { - describe('getStepsData', () => { - test('returns object with about, define, schedule and actions step properties formatted', () => { - const { - defineRuleData, - modifiedAboutRuleDetailsData, - aboutRuleData, - scheduleRuleData, - ruleActionsData, - }: GetStepsData = getStepsData({ - rule: mockRuleWithEverything('test-id'), - }); - const defineRuleStepData = { - isNew: false, - ruleType: 'saved_query', - anomalyThreshold: 50, - index: ['auditbeat-*'], - machineLearningJobId: '', - queryBar: { - query: { - query: 'user.name: root or user.name: admin', - language: 'kuery', - }, - filters: [ - { - $state: { - store: esFilters.FilterStateStore.GLOBAL_STATE, - }, - meta: { - alias: null, - disabled: false, - key: 'event.category', - negate: false, - params: { - query: 'file', - }, - type: 'phrase', - }, - query: { - match_phrase: { - 'event.category': 'file', - }, - }, - }, - ], - saved_id: 'test123', - }, - timeline: { - id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', - title: 'Titled timeline', - }, - }; - const aboutRuleStepData = { - description: '24/7', - falsePositives: ['test'], - isNew: false, - name: 'Query with rule-id', - note: '# this is some markdown documentation', - references: ['www.test.co'], - riskScore: 21, - severity: 'low', - tags: ['tag1', 'tag2'], - threat: [ - { - framework: 'mockFramework', - tactic: { - id: '1234', - name: 'tactic1', - reference: 'reference1', - }, - technique: [ - { - id: '456', - name: 'technique1', - reference: 'technique reference', - }, - ], - }, - ], - }; - const scheduleRuleStepData = { from: '0s', interval: '5m', isNew: false }; - const ruleActionsStepData = { - enabled: true, - throttle: 'no_actions', - isNew: false, - actions: [], - }; - const aboutRuleDataDetailsData = { - note: '# this is some markdown documentation', - description: '24/7', - }; - - expect(defineRuleData).toEqual(defineRuleStepData); - expect(aboutRuleData).toEqual(aboutRuleStepData); - expect(scheduleRuleData).toEqual(scheduleRuleStepData); - expect(ruleActionsData).toEqual(ruleActionsStepData); - expect(modifiedAboutRuleDetailsData).toEqual(aboutRuleDataDetailsData); - }); - }); - - describe('getAboutStepsData', () => { - test('returns name, description, and note as empty string if detailsView is true', () => { - const result: AboutStepRule = getAboutStepsData(mockRuleWithEverything('test-id'), true); - - expect(result.name).toEqual(''); - expect(result.description).toEqual(''); - expect(result.note).toEqual(''); - }); - - test('returns note as empty string if property does not exist on rule', () => { - const mockedRule = mockRuleWithEverything('test-id'); - delete mockedRule.note; - const result: AboutStepRule = getAboutStepsData(mockedRule, false); - - expect(result.note).toEqual(''); - }); - }); - - describe('determineDetailsValue', () => { - test('returns name, description, and note as empty string if detailsView is true', () => { - const result: Pick = determineDetailsValue( - mockRuleWithEverything('test-id'), - true - ); - const expected = { name: '', description: '', note: '' }; - - expect(result).toEqual(expected); - }); - - test('returns name, description, and note values if detailsView is false', () => { - const mockedRule = mockRuleWithEverything('test-id'); - const result: Pick = determineDetailsValue( - mockedRule, - false - ); - const expected = { - name: mockedRule.name, - description: mockedRule.description, - note: mockedRule.note, - }; - - expect(result).toEqual(expected); - }); - - test('returns note as empty string if property does not exist on rule', () => { - const mockedRule = mockRuleWithEverything('test-id'); - delete mockedRule.note; - const result: Pick = determineDetailsValue( - mockedRule, - false - ); - const expected = { name: mockedRule.name, description: mockedRule.description, note: '' }; - - expect(result).toEqual(expected); - }); - }); - - describe('getDefineStepsData', () => { - test('returns with saved_id if value exists on rule', () => { - const result: DefineStepRule = getDefineStepsData(mockRule('test-id')); - const expected = { - isNew: false, - ruleType: 'saved_query', - anomalyThreshold: 50, - machineLearningJobId: '', - index: ['auditbeat-*'], - queryBar: { - query: { - query: '', - language: 'kuery', - }, - filters: [], - saved_id: "Garrett's IP", - }, - timeline: { - id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', - title: 'Untitled timeline', - }, - }; - - expect(result).toEqual(expected); - }); - - test('returns with saved_id of undefined if value does not exist on rule', () => { - const mockedRule = { - ...mockRule('test-id'), - }; - delete mockedRule.saved_id; - const result: DefineStepRule = getDefineStepsData(mockedRule); - const expected = { - isNew: false, - ruleType: 'saved_query', - anomalyThreshold: 50, - machineLearningJobId: '', - index: ['auditbeat-*'], - queryBar: { - query: { - query: '', - language: 'kuery', - }, - filters: [], - saved_id: undefined, - }, - timeline: { - id: '86aa74d0-2136-11ea-9864-ebc8cc1cb8c2', - title: 'Untitled timeline', - }, - }; - - expect(result).toEqual(expected); - }); - - test('returns timeline id and title of null if they do not exist on rule', () => { - const mockedRule = mockRuleWithEverything('test-id'); - delete mockedRule.timeline_id; - delete mockedRule.timeline_title; - const result: DefineStepRule = getDefineStepsData(mockedRule); - - expect(result.timeline.id).toBeNull(); - expect(result.timeline.title).toBeNull(); - }); - }); - - describe('getHumanizedDuration', () => { - test('returns from as seconds if from duration is less than a minute', () => { - const result = getHumanizedDuration('now-62s', '1m'); - - expect(result).toEqual('2s'); - }); - - test('returns from as minutes if from duration is less than an hour', () => { - const result = getHumanizedDuration('now-660s', '5m'); - - expect(result).toEqual('6m'); - }); - - test('returns from as hours if from duration is more than 60 minutes', () => { - const result = getHumanizedDuration('now-7400s', '5m'); - - expect(result).toEqual('1h'); - }); - - test('returns from as if from is not parsable as dateMath', () => { - const result = getHumanizedDuration('randomstring', '5m'); - - expect(result).toEqual('NaNh'); - }); - - test('returns from as 5m if interval is not parsable as dateMath', () => { - const result = getHumanizedDuration('now-300s', 'randomstring'); - - expect(result).toEqual('5m'); - }); - }); - - describe('getScheduleStepsData', () => { - test('returns expected ScheduleStep rule object', () => { - const mockedRule = { - ...mockRule('test-id'), - }; - const result: ScheduleStepRule = getScheduleStepsData(mockedRule); - const expected = { - isNew: false, - interval: mockedRule.interval, - from: '0s', - }; - - expect(result).toEqual(expected); - }); - }); - - describe('getActionsStepsData', () => { - test('returns expected ActionsStepRule rule object', () => { - const mockedRule = { - ...mockRule('test-id'), - actions: [ - { - id: 'id', - group: 'group', - params: {}, - action_type_id: 'action_type_id', - }, - ], - }; - const result: ActionsStepRule = getActionsStepsData(mockedRule); - const expected = { - actions: [ - { - id: 'id', - group: 'group', - params: {}, - actionTypeId: 'action_type_id', - }, - ], - enabled: mockedRule.enabled, - isNew: false, - throttle: 'no_actions', - }; - - expect(result).toEqual(expected); - }); - }); - - describe('getModifiedAboutDetailsData', () => { - test('returns object with "note" and "description" being those of passed in rule', () => { - const result: AboutStepRuleDetails = getModifiedAboutDetailsData( - mockRuleWithEverything('test-id') - ); - const aboutRuleDataDetailsData = { - note: '# this is some markdown documentation', - description: '24/7', - }; - - expect(result).toEqual(aboutRuleDataDetailsData); - }); - - test('returns "note" with empty string if "note" does not exist', () => { - const { note, ...mockRuleWithoutNote } = { ...mockRuleWithEverything('test-id') }; - const result: AboutStepRuleDetails = getModifiedAboutDetailsData(mockRuleWithoutNote); - - const aboutRuleDetailsData = { note: '', description: mockRuleWithoutNote.description }; - - expect(result).toEqual(aboutRuleDetailsData); - }); - }); - - describe('userHasNoPermissions', () => { - test("returns false when user's CRUD operations are null", () => { - const result: boolean = userHasNoPermissions(null); - const userHasNoPermissionsExpectedResult = false; - - expect(result).toEqual(userHasNoPermissionsExpectedResult); - }); - - test('returns true when user cannot CRUD', () => { - const result: boolean = userHasNoPermissions(false); - const userHasNoPermissionsExpectedResult = true; - - expect(result).toEqual(userHasNoPermissionsExpectedResult); - }); - - test('returns false when user can CRUD', () => { - const result: boolean = userHasNoPermissions(true); - const userHasNoPermissionsExpectedResult = false; - - expect(result).toEqual(userHasNoPermissionsExpectedResult); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/helpers.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/helpers.tsx deleted file mode 100644 index 2ccbffd864070e..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/helpers.tsx +++ /dev/null @@ -1,273 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import dateMath from '@elastic/datemath'; -import { get } from 'lodash/fp'; -import moment from 'moment'; -import memoizeOne from 'memoize-one'; -import { useLocation } from 'react-router-dom'; - -import { RuleAlertAction, RuleType } from '../../../../common/detection_engine/types'; -import { isMlRule } from '../../../../common/detection_engine/ml_helpers'; -import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; -import { Filter } from '../../../../../../../src/plugins/data/public'; -import { Rule } from '../../../containers/detection_engine/rules'; -import { FormData, FormHook, FormSchema } from '../../../shared_imports'; -import { - AboutStepRule, - AboutStepRuleDetails, - DefineStepRule, - IMitreEnterpriseAttack, - ScheduleStepRule, - ActionsStepRule, -} from './types'; - -export interface GetStepsData { - aboutRuleData: AboutStepRule; - modifiedAboutRuleDetailsData: AboutStepRuleDetails; - defineRuleData: DefineStepRule; - scheduleRuleData: ScheduleStepRule; - ruleActionsData: ActionsStepRule; -} - -export const getStepsData = ({ - rule, - detailsView = false, -}: { - rule: Rule; - detailsView?: boolean; -}): GetStepsData => { - const defineRuleData: DefineStepRule = getDefineStepsData(rule); - const aboutRuleData: AboutStepRule = getAboutStepsData(rule, detailsView); - const modifiedAboutRuleDetailsData: AboutStepRuleDetails = getModifiedAboutDetailsData(rule); - const scheduleRuleData: ScheduleStepRule = getScheduleStepsData(rule); - const ruleActionsData: ActionsStepRule = getActionsStepsData(rule); - - return { - aboutRuleData, - modifiedAboutRuleDetailsData, - defineRuleData, - scheduleRuleData, - ruleActionsData, - }; -}; - -export const getActionsStepsData = ( - rule: Omit & { actions: RuleAlertAction[] } -): ActionsStepRule => { - const { enabled, throttle, meta, actions = [] } = rule; - - return { - actions: actions?.map(transformRuleToAlertAction), - isNew: false, - throttle, - kibanaSiemAppUrl: meta?.kibana_siem_app_url, - enabled, - }; -}; - -export const getDefineStepsData = (rule: Rule): DefineStepRule => ({ - isNew: false, - ruleType: rule.type, - anomalyThreshold: rule.anomaly_threshold ?? 50, - machineLearningJobId: rule.machine_learning_job_id ?? '', - index: rule.index ?? [], - queryBar: { - query: { query: rule.query ?? '', language: rule.language ?? '' }, - filters: (rule.filters ?? []) as Filter[], - saved_id: rule.saved_id, - }, - timeline: { - id: rule.timeline_id ?? null, - title: rule.timeline_title ?? null, - }, -}); - -export const getScheduleStepsData = (rule: Rule): ScheduleStepRule => { - const { interval, from } = rule; - const fromHumanizedValue = getHumanizedDuration(from, interval); - - return { - isNew: false, - interval, - from: fromHumanizedValue, - }; -}; - -export const getHumanizedDuration = (from: string, interval: string): string => { - const fromValue = dateMath.parse(from) ?? moment(); - const intervalValue = dateMath.parse(`now-${interval}`) ?? moment(); - - const fromDuration = moment.duration(intervalValue.diff(fromValue)); - const fromHumanize = `${Math.floor(fromDuration.asHours())}h`; - - if (fromDuration.asSeconds() < 60) { - return `${Math.floor(fromDuration.asSeconds())}s`; - } else if (fromDuration.asMinutes() < 60) { - return `${Math.floor(fromDuration.asMinutes())}m`; - } - - return fromHumanize; -}; - -export const getAboutStepsData = (rule: Rule, detailsView: boolean): AboutStepRule => { - const { name, description, note } = determineDetailsValue(rule, detailsView); - const { - references, - severity, - false_positives: falsePositives, - risk_score: riskScore, - tags, - threat, - } = rule; - - return { - isNew: false, - name, - description, - note: note!, - references, - severity, - tags, - riskScore, - falsePositives, - threat: threat as IMitreEnterpriseAttack[], - }; -}; - -export const determineDetailsValue = ( - rule: Rule, - detailsView: boolean -): Pick => { - const { name, description, note } = rule; - if (detailsView) { - return { name: '', description: '', note: '' }; - } - - return { name, description, note: note ?? '' }; -}; - -export const getModifiedAboutDetailsData = (rule: Rule): AboutStepRuleDetails => ({ - note: rule.note ?? '', - description: rule.description, -}); - -export const useQuery = () => new URLSearchParams(useLocation().search); - -export type PrePackagedRuleStatus = - | 'ruleInstalled' - | 'ruleNotInstalled' - | 'ruleNeedUpdate' - | 'someRuleUninstall' - | 'unknown'; - -export const getPrePackagedRuleStatus = ( - rulesInstalled: number | null, - rulesNotInstalled: number | null, - rulesNotUpdated: number | null -): PrePackagedRuleStatus => { - if ( - rulesNotInstalled != null && - rulesInstalled === 0 && - rulesNotInstalled > 0 && - rulesNotUpdated === 0 - ) { - return 'ruleNotInstalled'; - } else if ( - rulesInstalled != null && - rulesInstalled > 0 && - rulesNotInstalled === 0 && - rulesNotUpdated === 0 - ) { - return 'ruleInstalled'; - } else if ( - rulesInstalled != null && - rulesNotInstalled != null && - rulesInstalled > 0 && - rulesNotInstalled > 0 && - rulesNotUpdated === 0 - ) { - return 'someRuleUninstall'; - } else if ( - rulesInstalled != null && - rulesNotInstalled != null && - rulesNotUpdated != null && - rulesInstalled > 0 && - rulesNotInstalled >= 0 && - rulesNotUpdated > 0 - ) { - return 'ruleNeedUpdate'; - } - return 'unknown'; -}; -export const setFieldValue = ( - form: FormHook, - schema: FormSchema, - defaultValues: unknown -) => - Object.keys(schema).forEach(key => { - const val = get(key, defaultValues); - if (val != null) { - form.setFieldValue(key, val); - } - }); - -export const redirectToDetections = ( - isSignalIndexExists: boolean | null, - isAuthenticated: boolean | null, - hasEncryptionKey: boolean | null -) => - isSignalIndexExists != null && - isAuthenticated != null && - hasEncryptionKey != null && - (!isSignalIndexExists || !isAuthenticated || !hasEncryptionKey); - -export const getActionMessageRuleParams = (ruleType: RuleType): string[] => { - const commonRuleParamsKeys = [ - 'id', - 'name', - 'description', - 'false_positives', - 'rule_id', - 'max_signals', - 'risk_score', - 'output_index', - 'references', - 'severity', - 'timeline_id', - 'timeline_title', - 'threat', - 'type', - 'version', - // 'lists', - ]; - - const ruleParamsKeys = [ - ...commonRuleParamsKeys, - ...(isMlRule(ruleType) - ? ['anomaly_threshold', 'machine_learning_job_id'] - : ['index', 'filters', 'language', 'query', 'saved_id']), - ].sort(); - - return ruleParamsKeys; -}; - -export const getActionMessageParams = memoizeOne((ruleType: RuleType | undefined): string[] => { - if (!ruleType) { - return []; - } - const actionMessageRuleParams = getActionMessageRuleParams(ruleType); - - return [ - 'state.signals_count', - '{context.results_link}', - ...actionMessageRuleParams.map(param => `context.rule.${param}`), - ]; -}); - -// typed as null not undefined as the initial state for this value is null. -export const userHasNoPermissions = (canUserCRUD: boolean | null): boolean => - canUserCRUD != null ? !canUserCRUD : false; diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/index.test.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/index.test.tsx deleted file mode 100644 index 3fa81ca3ced086..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/index.test.tsx +++ /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 React from 'react'; -import { shallow } from 'enzyme'; - -import { RulesPage } from './index'; -import { useUserInfo } from '../components/user_info'; -import { usePrePackagedRules } from '../../../containers/detection_engine/rules'; - -jest.mock('../components/user_info'); -jest.mock('../../../containers/detection_engine/rules'); - -describe('RulesPage', () => { - beforeAll(() => { - (useUserInfo as jest.Mock).mockReturnValue({}); - (usePrePackagedRules as jest.Mock).mockReturnValue({}); - }); - it('renders correctly', () => { - const wrapper = shallow(); - - expect(wrapper.find('AllRules')).toHaveLength(1); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/index.tsx b/x-pack/plugins/siem/public/pages/detection_engine/rules/index.tsx deleted file mode 100644 index 8831bc77691fa7..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/index.tsx +++ /dev/null @@ -1,193 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React, { useCallback, useRef, useState } from 'react'; -import { Redirect } from 'react-router-dom'; - -import { usePrePackagedRules, importRules } from '../../../containers/detection_engine/rules'; -import { - DETECTION_ENGINE_PAGE_NAME, - getDetectionEngineUrl, - getCreateRuleUrl, -} from '../../../components/link_to/redirect_to_detection_engine'; -import { DetectionEngineHeaderPage } from '../components/detection_engine_header_page'; -import { WrapperPage } from '../../../components/wrapper_page'; -import { SpyRoute } from '../../../utils/route/spy_routes'; - -import { useUserInfo } from '../components/user_info'; -import { AllRules } from './all'; -import { ImportDataModal } from '../../../components/import_data_modal'; -import { ReadOnlyCallOut } from './components/read_only_callout'; -import { UpdatePrePackagedRulesCallOut } from './components/pre_packaged_rules/update_callout'; -import { getPrePackagedRuleStatus, redirectToDetections, userHasNoPermissions } from './helpers'; -import * as i18n from './translations'; - -type Func = (refreshPrePackagedRule?: boolean) => void; - -const RulesPageComponent: React.FC = () => { - const [showImportModal, setShowImportModal] = useState(false); - const refreshRulesData = useRef(null); - const { - loading, - isSignalIndexExists, - isAuthenticated, - hasEncryptionKey, - canUserCRUD, - hasIndexWrite, - } = useUserInfo(); - const { - createPrePackagedRules, - loading: prePackagedRuleLoading, - loadingCreatePrePackagedRules, - refetchPrePackagedRulesStatus, - rulesCustomInstalled, - rulesInstalled, - rulesNotInstalled, - rulesNotUpdated, - } = usePrePackagedRules({ - canUserCRUD, - hasIndexWrite, - isSignalIndexExists, - isAuthenticated, - hasEncryptionKey, - }); - const prePackagedRuleStatus = getPrePackagedRuleStatus( - rulesInstalled, - rulesNotInstalled, - rulesNotUpdated - ); - - const handleRefreshRules = useCallback(async () => { - if (refreshRulesData.current != null) { - refreshRulesData.current(true); - } - }, [refreshRulesData]); - - const handleCreatePrePackagedRules = useCallback(async () => { - if (createPrePackagedRules != null) { - await createPrePackagedRules(); - handleRefreshRules(); - } - }, [createPrePackagedRules, handleRefreshRules]); - - const handleRefetchPrePackagedRulesStatus = useCallback(() => { - if (refetchPrePackagedRulesStatus != null) { - refetchPrePackagedRulesStatus(); - } - }, [refetchPrePackagedRulesStatus]); - - const handleSetRefreshRulesData = useCallback((refreshRule: Func) => { - refreshRulesData.current = refreshRule; - }, []); - - if (redirectToDetections(isSignalIndexExists, isAuthenticated, hasEncryptionKey)) { - return ; - } - - return ( - <> - {userHasNoPermissions(canUserCRUD) && } - setShowImportModal(false)} - description={i18n.SELECT_RULE} - errorMessage={i18n.IMPORT_FAILED} - failedDetailed={i18n.IMPORT_FAILED_DETAILED} - importComplete={handleRefreshRules} - importData={importRules} - successMessage={i18n.SUCCESSFULLY_IMPORTED_RULES} - showCheckBox={true} - showModal={showImportModal} - submitBtnText={i18n.IMPORT_RULE_BTN_TITLE} - subtitle={i18n.INITIAL_PROMPT_TEXT} - title={i18n.IMPORT_RULE} - /> - - - - {prePackagedRuleStatus === 'ruleNotInstalled' && ( - - - {i18n.LOAD_PREPACKAGED_RULES} - - - )} - {prePackagedRuleStatus === 'someRuleUninstall' && ( - - - {i18n.RELOAD_MISSING_PREPACKAGED_RULES(rulesNotInstalled ?? 0)} - - - )} - - { - setShowImportModal(true); - }} - > - {i18n.IMPORT_RULE} - - - - - {i18n.ADD_NEW_RULE} - - - - - {prePackagedRuleStatus === 'ruleNeedUpdate' && ( - - )} - - - - - - ); -}; - -export const RulesPage = React.memo(RulesPageComponent); diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/types.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/types.ts deleted file mode 100644 index dcb5397d28f7ce..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/types.ts +++ /dev/null @@ -1,141 +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 { RuleAlertAction, RuleType } from '../../../../common/detection_engine/types'; -import { AlertAction } from '../../../../../alerting/common'; -import { Filter } from '../../../../../../../src/plugins/data/common'; -import { FieldValueQueryBar } from './components/query_bar'; -import { FormData, FormHook } from '../../../shared_imports'; -import { FieldValueTimeline } from './components/pick_timeline'; - -export interface EuiBasicTableSortTypes { - field: string; - direction: 'asc' | 'desc'; -} - -export interface EuiBasicTableOnChange { - page: { - index: number; - size: number; - }; - sort?: EuiBasicTableSortTypes; -} - -export enum RuleStep { - defineRule = 'define-rule', - aboutRule = 'about-rule', - scheduleRule = 'schedule-rule', - ruleActions = 'rule-actions', -} -export type RuleStatusType = 'passive' | 'active' | 'valid'; - -export interface RuleStepData { - data: unknown; - isValid: boolean; -} - -export interface RuleStepProps { - addPadding?: boolean; - descriptionColumns?: 'multi' | 'single' | 'singleSplit'; - setStepData?: (step: RuleStep, data: unknown, isValid: boolean) => void; - isReadOnlyView: boolean; - isUpdateView?: boolean; - isLoading: boolean; - resizeParentContainer?: (height: number) => void; - setForm?: (step: RuleStep, form: FormHook) => void; -} - -interface StepRuleData { - isNew: boolean; -} -export interface AboutStepRule extends StepRuleData { - name: string; - description: string; - severity: string; - riskScore: number; - references: string[]; - falsePositives: string[]; - tags: string[]; - threat: IMitreEnterpriseAttack[]; - note: string; -} - -export interface AboutStepRuleDetails { - note: string; - description: string; -} - -export interface DefineStepRule extends StepRuleData { - anomalyThreshold: number; - index: string[]; - machineLearningJobId: string; - queryBar: FieldValueQueryBar; - ruleType: RuleType; - timeline: FieldValueTimeline; -} - -export interface ScheduleStepRule extends StepRuleData { - interval: string; - from: string; - to?: string; -} - -export interface ActionsStepRule extends StepRuleData { - actions: AlertAction[]; - enabled: boolean; - kibanaSiemAppUrl?: string; - throttle?: string | null; -} - -export interface DefineStepRuleJson { - anomaly_threshold?: number; - index?: string[]; - filters?: Filter[]; - machine_learning_job_id?: string; - saved_id?: string; - query?: string; - language?: string; - timeline_id?: string; - timeline_title?: string; - type: RuleType; -} - -export interface AboutStepRuleJson { - name: string; - description: string; - severity: string; - risk_score: number; - references: string[]; - false_positives: string[]; - tags: string[]; - threat: IMitreEnterpriseAttack[]; - note?: string; -} - -export interface ScheduleStepRuleJson { - interval: string; - from: string; - to?: string; - meta?: unknown; -} - -export interface ActionsStepRuleJson { - actions: RuleAlertAction[]; - enabled: boolean; - throttle?: string | null; - meta?: unknown; -} - -export interface IMitreAttack { - id: string; - name: string; - reference: string; -} -export interface IMitreEnterpriseAttack { - framework: string; - tactic: IMitreAttack; - technique: IMitreAttack[]; -} diff --git a/x-pack/plugins/siem/public/pages/detection_engine/rules/utils.ts b/x-pack/plugins/siem/public/pages/detection_engine/rules/utils.ts deleted file mode 100644 index f93ad94dd462b0..00000000000000 --- a/x-pack/plugins/siem/public/pages/detection_engine/rules/utils.ts +++ /dev/null @@ -1,98 +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 { isEmpty } from 'lodash/fp'; - -import { ChromeBreadcrumb } from '../../../../../../../src/core/public'; -import { - getDetectionEngineUrl, - getDetectionEngineTabUrl, - getRulesUrl, - getRuleDetailsUrl, - getCreateRuleUrl, - getEditRuleUrl, -} from '../../../components/link_to/redirect_to_detection_engine'; -import * as i18nDetections from '../translations'; -import * as i18nRules from './translations'; -import { RouteSpyState } from '../../../utils/route/types'; - -const getTabBreadcrumb = (pathname: string, search: string[]) => { - const tabPath = pathname.split('/')[2]; - - if (tabPath === 'alerts') { - return { - text: i18nDetections.ALERT, - href: `${getDetectionEngineTabUrl(tabPath)}${!isEmpty(search[0]) ? search[0] : ''}`, - }; - } - - if (tabPath === 'signals') { - return { - text: i18nDetections.SIGNAL, - href: `${getDetectionEngineTabUrl(tabPath)}${!isEmpty(search[0]) ? search[0] : ''}`, - }; - } - - if (tabPath === 'rules') { - return { - text: i18nRules.PAGE_TITLE, - href: `${getRulesUrl()}${!isEmpty(search[0]) ? search[0] : ''}`, - }; - } -}; - -const isRuleCreatePage = (pathname: string) => - pathname.includes('/rules') && pathname.includes('/create'); - -const isRuleEditPage = (pathname: string) => - pathname.includes('/rules') && pathname.includes('/edit'); - -export const getBreadcrumbs = (params: RouteSpyState, search: string[]): ChromeBreadcrumb[] => { - let breadcrumb = [ - { - text: i18nDetections.PAGE_TITLE, - href: `${getDetectionEngineUrl()}${!isEmpty(search[0]) ? search[0] : ''}`, - }, - ]; - - const tabBreadcrumb = getTabBreadcrumb(params.pathName, search); - - if (tabBreadcrumb) { - breadcrumb = [...breadcrumb, tabBreadcrumb]; - } - - if (params.detailName && params.state?.ruleName) { - breadcrumb = [ - ...breadcrumb, - { - text: params.state.ruleName, - href: `${getRuleDetailsUrl(params.detailName)}${!isEmpty(search[1]) ? search[1] : ''}`, - }, - ]; - } - - if (isRuleCreatePage(params.pathName)) { - breadcrumb = [ - ...breadcrumb, - { - text: i18nRules.ADD_PAGE_TITLE, - href: `${getCreateRuleUrl()}${!isEmpty(search[1]) ? search[1] : ''}`, - }, - ]; - } - - if (isRuleEditPage(params.pathName) && params.detailName && params.state?.ruleName) { - breadcrumb = [ - ...breadcrumb, - { - text: i18nRules.EDIT_PAGE_TITLE, - href: `${getEditRuleUrl(params.detailName)}${!isEmpty(search[1]) ? search[1] : ''}`, - }, - ]; - } - - return breadcrumb; -}; diff --git a/x-pack/plugins/siem/public/pages/home/index.tsx b/x-pack/plugins/siem/public/pages/home/index.tsx deleted file mode 100644 index a9e0962f16e6ed..00000000000000 --- a/x-pack/plugins/siem/public/pages/home/index.tsx +++ /dev/null @@ -1,143 +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, { useMemo } from 'react'; -import { Redirect, Route, Switch } from 'react-router-dom'; -import styled from 'styled-components'; - -import { useThrottledResizeObserver } from '../../components/utils'; -import { DragDropContextWrapper } from '../../components/drag_and_drop/drag_drop_context_wrapper'; -import { Flyout } from '../../components/flyout'; -import { HeaderGlobal } from '../../components/header_global'; -import { HelpMenu } from '../../components/help_menu'; -import { LinkToPage } from '../../components/link_to'; -import { MlHostConditionalContainer } from '../../components/ml/conditional_links/ml_host_conditional_container'; -import { MlNetworkConditionalContainer } from '../../components/ml/conditional_links/ml_network_conditional_container'; -import { AutoSaveWarningMsg } from '../../components/timeline/auto_save_warning'; -import { UseUrlState } from '../../components/url_state'; -import { WithSource, indicesExistOrDataTemporarilyUnavailable } from '../../containers/source'; -import { SpyRoute } from '../../utils/route/spy_routes'; -import { useShowTimeline } from '../../utils/timeline/use_show_timeline'; -import { NotFoundPage } from '../404'; -import { DetectionEngineContainer } from '../detection_engine'; -import { HostsContainer } from '../hosts'; -import { NetworkContainer } from '../network'; -import { Overview } from '../overview'; -import { Case } from '../case'; -import { Timelines } from '../timelines'; -import { navTabs } from './home_navigations'; -import { SiemPageName } from './types'; - -const WrappedByAutoSizer = styled.div` - height: 100%; -`; -WrappedByAutoSizer.displayName = 'WrappedByAutoSizer'; - -const Main = styled.main` - height: 100%; -`; -Main.displayName = 'Main'; - -const usersViewing = ['elastic']; // TODO: get the users viewing this timeline from Elasticsearch (persistance) - -/** the global Kibana navigation at the top of every page */ -const globalHeaderHeightPx = 48; - -const calculateFlyoutHeight = ({ - globalHeaderSize, - windowHeight, -}: { - globalHeaderSize: number; - windowHeight: number; -}): number => Math.max(0, windowHeight - globalHeaderSize); - -export const HomePage: React.FC = () => { - const { ref: measureRef, height: windowHeight = 0 } = useThrottledResizeObserver(); - const flyoutHeight = useMemo( - () => - calculateFlyoutHeight({ - globalHeaderSize: globalHeaderHeightPx, - windowHeight, - }), - [windowHeight] - ); - - const [showTimeline] = useShowTimeline(); - - return ( - - - -
- - {({ browserFields, indexPattern, indicesExist }) => ( - - - {indicesExistOrDataTemporarilyUnavailable(indicesExist) && showTimeline && ( - <> - - - - )} - - - - } /> - } - /> - ( - - )} - /> - ( - - )} - /> - } - /> - } /> - ( - - )} - /> - ( - - )} - /> - - - - } /> - - - )} - -
- - - - -
- ); -}; - -HomePage.displayName = 'HomePage'; diff --git a/x-pack/plugins/siem/public/pages/home/types.ts b/x-pack/plugins/siem/public/pages/home/types.ts deleted file mode 100644 index 6445ac91d9e131..00000000000000 --- a/x-pack/plugins/siem/public/pages/home/types.ts +++ /dev/null @@ -1,26 +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 { NavTab } from '../../components/navigation/types'; - -export enum SiemPageName { - overview = 'overview', - hosts = 'hosts', - network = 'network', - detections = 'detections', - timelines = 'timelines', - case = 'case', -} - -export type SiemNavTabKey = - | SiemPageName.overview - | SiemPageName.hosts - | SiemPageName.network - | SiemPageName.detections - | SiemPageName.timelines - | SiemPageName.case; - -export type SiemNavTab = Record; diff --git a/x-pack/plugins/siem/public/pages/hosts/details/helpers.ts b/x-pack/plugins/siem/public/pages/hosts/details/helpers.ts deleted file mode 100644 index 6da76f2fb5cac9..00000000000000 --- a/x-pack/plugins/siem/public/pages/hosts/details/helpers.ts +++ /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 { escapeQueryValue } from '../../../lib/keury'; -import { Filter } from '../../../../../../../src/plugins/data/public'; - -/** Returns the kqlQueryExpression for the `Events` widget on the `Host Details` page */ -export const getHostDetailsEventsKqlQueryExpression = ({ - filterQueryExpression, - hostName, -}: { - filterQueryExpression: string; - hostName: string; -}): string => { - if (filterQueryExpression.length) { - return `${filterQueryExpression}${ - hostName.length ? ` and host.name: ${escapeQueryValue(hostName)}` : '' - }`; - } else { - return hostName.length ? `host.name: ${escapeQueryValue(hostName)}` : ''; - } -}; - -export const getHostDetailsPageFilters = (hostName: string): Filter[] => [ - { - meta: { - alias: null, - negate: false, - disabled: false, - type: 'phrase', - key: 'host.name', - value: hostName, - params: { - query: hostName, - }, - }, - query: { - match: { - 'host.name': { - query: hostName, - type: 'phrase', - }, - }, - }, - }, -]; diff --git a/x-pack/plugins/siem/public/pages/hosts/details/index.tsx b/x-pack/plugins/siem/public/pages/hosts/details/index.tsx deleted file mode 100644 index 730c93b43709c2..00000000000000 --- a/x-pack/plugins/siem/public/pages/hosts/details/index.tsx +++ /dev/null @@ -1,234 +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 { EuiHorizontalRule, EuiSpacer } from '@elastic/eui'; -import React, { useEffect, useCallback, useMemo } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import { StickyContainer } from 'react-sticky'; - -import { UpdateDateRange } from '../../../components/charts/common'; -import { FiltersGlobal } from '../../../components/filters_global'; -import { HeaderPage } from '../../../components/header_page'; -import { LastEventTime } from '../../../components/last_event_time'; -import { AnomalyTableProvider } from '../../../components/ml/anomaly/anomaly_table_provider'; -import { hostToCriteria } from '../../../components/ml/criteria/host_to_criteria'; -import { hasMlUserPermissions } from '../../../components/ml/permissions/has_ml_user_permissions'; -import { useMlCapabilities } from '../../../components/ml_popover/hooks/use_ml_capabilities'; -import { scoreIntervalToDateTime } from '../../../components/ml/score/score_interval_to_datetime'; -import { SiemNavigation } from '../../../components/navigation'; -import { KpiHostsComponent } from '../../../components/page/hosts'; -import { HostOverview } from '../../../components/page/hosts/host_overview'; -import { manageQuery } from '../../../components/page/manage_query'; -import { SiemSearchBar } from '../../../components/search_bar'; -import { WrapperPage } from '../../../components/wrapper_page'; -import { HostOverviewByNameQuery } from '../../../containers/hosts/overview'; -import { KpiHostDetailsQuery } from '../../../containers/kpi_host_details'; -import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../../containers/source'; -import { LastEventIndexKey } from '../../../graphql/types'; -import { useKibana } from '../../../lib/kibana'; -import { convertToBuildEsQuery } from '../../../lib/keury'; -import { inputsSelectors, State } from '../../../store'; -import { setHostDetailsTablesActivePageToZero as dispatchHostDetailsTablesActivePageToZero } from '../../../store/hosts/actions'; -import { setAbsoluteRangeDatePicker as dispatchAbsoluteRangeDatePicker } from '../../../store/inputs/actions'; -import { SpyRoute } from '../../../utils/route/spy_routes'; -import { esQuery, Filter } from '../../../../../../../src/plugins/data/public'; - -import { HostsEmptyPage } from '../hosts_empty_page'; -import { HostDetailsTabs } from './details_tabs'; -import { navTabsHostDetails } from './nav_tabs'; -import { HostDetailsProps } from './types'; -import { type } from './utils'; -import { getHostDetailsPageFilters } from './helpers'; - -const HostOverviewManage = manageQuery(HostOverview); -const KpiHostDetailsManage = manageQuery(KpiHostsComponent); - -const HostDetailsComponent = React.memo( - ({ - filters, - from, - isInitializing, - query, - setAbsoluteRangeDatePicker, - setHostDetailsTablesActivePageToZero, - setQuery, - to, - detailName, - deleteQuery, - hostDetailsPagePath, - }) => { - useEffect(() => { - setHostDetailsTablesActivePageToZero(); - }, [setHostDetailsTablesActivePageToZero, detailName]); - const capabilities = useMlCapabilities(); - const kibana = useKibana(); - const hostDetailsPageFilters: Filter[] = useMemo(() => getHostDetailsPageFilters(detailName), [ - detailName, - ]); - const getFilters = () => [...hostDetailsPageFilters, ...filters]; - const narrowDateRange = useCallback( - ({ x }) => { - if (!x) { - return; - } - const [min, max] = x; - setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); - }, - [setAbsoluteRangeDatePicker] - ); - - return ( - <> - - {({ indicesExist, indexPattern }) => { - const filterQuery = convertToBuildEsQuery({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - indexPattern, - queries: [query], - filters: getFilters(), - }); - return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - - - - - - - } - title={detailName} - /> - - - {({ hostOverview, loading, id, inspect, refetch }) => ( - - {({ isLoadingAnomaliesData, anomaliesData }) => ( - { - const fromTo = scoreIntervalToDateTime(score, interval); - setAbsoluteRangeDatePicker({ - id: 'global', - from: fromTo.from, - to: fromTo.to, - }); - }} - /> - )} - - )} - - - - - - {({ kpiHostDetails, id, inspect, loading, refetch }) => ( - - )} - - - - - - - - - - - - ) : ( - - - - - - ); - }} - - - - - ); - } -); -HostDetailsComponent.displayName = 'HostDetailsComponent'; - -export const makeMapStateToProps = () => { - const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); - const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); - return (state: State) => ({ - query: getGlobalQuerySelector(state), - filters: getGlobalFiltersQuerySelector(state), - }); -}; - -const mapDispatchToProps = { - setAbsoluteRangeDatePicker: dispatchAbsoluteRangeDatePicker, - setHostDetailsTablesActivePageToZero: dispatchHostDetailsTablesActivePageToZero, -}; - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const HostDetails = connector(HostDetailsComponent); diff --git a/x-pack/plugins/siem/public/pages/hosts/details/nav_tabs.test.tsx b/x-pack/plugins/siem/public/pages/hosts/details/nav_tabs.test.tsx deleted file mode 100644 index 6710edb7b20fae..00000000000000 --- a/x-pack/plugins/siem/public/pages/hosts/details/nav_tabs.test.tsx +++ /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 { HostsTableType } from '../../../store/hosts/model'; -import { navTabsHostDetails } from './nav_tabs'; - -describe('navTabsHostDetails', () => { - const mockHostName = 'mockHostName'; - test('it should skip anomalies tab if without mlUserPermission', () => { - const tabs = navTabsHostDetails(mockHostName, false); - expect(tabs).toHaveProperty(HostsTableType.authentications); - expect(tabs).toHaveProperty(HostsTableType.uncommonProcesses); - expect(tabs).not.toHaveProperty(HostsTableType.anomalies); - expect(tabs).toHaveProperty(HostsTableType.events); - }); - - test('it should display anomalies tab if with mlUserPermission', () => { - const tabs = navTabsHostDetails(mockHostName, true); - expect(tabs).toHaveProperty(HostsTableType.authentications); - expect(tabs).toHaveProperty(HostsTableType.uncommonProcesses); - expect(tabs).toHaveProperty(HostsTableType.anomalies); - expect(tabs).toHaveProperty(HostsTableType.events); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/hosts/details/nav_tabs.tsx b/x-pack/plugins/siem/public/pages/hosts/details/nav_tabs.tsx deleted file mode 100644 index f828dc250f0d30..00000000000000 --- a/x-pack/plugins/siem/public/pages/hosts/details/nav_tabs.tsx +++ /dev/null @@ -1,65 +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 { omit } from 'lodash/fp'; -import * as i18n from './../translations'; -import { HostDetailsNavTab } from './types'; -import { HostsTableType } from '../../../store/hosts/model'; -import { SiemPageName } from '../../home/types'; - -const getTabsOnHostDetailsUrl = (hostName: string, tabName: HostsTableType) => - `#/${SiemPageName.hosts}/${hostName}/${tabName}`; - -export const navTabsHostDetails = ( - hostName: string, - hasMlUserPermissions: boolean -): HostDetailsNavTab => { - const hostDetailsNavTabs = { - [HostsTableType.authentications]: { - id: HostsTableType.authentications, - name: i18n.NAVIGATION_AUTHENTICATIONS_TITLE, - href: getTabsOnHostDetailsUrl(hostName, HostsTableType.authentications), - disabled: false, - urlKey: 'host', - isDetailPage: true, - }, - [HostsTableType.uncommonProcesses]: { - id: HostsTableType.uncommonProcesses, - name: i18n.NAVIGATION_UNCOMMON_PROCESSES_TITLE, - href: getTabsOnHostDetailsUrl(hostName, HostsTableType.uncommonProcesses), - disabled: false, - urlKey: 'host', - isDetailPage: true, - }, - [HostsTableType.anomalies]: { - id: HostsTableType.anomalies, - name: i18n.NAVIGATION_ANOMALIES_TITLE, - href: getTabsOnHostDetailsUrl(hostName, HostsTableType.anomalies), - disabled: false, - urlKey: 'host', - isDetailPage: true, - }, - [HostsTableType.events]: { - id: HostsTableType.events, - name: i18n.NAVIGATION_EVENTS_TITLE, - href: getTabsOnHostDetailsUrl(hostName, HostsTableType.events), - disabled: false, - urlKey: 'host', - isDetailPage: true, - }, - [HostsTableType.alerts]: { - id: HostsTableType.alerts, - name: i18n.NAVIGATION_ALERTS_TITLE, - href: getTabsOnHostDetailsUrl(hostName, HostsTableType.alerts), - disabled: false, - urlKey: 'host', - }, - }; - - return hasMlUserPermissions - ? hostDetailsNavTabs - : omit(HostsTableType.anomalies, hostDetailsNavTabs); -}; diff --git a/x-pack/plugins/siem/public/pages/hosts/details/types.ts b/x-pack/plugins/siem/public/pages/hosts/details/types.ts deleted file mode 100644 index 03c8646bae1478..00000000000000 --- a/x-pack/plugins/siem/public/pages/hosts/details/types.ts +++ /dev/null @@ -1,71 +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 { ActionCreator } from 'typescript-fsa'; -import { Query, IIndexPattern, Filter } from 'src/plugins/data/public'; -import { InputsModelId } from '../../../store/inputs/constants'; -import { HostComponentProps } from '../../../components/link_to/redirect_to_hosts'; -import { HostsTableType } from '../../../store/hosts/model'; -import { HostsQueryProps } from '../types'; -import { NavTab } from '../../../components/navigation/types'; -import { KeyHostsNavTabWithoutMlPermission } from '../navigation/types'; -import { hostsModel } from '../../../store'; - -interface HostDetailsComponentReduxProps { - query: Query; - filters: Filter[]; -} - -interface HostBodyComponentDispatchProps { - setAbsoluteRangeDatePicker: ActionCreator<{ - id: InputsModelId; - from: number; - to: number; - }>; - detailName: string; - hostDetailsPagePath: string; -} - -interface HostDetailsComponentDispatchProps extends HostBodyComponentDispatchProps { - setHostDetailsTablesActivePageToZero: ActionCreator; -} - -export interface HostDetailsProps extends HostsQueryProps { - detailName: string; - hostDetailsPagePath: string; -} - -export type HostDetailsComponentProps = HostDetailsComponentReduxProps & - HostDetailsComponentDispatchProps & - HostComponentProps & - HostsQueryProps; - -type KeyHostDetailsNavTabWithoutMlPermission = HostsTableType.authentications & - HostsTableType.uncommonProcesses & - HostsTableType.events; - -type KeyHostDetailsNavTabWithMlPermission = KeyHostsNavTabWithoutMlPermission & - HostsTableType.anomalies; - -type KeyHostDetailsNavTab = - | KeyHostDetailsNavTabWithoutMlPermission - | KeyHostDetailsNavTabWithMlPermission; - -export type HostDetailsNavTab = Record; - -export type HostDetailsTabsProps = HostBodyComponentDispatchProps & - HostsQueryProps & { - pageFilters?: Filter[]; - filterQuery: string; - indexPattern: IIndexPattern; - type: hostsModel.HostsType; - }; - -export type SetAbsoluteRangeDatePicker = ActionCreator<{ - id: InputsModelId; - from: number; - to: number; -}>; diff --git a/x-pack/plugins/siem/public/pages/hosts/details/utils.ts b/x-pack/plugins/siem/public/pages/hosts/details/utils.ts deleted file mode 100644 index af4ba8eb091e20..00000000000000 --- a/x-pack/plugins/siem/public/pages/hosts/details/utils.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { get, isEmpty } from 'lodash/fp'; - -import { ChromeBreadcrumb } from '../../../../../../../src/core/public'; -import { hostsModel } from '../../../store'; -import { HostsTableType } from '../../../store/hosts/model'; -import { getHostsUrl, getHostDetailsUrl } from '../../../components/link_to/redirect_to_hosts'; - -import * as i18n from '../translations'; -import { HostRouteSpyState } from '../../../utils/route/types'; - -export const type = hostsModel.HostsType.details; - -const TabNameMappedToI18nKey: Record = { - [HostsTableType.hosts]: i18n.NAVIGATION_ALL_HOSTS_TITLE, - [HostsTableType.authentications]: i18n.NAVIGATION_AUTHENTICATIONS_TITLE, - [HostsTableType.uncommonProcesses]: i18n.NAVIGATION_UNCOMMON_PROCESSES_TITLE, - [HostsTableType.anomalies]: i18n.NAVIGATION_ANOMALIES_TITLE, - [HostsTableType.events]: i18n.NAVIGATION_EVENTS_TITLE, - [HostsTableType.alerts]: i18n.NAVIGATION_ALERTS_TITLE, -}; - -export const getBreadcrumbs = (params: HostRouteSpyState, search: string[]): ChromeBreadcrumb[] => { - let breadcrumb = [ - { - text: i18n.PAGE_TITLE, - href: `${getHostsUrl()}${!isEmpty(search[0]) ? search[0] : ''}`, - }, - ]; - - if (params.detailName != null) { - breadcrumb = [ - ...breadcrumb, - { - text: params.detailName, - href: `${getHostDetailsUrl(params.detailName)}${!isEmpty(search[1]) ? search[1] : ''}`, - }, - ]; - } - if (params.tabName != null) { - const tabName = get('tabName', params); - if (!tabName) return breadcrumb; - - breadcrumb = [ - ...breadcrumb, - { - text: TabNameMappedToI18nKey[tabName], - href: '', - }, - ]; - } - return breadcrumb; -}; diff --git a/x-pack/plugins/siem/public/pages/hosts/index.tsx b/x-pack/plugins/siem/public/pages/hosts/index.tsx deleted file mode 100644 index 699b1441905c3a..00000000000000 --- a/x-pack/plugins/siem/public/pages/hosts/index.tsx +++ /dev/null @@ -1,95 +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 { Redirect, Route, Switch, RouteComponentProps } from 'react-router-dom'; - -import { HostDetails } from './details'; -import { HostsTableType } from '../../store/hosts/model'; - -import { GlobalTime } from '../../containers/global_time'; -import { SiemPageName } from '../home/types'; -import { Hosts } from './hosts'; -import { hostsPagePath, hostDetailsPagePath } from './types'; - -const getHostsTabPath = (pagePath: string) => - `${pagePath}/:tabName(` + - `${HostsTableType.hosts}|` + - `${HostsTableType.authentications}|` + - `${HostsTableType.uncommonProcesses}|` + - `${HostsTableType.anomalies}|` + - `${HostsTableType.events}|` + - `${HostsTableType.alerts})`; - -const getHostDetailsTabPath = (pagePath: string) => - `${hostDetailsPagePath}/:tabName(` + - `${HostsTableType.authentications}|` + - `${HostsTableType.uncommonProcesses}|` + - `${HostsTableType.anomalies}|` + - `${HostsTableType.events}|` + - `${HostsTableType.alerts})`; - -type Props = Partial> & { url: string }; - -export const HostsContainer = React.memo(({ url }) => ( - - {({ to, from, setQuery, deleteQuery, isInitializing }) => ( - - ( - - )} - /> - ( - - )} - /> - } - /> - ( - - )} - /> - - )} - -)); - -HostsContainer.displayName = 'HostsContainer'; diff --git a/x-pack/plugins/siem/public/pages/hosts/nav_tabs.test.tsx b/x-pack/plugins/siem/public/pages/hosts/nav_tabs.test.tsx deleted file mode 100644 index a42e83c835c61c..00000000000000 --- a/x-pack/plugins/siem/public/pages/hosts/nav_tabs.test.tsx +++ /dev/null @@ -1,28 +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 { HostsTableType } from '../../store/hosts/model'; -import { navTabsHosts } from './nav_tabs'; - -describe('navTabsHosts', () => { - test('it should skip anomalies tab if without mlUserPermission', () => { - const tabs = navTabsHosts(false); - expect(tabs).toHaveProperty(HostsTableType.hosts); - expect(tabs).toHaveProperty(HostsTableType.authentications); - expect(tabs).toHaveProperty(HostsTableType.uncommonProcesses); - expect(tabs).not.toHaveProperty(HostsTableType.anomalies); - expect(tabs).toHaveProperty(HostsTableType.events); - }); - - test('it should display anomalies tab if with mlUserPermission', () => { - const tabs = navTabsHosts(true); - expect(tabs).toHaveProperty(HostsTableType.hosts); - expect(tabs).toHaveProperty(HostsTableType.authentications); - expect(tabs).toHaveProperty(HostsTableType.uncommonProcesses); - expect(tabs).toHaveProperty(HostsTableType.anomalies); - expect(tabs).toHaveProperty(HostsTableType.events); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/hosts/nav_tabs.tsx b/x-pack/plugins/siem/public/pages/hosts/nav_tabs.tsx deleted file mode 100644 index 4109feff099e06..00000000000000 --- a/x-pack/plugins/siem/public/pages/hosts/nav_tabs.tsx +++ /dev/null @@ -1,62 +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 { omit } from 'lodash/fp'; -import * as i18n from './translations'; -import { HostsTableType } from '../../store/hosts/model'; -import { HostsNavTab } from './navigation/types'; -import { SiemPageName } from '../home/types'; - -const getTabsOnHostsUrl = (tabName: HostsTableType) => `#/${SiemPageName.hosts}/${tabName}`; - -export const navTabsHosts = (hasMlUserPermissions: boolean): HostsNavTab => { - const hostsNavTabs = { - [HostsTableType.hosts]: { - id: HostsTableType.hosts, - name: i18n.NAVIGATION_ALL_HOSTS_TITLE, - href: getTabsOnHostsUrl(HostsTableType.hosts), - disabled: false, - urlKey: 'host', - }, - [HostsTableType.authentications]: { - id: HostsTableType.authentications, - name: i18n.NAVIGATION_AUTHENTICATIONS_TITLE, - href: getTabsOnHostsUrl(HostsTableType.authentications), - disabled: false, - urlKey: 'host', - }, - [HostsTableType.uncommonProcesses]: { - id: HostsTableType.uncommonProcesses, - name: i18n.NAVIGATION_UNCOMMON_PROCESSES_TITLE, - href: getTabsOnHostsUrl(HostsTableType.uncommonProcesses), - disabled: false, - urlKey: 'host', - }, - [HostsTableType.anomalies]: { - id: HostsTableType.anomalies, - name: i18n.NAVIGATION_ANOMALIES_TITLE, - href: getTabsOnHostsUrl(HostsTableType.anomalies), - disabled: false, - urlKey: 'host', - }, - [HostsTableType.events]: { - id: HostsTableType.events, - name: i18n.NAVIGATION_EVENTS_TITLE, - href: getTabsOnHostsUrl(HostsTableType.events), - disabled: false, - urlKey: 'host', - }, - [HostsTableType.alerts]: { - id: HostsTableType.alerts, - name: i18n.NAVIGATION_ALERTS_TITLE, - href: getTabsOnHostsUrl(HostsTableType.alerts), - disabled: false, - urlKey: 'host', - }, - }; - - return hasMlUserPermissions ? hostsNavTabs : omit([HostsTableType.anomalies], hostsNavTabs); -}; diff --git a/x-pack/plugins/siem/public/pages/hosts/navigation/alerts_query_tab_body.tsx b/x-pack/plugins/siem/public/pages/hosts/navigation/alerts_query_tab_body.tsx deleted file mode 100644 index ec33834b1bf734..00000000000000 --- a/x-pack/plugins/siem/public/pages/hosts/navigation/alerts_query_tab_body.tsx +++ /dev/null @@ -1,54 +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, { useMemo } from 'react'; - -import { Filter } from '../../../../../../../src/plugins/data/public'; -import { AlertsView } from '../../../components/alerts_viewer'; -import { AlertsComponentQueryProps } from './types'; - -export const filterHostData: Filter[] = [ - { - query: { - bool: { - filter: [ - { - bool: { - should: [ - { - exists: { - field: 'host.name', - }, - }, - ], - minimum_should_match: 1, - }, - }, - ], - }, - }, - meta: { - alias: '', - disabled: false, - key: 'bool', - negate: false, - type: 'custom', - value: - '{"query": {"bool": {"filter": [{"bool": {"should": [{"exists": {"field": "host.name"}}],"minimum_should_match": 1}}]}}}', - }, - }, -]; -export const HostAlertsQueryTabBody = React.memo((alertsProps: AlertsComponentQueryProps) => { - const { pageFilters, ...rest } = alertsProps; - const hostPageFilters = useMemo( - () => (pageFilters != null ? [...filterHostData, ...pageFilters] : filterHostData), - [pageFilters] - ); - - return ; -}); - -HostAlertsQueryTabBody.displayName = 'HostAlertsQueryTabBody'; diff --git a/x-pack/plugins/siem/public/pages/hosts/navigation/types.ts b/x-pack/plugins/siem/public/pages/hosts/navigation/types.ts deleted file mode 100644 index 20d4d4e463a7f0..00000000000000 --- a/x-pack/plugins/siem/public/pages/hosts/navigation/types.ts +++ /dev/null @@ -1,61 +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 { ESTermQuery } from '../../../../common/typed_json'; -import { Filter, IIndexPattern } from '../../../../../../../src/plugins/data/public'; -import { NarrowDateRange } from '../../../components/ml/types'; -import { InspectQuery, Refetch } from '../../../store/inputs/model'; - -import { HostsTableType, HostsType } from '../../../store/hosts/model'; -import { NavTab } from '../../../components/navigation/types'; -import { UpdateDateRange } from '../../../components/charts/common'; - -export type KeyHostsNavTabWithoutMlPermission = HostsTableType.hosts & - HostsTableType.authentications & - HostsTableType.uncommonProcesses & - HostsTableType.events; - -type KeyHostsNavTabWithMlPermission = KeyHostsNavTabWithoutMlPermission & HostsTableType.anomalies; - -type KeyHostsNavTab = KeyHostsNavTabWithoutMlPermission | KeyHostsNavTabWithMlPermission; - -export type HostsNavTab = Record; - -export type SetQuery = ({ - id, - inspect, - loading, - refetch, -}: { - id: string; - inspect: InspectQuery | null; - loading: boolean; - refetch: Refetch; -}) => void; - -export interface QueryTabBodyProps { - type: HostsType; - startDate: number; - endDate: number; - filterQuery?: string | ESTermQuery; -} - -export type HostsComponentsQueryProps = QueryTabBodyProps & { - deleteQuery?: ({ id }: { id: string }) => void; - indexPattern: IIndexPattern; - pageFilters?: Filter[]; - skip: boolean; - setQuery: SetQuery; - updateDateRange?: UpdateDateRange; - narrowDateRange?: NarrowDateRange; -}; - -export type AlertsComponentQueryProps = HostsComponentsQueryProps & { - filterQuery: string; - pageFilters?: Filter[]; -}; - -export type CommonChildren = (args: HostsComponentsQueryProps) => JSX.Element; diff --git a/x-pack/plugins/siem/public/pages/hosts/types.ts b/x-pack/plugins/siem/public/pages/hosts/types.ts deleted file mode 100644 index 408450aebebbdc..00000000000000 --- a/x-pack/plugins/siem/public/pages/hosts/types.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 { IIndexPattern } from 'src/plugins/data/public'; -import { ActionCreator } from 'typescript-fsa'; - -import { SiemPageName } from '../home/types'; -import { hostsModel } from '../../store'; -import { GlobalTimeArgs } from '../../containers/global_time'; -import { InputsModelId } from '../../store/inputs/constants'; - -export const hostsPagePath = `/:pageName(${SiemPageName.hosts})`; -export const hostDetailsPagePath = `${hostsPagePath}/:detailName`; - -export type HostsTabsProps = HostsComponentProps & { - filterQuery: string; - type: hostsModel.HostsType; - indexPattern: IIndexPattern; - setAbsoluteRangeDatePicker: ActionCreator<{ - id: InputsModelId; - from: number; - to: number; - }>; -}; - -export type HostsQueryProps = GlobalTimeArgs; - -export type HostsComponentProps = HostsQueryProps & { hostsPagePath: string }; diff --git a/x-pack/plugins/siem/public/pages/network/index.tsx b/x-pack/plugins/siem/public/pages/network/index.tsx deleted file mode 100644 index babc153823b5a0..00000000000000 --- a/x-pack/plugins/siem/public/pages/network/index.tsx +++ /dev/null @@ -1,100 +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, { useMemo } from 'react'; -import { Redirect, Route, Switch, RouteComponentProps } from 'react-router-dom'; - -import { useMlCapabilities } from '../../components/ml_popover/hooks/use_ml_capabilities'; -import { hasMlUserPermissions } from '../../components/ml/permissions/has_ml_user_permissions'; -import { FlowTarget } from '../../graphql/types'; - -import { IPDetails } from './ip_details'; -import { Network } from './network'; -import { GlobalTime } from '../../containers/global_time'; -import { SiemPageName } from '../home/types'; -import { getNetworkRoutePath } from './navigation'; -import { NetworkRouteType } from './navigation/types'; - -type Props = Partial> & { url: string }; - -const networkPagePath = `/:pageName(${SiemPageName.network})`; -const ipDetailsPageBasePath = `${networkPagePath}/ip/:detailName`; - -const NetworkContainerComponent: React.FC = () => { - const capabilities = useMlCapabilities(); - const capabilitiesFetched = capabilities.capabilitiesFetched; - const userHasMlUserPermissions = useMemo(() => hasMlUserPermissions(capabilities), [ - capabilities, - ]); - const networkRoutePath = useMemo( - () => getNetworkRoutePath(networkPagePath, capabilitiesFetched, userHasMlUserPermissions), - [capabilitiesFetched, userHasMlUserPermissions] - ); - - return ( - - {({ to, from, setQuery, deleteQuery, isInitializing }) => ( - - ( - - )} - /> - ( - - )} - /> - ( - - )} - /> - ( - - )} - /> - - )} - - ); -}; - -export const NetworkContainer = React.memo(NetworkContainerComponent); diff --git a/x-pack/plugins/siem/public/pages/network/ip_details/index.test.tsx b/x-pack/plugins/siem/public/pages/network/ip_details/index.test.tsx deleted file mode 100644 index 02132d790796c5..00000000000000 --- a/x-pack/plugins/siem/public/pages/network/ip_details/index.test.tsx +++ /dev/null @@ -1,155 +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 { shallow } from 'enzyme'; -import { cloneDeep } from 'lodash/fp'; -import React from 'react'; -import { Router } from 'react-router-dom'; -import { MockedProvider } from 'react-apollo/test-utils'; -import { ActionCreator } from 'typescript-fsa'; - -import '../../../mock/match_media'; - -import { mocksSource } from '../../../containers/source/mock'; -import { FlowTarget } from '../../../graphql/types'; -import { apolloClientObservable, mockGlobalState, TestProviders } from '../../../mock'; -import { useMountAppended } from '../../../utils/use_mount_appended'; -import { createStore, State } from '../../../store'; -import { InputsModelId } from '../../../store/inputs/constants'; - -import { IPDetailsComponent, IPDetails } from './index'; - -type Action = 'PUSH' | 'POP' | 'REPLACE'; -const pop: Action = 'POP'; - -type GlobalWithFetch = NodeJS.Global & { fetch: jest.Mock }; - -// Test will fail because we will to need to mock some core services to make the test work -// For now let's forget about SiemSearchBar and QueryBar -jest.mock('../../../components/search_bar', () => ({ - SiemSearchBar: () => null, -})); -jest.mock('../../../components/query_bar', () => ({ - QueryBar: () => null, -})); - -let localSource: Array<{ - request: {}; - result: { - data: { - source: { - status: { - indicesExist: boolean; - }; - }; - }; - }; -}>; - -const getMockHistory = (ip: string) => ({ - length: 2, - location: { - pathname: `/network/ip/${ip}`, - search: '', - state: '', - hash: '', - }, - action: pop, - push: jest.fn(), - replace: jest.fn(), - go: jest.fn(), - goBack: jest.fn(), - goForward: jest.fn(), - block: jest.fn(), - createHref: jest.fn(), - listen: jest.fn(), -}); - -const to = new Date('2018-03-23T18:49:23.132Z').valueOf(); -const from = new Date('2018-03-24T03:33:52.253Z').valueOf(); -const getMockProps = (ip: string) => ({ - to, - from, - isInitializing: false, - setQuery: jest.fn(), - query: { query: 'coolQueryhuh?', language: 'keury' }, - filters: [], - flowTarget: FlowTarget.source, - history: getMockHistory(ip), - location: { - pathname: `/network/ip/${ip}`, - search: '', - state: '', - hash: '', - }, - detailName: ip, - match: { params: { detailName: ip, search: '' }, isExact: true, path: '', url: '' }, - setAbsoluteRangeDatePicker: (jest.fn() as unknown) as ActionCreator<{ - id: InputsModelId; - from: number; - to: number; - }>, - setIpDetailsTablesActivePageToZero: (jest.fn() as unknown) as ActionCreator, -}); - -describe('Ip Details', () => { - const mount = useMountAppended(); - - beforeAll(() => { - (global as GlobalWithFetch).fetch = jest.fn().mockImplementationOnce(() => - Promise.resolve({ - ok: true, - json: () => { - return null; - }, - }) - ); - }); - - afterAll(() => { - delete (global as GlobalWithFetch).fetch; - }); - - const state: State = mockGlobalState; - let store = createStore(state, apolloClientObservable); - - beforeEach(() => { - store = createStore(state, apolloClientObservable); - localSource = cloneDeep(mocksSource); - }); - - test('it renders', () => { - const wrapper = shallow(); - expect(wrapper.find('[data-test-subj="ip-details-page"]').exists()).toBe(true); - }); - - test('it matches the snapshot', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); - }); - - test('it renders ipv6 headline', async () => { - localSource[0].result.data.source.status.indicesExist = true; - const ip = 'fe80--24ce-f7ff-fede-a571'; - const wrapper = mount( - - - - - - - - ); - // Why => https://github.com/apollographql/react-apollo/issues/1711 - await new Promise(resolve => setTimeout(resolve)); - wrapper.update(); - expect( - wrapper - .find('[data-test-subj="ip-details-headline"] [data-test-subj="header-page-title"]') - .text() - ).toEqual('fe80::24ce:f7ff:fede:a571'); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/network/ip_details/index.tsx b/x-pack/plugins/siem/public/pages/network/ip_details/index.tsx deleted file mode 100644 index 350d6e34c1c0f7..00000000000000 --- a/x-pack/plugins/siem/public/pages/network/ip_details/index.tsx +++ /dev/null @@ -1,298 +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 { EuiHorizontalRule, EuiSpacer, EuiFlexItem } from '@elastic/eui'; -import React, { useCallback, useEffect } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import { StickyContainer } from 'react-sticky'; - -import { FiltersGlobal } from '../../../components/filters_global'; -import { HeaderPage } from '../../../components/header_page'; -import { LastEventTime } from '../../../components/last_event_time'; -import { AnomalyTableProvider } from '../../../components/ml/anomaly/anomaly_table_provider'; -import { networkToCriteria } from '../../../components/ml/criteria/network_to_criteria'; -import { scoreIntervalToDateTime } from '../../../components/ml/score/score_interval_to_datetime'; -import { AnomaliesNetworkTable } from '../../../components/ml/tables/anomalies_network_table'; -import { manageQuery } from '../../../components/page/manage_query'; -import { FlowTargetSelectConnected } from '../../../components/page/network/flow_target_select_connected'; -import { IpOverview } from '../../../components/page/network/ip_overview'; -import { SiemSearchBar } from '../../../components/search_bar'; -import { WrapperPage } from '../../../components/wrapper_page'; -import { IpOverviewQuery } from '../../../containers/ip_overview'; -import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../../containers/source'; -import { FlowTargetSourceDest, LastEventIndexKey } from '../../../graphql/types'; -import { useKibana } from '../../../lib/kibana'; -import { decodeIpv6 } from '../../../lib/helpers'; -import { convertToBuildEsQuery } from '../../../lib/keury'; -import { ConditionalFlexGroup } from '../../../pages/network/navigation/conditional_flex_group'; -import { networkModel, State, inputsSelectors } from '../../../store'; -import { setAbsoluteRangeDatePicker as dispatchAbsoluteRangeDatePicker } from '../../../store/inputs/actions'; -import { setIpDetailsTablesActivePageToZero as dispatchIpDetailsTablesActivePageToZero } from '../../../store/network/actions'; -import { SpyRoute } from '../../../utils/route/spy_routes'; -import { NetworkEmptyPage } from '../network_empty_page'; -import { NetworkHttpQueryTable } from './network_http_query_table'; -import { NetworkTopCountriesQueryTable } from './network_top_countries_query_table'; -import { NetworkTopNFlowQueryTable } from './network_top_n_flow_query_table'; -import { TlsQueryTable } from './tls_query_table'; -import { IPDetailsComponentProps } from './types'; -import { UsersQueryTable } from './users_query_table'; -import { AnomaliesQueryTabBody } from '../../../containers/anomalies/anomalies_query_tab_body'; -import { esQuery } from '../../../../../../../src/plugins/data/public'; - -export { getBreadcrumbs } from './utils'; - -const IpOverviewManage = manageQuery(IpOverview); - -export const IPDetailsComponent: React.FC = ({ - detailName, - filters, - flowTarget, - from, - isInitializing, - query, - setAbsoluteRangeDatePicker, - setIpDetailsTablesActivePageToZero, - setQuery, - to, -}) => { - const type = networkModel.NetworkType.details; - const narrowDateRange = useCallback( - (score, interval) => { - const fromTo = scoreIntervalToDateTime(score, interval); - setAbsoluteRangeDatePicker({ - id: 'global', - from: fromTo.from, - to: fromTo.to, - }); - }, - [setAbsoluteRangeDatePicker] - ); - const kibana = useKibana(); - - useEffect(() => { - setIpDetailsTablesActivePageToZero(); - }, [detailName, setIpDetailsTablesActivePageToZero]); - - return ( - <> - - {({ indicesExist, indexPattern }) => { - const ip = decodeIpv6(detailName); - const filterQuery = convertToBuildEsQuery({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - indexPattern, - queries: [query], - filters, - }); - - return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - - - - - - } - title={ip} - > - - - - - {({ id, inspect, ipOverviewData, loading, refetch }) => ( - - {({ isLoadingAnomaliesData, anomaliesData }) => ( - - )} - - )} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ) : ( - - - - - - ); - }} - - - - - ); -}; -IPDetailsComponent.displayName = 'IPDetailsComponent'; - -const makeMapStateToProps = () => { - const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); - const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); - - return (state: State) => ({ - query: getGlobalQuerySelector(state), - filters: getGlobalFiltersQuerySelector(state), - }); -}; - -const mapDispatchToProps = { - setAbsoluteRangeDatePicker: dispatchAbsoluteRangeDatePicker, - setIpDetailsTablesActivePageToZero: dispatchIpDetailsTablesActivePageToZero, -}; - -export const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const IPDetails = connector(React.memo(IPDetailsComponent)); diff --git a/x-pack/plugins/siem/public/pages/network/ip_details/types.ts b/x-pack/plugins/siem/public/pages/network/ip_details/types.ts deleted file mode 100644 index 11c41fc74515e2..00000000000000 --- a/x-pack/plugins/siem/public/pages/network/ip_details/types.ts +++ /dev/null @@ -1,53 +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 { IIndexPattern } from 'src/plugins/data/public'; - -import { ESTermQuery } from '../../../../common/typed_json'; -import { NetworkType } from '../../../store/network/model'; -import { InspectQuery, Refetch } from '../../../store/inputs/model'; -import { FlowTarget, FlowTargetSourceDest } from '../../../graphql/types'; -import { GlobalTimeArgs } from '../../../containers/global_time'; - -export const type = NetworkType.details; - -export type IPDetailsComponentProps = GlobalTimeArgs & { - detailName: string; - flowTarget: FlowTarget; -}; - -export interface OwnProps { - type: NetworkType; - startDate: number; - endDate: number; - filterQuery: string | ESTermQuery; - ip: string; - skip: boolean; - setQuery: ({ - id, - inspect, - loading, - refetch, - }: { - id: string; - inspect: InspectQuery | null; - loading: boolean; - refetch: Refetch; - }) => void; -} - -export type NetworkComponentsQueryProps = OwnProps & { - flowTarget: FlowTarget; -}; - -export type TlsQueryTableComponentProps = OwnProps & { - flowTarget: FlowTargetSourceDest; -}; - -export type NetworkWithIndexComponentsQueryTableProps = OwnProps & { - flowTarget: FlowTargetSourceDest; - indexPattern: IIndexPattern; -}; diff --git a/x-pack/plugins/siem/public/pages/network/ip_details/utils.ts b/x-pack/plugins/siem/public/pages/network/ip_details/utils.ts deleted file mode 100644 index 9d15d7ee250c99..00000000000000 --- a/x-pack/plugins/siem/public/pages/network/ip_details/utils.ts +++ /dev/null @@ -1,60 +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 { get, isEmpty } from 'lodash/fp'; - -import { ChromeBreadcrumb } from '../../../../../../../src/core/public'; -import { decodeIpv6 } from '../../../lib/helpers'; -import { getNetworkUrl, getIPDetailsUrl } from '../../../components/link_to/redirect_to_network'; -import { networkModel } from '../../../store/network'; -import * as i18n from '../translations'; -import { NetworkRouteType } from '../navigation/types'; -import { NetworkRouteSpyState } from '../../../utils/route/types'; - -export const type = networkModel.NetworkType.details; -const TabNameMappedToI18nKey: Record = { - [NetworkRouteType.alerts]: i18n.NAVIGATION_ALERTS_TITLE, - [NetworkRouteType.anomalies]: i18n.NAVIGATION_ANOMALIES_TITLE, - [NetworkRouteType.flows]: i18n.NAVIGATION_FLOWS_TITLE, - [NetworkRouteType.dns]: i18n.NAVIGATION_DNS_TITLE, - [NetworkRouteType.http]: i18n.NAVIGATION_HTTP_TITLE, - [NetworkRouteType.tls]: i18n.NAVIGATION_TLS_TITLE, -}; - -export const getBreadcrumbs = ( - params: NetworkRouteSpyState, - search: string[] -): ChromeBreadcrumb[] => { - let breadcrumb = [ - { - text: i18n.PAGE_TITLE, - href: `${getNetworkUrl()}${!isEmpty(search[0]) ? search[0] : ''}`, - }, - ]; - if (params.detailName != null) { - breadcrumb = [ - ...breadcrumb, - { - text: decodeIpv6(params.detailName), - href: `${getIPDetailsUrl(params.detailName, params.flowTarget)}${ - !isEmpty(search[1]) ? search[1] : '' - }`, - }, - ]; - } - - const tabName = get('tabName', params); - if (!tabName) return breadcrumb; - - breadcrumb = [ - ...breadcrumb, - { - text: TabNameMappedToI18nKey[tabName], - href: '', - }, - ]; - return breadcrumb; -}; diff --git a/x-pack/plugins/siem/public/pages/network/navigation/alerts_query_tab_body.tsx b/x-pack/plugins/siem/public/pages/network/navigation/alerts_query_tab_body.tsx deleted file mode 100644 index 4c4f6c06ce1e1a..00000000000000 --- a/x-pack/plugins/siem/public/pages/network/navigation/alerts_query_tab_body.tsx +++ /dev/null @@ -1,68 +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 { Filter } from '../../../../../../../src/plugins/data/common/es_query'; -import { AlertsView } from '../../../components/alerts_viewer'; -import { NetworkComponentQueryProps } from './types'; - -export const filterNetworkData: Filter[] = [ - { - query: { - bool: { - filter: [ - { - bool: { - should: [ - { - bool: { - should: [ - { - exists: { - field: 'source.ip', - }, - }, - ], - minimum_should_match: 1, - }, - }, - { - bool: { - should: [ - { - exists: { - field: 'destination.ip', - }, - }, - ], - minimum_should_match: 1, - }, - }, - ], - minimum_should_match: 1, - }, - }, - ], - }, - }, - meta: { - alias: '', - disabled: false, - key: 'bool', - negate: false, - type: 'custom', - value: - '{"bool":{"filter":[{"bool":{"should":[{"bool":{"should":[{"exists":{"field": "source.ip"}}],"minimum_should_match":1}},{"bool":{"should":[{"exists":{"field": "destination.ip"}}],"minimum_should_match":1}}],"minimum_should_match":1}}]}}', - }, - }, -]; - -export const NetworkAlertsQueryTabBody = React.memo((alertsProps: NetworkComponentQueryProps) => ( - -)); - -NetworkAlertsQueryTabBody.displayName = 'NetworkAlertsQueryTabBody'; diff --git a/x-pack/plugins/siem/public/pages/network/navigation/types.ts b/x-pack/plugins/siem/public/pages/network/navigation/types.ts deleted file mode 100644 index ee03bff99b9677..00000000000000 --- a/x-pack/plugins/siem/public/pages/network/navigation/types.ts +++ /dev/null @@ -1,77 +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 { ESTermQuery } from '../../../../common/typed_json'; -import { IIndexPattern } from '../../../../../../../src/plugins/data/common'; - -import { NavTab } from '../../../components/navigation/types'; -import { FlowTargetSourceDest } from '../../../graphql/types'; -import { networkModel } from '../../../store'; -import { GlobalTimeArgs } from '../../../containers/global_time'; - -import { SetAbsoluteRangeDatePicker } from '../types'; -import { NarrowDateRange } from '../../../components/ml/types'; - -interface QueryTabBodyProps extends Pick { - skip: boolean; - type: networkModel.NetworkType; - startDate: number; - endDate: number; - filterQuery?: string | ESTermQuery; - narrowDateRange?: NarrowDateRange; -} - -export type NetworkComponentQueryProps = QueryTabBodyProps; - -export type IPsQueryTabBodyProps = QueryTabBodyProps & { - indexPattern: IIndexPattern; - flowTarget: FlowTargetSourceDest; -}; - -export type TlsQueryTabBodyProps = QueryTabBodyProps & { - flowTarget: FlowTargetSourceDest; - ip?: string; -}; - -export type HttpQueryTabBodyProps = QueryTabBodyProps & { - ip?: string; -}; - -export type NetworkRoutesProps = GlobalTimeArgs & { - networkPagePath: string; - type: networkModel.NetworkType; - filterQuery?: string | ESTermQuery; - indexPattern: IIndexPattern; - setAbsoluteRangeDatePicker: SetAbsoluteRangeDatePicker; -}; - -export type KeyNetworkNavTabWithoutMlPermission = NetworkRouteType.dns & - NetworkRouteType.flows & - NetworkRouteType.http & - NetworkRouteType.tls & - NetworkRouteType.alerts; - -type KeyNetworkNavTabWithMlPermission = KeyNetworkNavTabWithoutMlPermission & - NetworkRouteType.anomalies; - -type KeyNetworkNavTab = KeyNetworkNavTabWithoutMlPermission | KeyNetworkNavTabWithMlPermission; - -export type NetworkNavTab = Record; - -export enum NetworkRouteType { - flows = 'flows', - dns = 'dns', - anomalies = 'anomalies', - tls = 'tls', - http = 'http', - alerts = 'alerts', -} - -export type GetNetworkRoutePath = ( - pagePath: string, - capabilitiesFetched: boolean, - hasMlUserPermission: boolean -) => string; diff --git a/x-pack/plugins/siem/public/pages/network/network.tsx b/x-pack/plugins/siem/public/pages/network/network.tsx deleted file mode 100644 index 698f51efbb451c..00000000000000 --- a/x-pack/plugins/siem/public/pages/network/network.tsx +++ /dev/null @@ -1,199 +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 { EuiSpacer } from '@elastic/eui'; -import React, { useCallback, useMemo } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; -import { useParams } from 'react-router-dom'; -import { StickyContainer } from 'react-sticky'; - -import { esQuery } from '../../../../../../src/plugins/data/public'; -import { UpdateDateRange } from '../../components/charts/common'; -import { EmbeddedMap } from '../../components/embeddables/embedded_map'; -import { FiltersGlobal } from '../../components/filters_global'; -import { HeaderPage } from '../../components/header_page'; -import { LastEventTime } from '../../components/last_event_time'; -import { SiemNavigation } from '../../components/navigation'; -import { manageQuery } from '../../components/page/manage_query'; -import { KpiNetworkComponent } from '../../components/page/network'; -import { SiemSearchBar } from '../../components/search_bar'; -import { WrapperPage } from '../../components/wrapper_page'; -import { KpiNetworkQuery } from '../../containers/kpi_network'; -import { indicesExistOrDataTemporarilyUnavailable, WithSource } from '../../containers/source'; -import { LastEventIndexKey } from '../../graphql/types'; -import { useKibana } from '../../lib/kibana'; -import { convertToBuildEsQuery } from '../../lib/keury'; -import { networkModel, State, inputsSelectors } from '../../store'; -import { setAbsoluteRangeDatePicker as dispatchSetAbsoluteRangeDatePicker } from '../../store/inputs/actions'; -import { SpyRoute } from '../../utils/route/spy_routes'; -import { navTabsNetwork, NetworkRoutes, NetworkRoutesLoading } from './navigation'; -import { filterNetworkData } from './navigation/alerts_query_tab_body'; -import { NetworkEmptyPage } from './network_empty_page'; -import * as i18n from './translations'; -import { NetworkComponentProps } from './types'; -import { NetworkRouteType } from './navigation/types'; - -const KpiNetworkComponentManage = manageQuery(KpiNetworkComponent); -const sourceId = 'default'; - -const NetworkComponent = React.memo( - ({ - filters, - query, - setAbsoluteRangeDatePicker, - networkPagePath, - to, - from, - setQuery, - isInitializing, - hasMlUserPermissions, - capabilitiesFetched, - }) => { - const kibana = useKibana(); - const { tabName } = useParams(); - - const tabsFilters = useMemo(() => { - if (tabName === NetworkRouteType.alerts) { - return filters.length > 0 ? [...filters, ...filterNetworkData] : filterNetworkData; - } - return filters; - }, [tabName, filters]); - - const narrowDateRange = useCallback( - ({ x }) => { - if (!x) { - return; - } - const [min, max] = x; - setAbsoluteRangeDatePicker({ id: 'global', from: min, to: max }); - }, - [setAbsoluteRangeDatePicker] - ); - - return ( - <> - - {({ indicesExist, indexPattern }) => { - const filterQuery = convertToBuildEsQuery({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - indexPattern, - queries: [query], - filters, - }); - const tabsFilterQuery = convertToBuildEsQuery({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - indexPattern, - queries: [query], - filters: tabsFilters, - }); - - return indicesExistOrDataTemporarilyUnavailable(indicesExist) ? ( - - - - - - - } - title={i18n.PAGE_TITLE} - /> - - - - - - - {({ kpiNetwork, loading, id, inspect, refetch }) => ( - - )} - - - {capabilitiesFetched && !isInitializing ? ( - <> - - - - - - - - - ) : ( - - )} - - - - - ) : ( - - - - - ); - }} - - - - - ); - } -); -NetworkComponent.displayName = 'NetworkComponent'; - -const makeMapStateToProps = () => { - const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); - const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); - const mapStateToProps = (state: State) => ({ - query: getGlobalQuerySelector(state), - filters: getGlobalFiltersQuerySelector(state), - }); - return mapStateToProps; -}; - -const mapDispatchToProps = { - setAbsoluteRangeDatePicker: dispatchSetAbsoluteRangeDatePicker, -}; - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const Network = connector(NetworkComponent); diff --git a/x-pack/plugins/siem/public/pages/network/types.ts b/x-pack/plugins/siem/public/pages/network/types.ts deleted file mode 100644 index 01d3fb6b48c631..00000000000000 --- a/x-pack/plugins/siem/public/pages/network/types.ts +++ /dev/null @@ -1,23 +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 { RouteComponentProps } from 'react-router-dom'; -import { ActionCreator } from 'typescript-fsa'; -import { InputsModelId } from '../../store/inputs/constants'; -import { GlobalTimeArgs } from '../../containers/global_time'; - -export type SetAbsoluteRangeDatePicker = ActionCreator<{ - id: InputsModelId; - from: number; - to: number; -}>; - -export type NetworkComponentProps = Partial> & - GlobalTimeArgs & { - networkPagePath: string; - hasMlUserPermissions: boolean; - capabilitiesFetched: boolean; - }; diff --git a/x-pack/plugins/siem/public/pages/overview/alerts_by_category/index.test.tsx b/x-pack/plugins/siem/public/pages/overview/alerts_by_category/index.test.tsx deleted file mode 100644 index bd9743bdccb4b6..00000000000000 --- a/x-pack/plugins/siem/public/pages/overview/alerts_by_category/index.test.tsx +++ /dev/null @@ -1,135 +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. - */ - -/* eslint-disable react/display-name */ - -import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import { mount, ReactWrapper } from 'enzyme'; -import React from 'react'; -import { ThemeProvider } from 'styled-components'; - -import { useQuery } from '../../../containers/matrix_histogram'; -import { wait } from '../../../lib/helpers'; -import { mockIndexPattern, TestProviders } from '../../../mock'; - -import { AlertsByCategory } from '.'; - -jest.mock('../../../lib/kibana'); - -jest.mock('../../../containers/matrix_histogram', () => { - return { - useQuery: jest.fn(), - }; -}); - -const theme = () => ({ eui: { ...euiDarkVars, euiSizeL: '24px' }, darkMode: true }); -const from = new Date('2020-03-31T06:00:00.000Z').valueOf(); -const to = new Date('2019-03-31T06:00:00.000Z').valueOf(); - -describe('Alerts by category', () => { - let wrapper: ReactWrapper; - - describe('before loading data', () => { - beforeAll(async () => { - (useQuery as jest.Mock).mockReturnValue({ - data: null, - loading: false, - inspect: false, - totalCount: null, - }); - - wrapper = mount( - - - - - - ); - - await wait(); - wrapper.update(); - }); - - test('it renders the expected title', () => { - expect(wrapper.find('[data-test-subj="header-section-title"]').text()).toEqual( - 'External alert count' - ); - }); - - test('it renders the subtitle (to prevent layout thrashing)', () => { - expect(wrapper.find('[data-test-subj="header-panel-subtitle"]').exists()).toBe(true); - }); - - test('it renders the expected filter fields', () => { - const expectedOptions = ['event.category', 'event.module']; - - expectedOptions.forEach(option => { - expect(wrapper.find(`option[value="${option}"]`).text()).toEqual(option); - }); - }); - - test('it renders the `View alerts` button', () => { - expect(wrapper.find('[data-test-subj="view-alerts"]').exists()).toBe(true); - }); - - test('it does NOT render the bar chart when data is not available', () => { - expect(wrapper.find(`.echChart`).exists()).toBe(false); - }); - }); - - describe('after loading data', () => { - beforeAll(async () => { - (useQuery as jest.Mock).mockReturnValue({ - data: [ - { x: 1, y: 2, g: 'g1' }, - { x: 2, y: 4, g: 'g1' }, - { x: 3, y: 6, g: 'g1' }, - { x: 1, y: 1, g: 'g2' }, - { x: 2, y: 3, g: 'g2' }, - { x: 3, y: 5, g: 'g2' }, - ], - loading: false, - inspect: false, - totalCount: 6, - }); - - wrapper = mount( - - - - - - ); - - await wait(); - wrapper.update(); - }); - - test('it renders the expected subtitle', () => { - expect(wrapper.find('[data-test-subj="header-panel-subtitle"]').text()).toEqual( - 'Showing: 6 external alerts' - ); - }); - - test('it renders the bar chart when data is available', () => { - expect(wrapper.find(`.echChart`).exists()).toBe(true); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/overview/alerts_by_category/index.tsx b/x-pack/plugins/siem/public/pages/overview/alerts_by_category/index.tsx deleted file mode 100644 index a1936cf9221f85..00000000000000 --- a/x-pack/plugins/siem/public/pages/overview/alerts_by_category/index.tsx +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiButton } from '@elastic/eui'; -import numeral from '@elastic/numeral'; -import React, { useEffect, useMemo } from 'react'; -import { Position } from '@elastic/charts'; - -import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; -import { SHOWING, UNIT } from '../../../components/alerts_viewer/translations'; -import { getDetectionEngineAlertUrl } from '../../../components/link_to/redirect_to_detection_engine'; -import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; -import { useKibana, useUiSetting$ } from '../../../lib/kibana'; -import { convertToBuildEsQuery } from '../../../lib/keury'; -import { - Filter, - esQuery, - IIndexPattern, - Query, -} from '../../../../../../../src/plugins/data/public'; -import { inputsModel } from '../../../store'; -import { HostsType } from '../../../store/hosts/model'; - -import * as i18n from '../translations'; -import { - alertsStackByOptions, - histogramConfigs, -} from '../../../components/alerts_viewer/histogram_configs'; -import { MatrixHisrogramConfigs } from '../../../components/matrix_histogram/types'; -import { useGetUrlSearch } from '../../../components/navigation/use_get_url_search'; -import { navTabs } from '../../home/home_navigations'; - -const ID = 'alertsByCategoryOverview'; - -const NO_FILTERS: Filter[] = []; -const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; -const DEFAULT_STACK_BY = 'event.module'; - -interface Props { - deleteQuery?: ({ id }: { id: string }) => void; - filters?: Filter[]; - from: number; - hideHeaderChildren?: boolean; - indexPattern: IIndexPattern; - query?: Query; - setQuery: (params: { - id: string; - inspect: inputsModel.InspectQuery | null; - loading: boolean; - refetch: inputsModel.Refetch; - }) => void; - to: number; -} - -const AlertsByCategoryComponent: React.FC = ({ - deleteQuery, - filters = NO_FILTERS, - from, - hideHeaderChildren = false, - indexPattern, - query = DEFAULT_QUERY, - setQuery, - to, -}) => { - useEffect(() => { - return () => { - if (deleteQuery) { - deleteQuery({ id: ID }); - } - }; - }, []); - - const kibana = useKibana(); - const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); - const urlSearch = useGetUrlSearch(navTabs.detections); - - const alertsCountViewAlertsButton = useMemo( - () => ( - - {i18n.VIEW_ALERTS} - - ), - [urlSearch] - ); - - const alertsByCategoryHistogramConfigs: MatrixHisrogramConfigs = useMemo( - () => ({ - ...histogramConfigs, - defaultStackByOption: - alertsStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? alertsStackByOptions[0], - subtitle: (totalCount: number) => - `${SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${UNIT(totalCount)}`, - legendPosition: Position.Right, - }), - [] - ); - - return ( - - ); -}; - -AlertsByCategoryComponent.displayName = 'AlertsByCategoryComponent'; - -export const AlertsByCategory = React.memo(AlertsByCategoryComponent); diff --git a/x-pack/plugins/siem/public/pages/overview/event_counts/index.test.tsx b/x-pack/plugins/siem/public/pages/overview/event_counts/index.test.tsx deleted file mode 100644 index f5419a3ff50e9e..00000000000000 --- a/x-pack/plugins/siem/public/pages/overview/event_counts/index.test.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { mount } from 'enzyme'; -import React from 'react'; - -import { OverviewHostProps } from '../../../components/page/overview/overview_host'; -import { OverviewNetworkProps } from '../../../components/page/overview/overview_network'; -import { mockIndexPattern, TestProviders } from '../../../mock'; - -import { EventCounts } from '.'; - -describe('EventCounts', () => { - const from = 1579553397080; - const to = 1579639797080; - - test('it filters the `Host events` widget with a `host.name` `exists` filter', () => { - const wrapper = mount( - - - - ); - - expect( - (wrapper - .find('[data-test-subj="overview-host-query"]') - .first() - .props() as OverviewHostProps).filterQuery - ).toContain('[{"bool":{"should":[{"exists":{"field":"host.name"}}]'); - }); - - test('it filters the `Network events` widget with a `source.ip` or `destination.ip` `exists` filter', () => { - const wrapper = mount( - - - - ); - - expect( - (wrapper - .find('[data-test-subj="overview-network-query"]') - .first() - .props() as OverviewNetworkProps).filterQuery - ).toContain( - '{"bool":{"filter":[{"bool":{"should":[{"bool":{"should":[{"exists":{"field":"source.ip"}}],"minimum_should_match":1}},{"bool":{"should":[{"exists":{"field":"destination.ip"}}],"minimum_should_match":1}}],"minimum_should_match":1}}]}}]' - ); - }); -}); diff --git a/x-pack/plugins/siem/public/pages/overview/event_counts/index.tsx b/x-pack/plugins/siem/public/pages/overview/event_counts/index.tsx deleted file mode 100644 index f242b0d84d7c13..00000000000000 --- a/x-pack/plugins/siem/public/pages/overview/event_counts/index.tsx +++ /dev/null @@ -1,91 +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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import React from 'react'; -import styled from 'styled-components'; - -import { OverviewHost } from '../../../components/page/overview/overview_host'; -import { OverviewNetwork } from '../../../components/page/overview/overview_network'; -import { filterHostData } from '../../hosts/navigation/alerts_query_tab_body'; -import { useKibana } from '../../../lib/kibana'; -import { convertToBuildEsQuery } from '../../../lib/keury'; -import { filterNetworkData } from '../../network/navigation/alerts_query_tab_body'; -import { - Filter, - esQuery, - IIndexPattern, - Query, -} from '../../../../../../../src/plugins/data/public'; -import { inputsModel } from '../../../store'; - -const HorizontalSpacer = styled(EuiFlexItem)` - width: 24px; -`; - -const NO_FILTERS: Filter[] = []; -const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; - -interface Props { - filters?: Filter[]; - from: number; - indexPattern: IIndexPattern; - query?: Query; - setQuery: (params: { - id: string; - inspect: inputsModel.InspectQuery | null; - loading: boolean; - refetch: inputsModel.Refetch; - }) => void; - to: number; -} - -const EventCountsComponent: React.FC = ({ - filters = NO_FILTERS, - from, - indexPattern, - query = DEFAULT_QUERY, - setQuery, - to, -}) => { - const kibana = useKibana(); - - return ( - - - - - - - - - - - - ); -}; - -export const EventCounts = React.memo(EventCountsComponent); diff --git a/x-pack/plugins/siem/public/pages/overview/events_by_dataset/index.tsx b/x-pack/plugins/siem/public/pages/overview/events_by_dataset/index.tsx deleted file mode 100644 index 77d6da7a7efc4d..00000000000000 --- a/x-pack/plugins/siem/public/pages/overview/events_by_dataset/index.tsx +++ /dev/null @@ -1,174 +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 { Position } from '@elastic/charts'; -import { EuiButton } from '@elastic/eui'; -import numeral from '@elastic/numeral'; -import React, { useEffect, useMemo } from 'react'; -import uuid from 'uuid'; - -import { DEFAULT_NUMBER_FORMAT } from '../../../../common/constants'; -import { SHOWING, UNIT } from '../../../components/events_viewer/translations'; -import { getTabsOnHostsUrl } from '../../../components/link_to/redirect_to_hosts'; -import { MatrixHistogramContainer } from '../../../components/matrix_histogram'; -import { - MatrixHisrogramConfigs, - MatrixHistogramOption, -} from '../../../components/matrix_histogram/types'; -import { useGetUrlSearch } from '../../../components/navigation/use_get_url_search'; -import { navTabs } from '../../home/home_navigations'; -import { eventsStackByOptions } from '../../hosts/navigation'; -import { convertToBuildEsQuery } from '../../../lib/keury'; -import { useKibana, useUiSetting$ } from '../../../lib/kibana'; -import { histogramConfigs } from '../../../pages/hosts/navigation/events_query_tab_body'; -import { - Filter, - esQuery, - IIndexPattern, - Query, -} from '../../../../../../../src/plugins/data/public'; -import { inputsModel } from '../../../store'; -import { HostsTableType, HostsType } from '../../../store/hosts/model'; -import { InputsModelId } from '../../../store/inputs/constants'; - -import * as i18n from '../translations'; - -const NO_FILTERS: Filter[] = []; -const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; -const DEFAULT_STACK_BY = 'event.dataset'; - -const ID = 'eventsByDatasetOverview'; - -interface Props { - combinedQueries?: string; - deleteQuery?: ({ id }: { id: string }) => void; - filters?: Filter[]; - from: number; - headerChildren?: React.ReactNode; - indexPattern: IIndexPattern; - indexToAdd?: string[] | null; - onlyField?: string; - query?: Query; - setAbsoluteRangeDatePickerTarget?: InputsModelId; - setQuery: (params: { - id: string; - inspect: inputsModel.InspectQuery | null; - loading: boolean; - refetch: inputsModel.Refetch; - }) => void; - showSpacer?: boolean; - to: number; -} - -const getHistogramOption = (fieldName: string): MatrixHistogramOption => ({ - text: fieldName, - value: fieldName, -}); - -const EventsByDatasetComponent: React.FC = ({ - combinedQueries, - deleteQuery, - filters = NO_FILTERS, - from, - headerChildren, - indexPattern, - indexToAdd, - onlyField, - query = DEFAULT_QUERY, - setAbsoluteRangeDatePickerTarget, - setQuery, - showSpacer = true, - to, -}) => { - // create a unique, but stable (across re-renders) query id - const uniqueQueryId = useMemo(() => `${ID}-${uuid.v4()}`, []); - - useEffect(() => { - return () => { - if (deleteQuery) { - deleteQuery({ id: uniqueQueryId }); - } - }; - }, [deleteQuery, uniqueQueryId]); - - const kibana = useKibana(); - const [defaultNumberFormat] = useUiSetting$(DEFAULT_NUMBER_FORMAT); - const urlSearch = useGetUrlSearch(navTabs.hosts); - - const eventsCountViewEventsButton = useMemo( - () => ( - - {i18n.VIEW_EVENTS} - - ), - [urlSearch] - ); - - const filterQuery = useMemo( - () => - combinedQueries == null - ? convertToBuildEsQuery({ - config: esQuery.getEsQueryConfig(kibana.services.uiSettings), - indexPattern, - queries: [query], - filters, - }) - : combinedQueries, - [combinedQueries, kibana, indexPattern, query, filters] - ); - - const eventsByDatasetHistogramConfigs: MatrixHisrogramConfigs = useMemo( - () => ({ - ...histogramConfigs, - stackByOptions: - onlyField != null ? [getHistogramOption(onlyField)] : histogramConfigs.stackByOptions, - defaultStackByOption: - onlyField != null - ? getHistogramOption(onlyField) - : eventsStackByOptions.find(o => o.text === DEFAULT_STACK_BY) ?? eventsStackByOptions[0], - legendPosition: Position.Right, - subtitle: (totalCount: number) => - `${SHOWING}: ${numeral(totalCount).format(defaultNumberFormat)} ${UNIT(totalCount)}`, - titleSize: onlyField == null ? 'm' : 's', - }), - [onlyField, defaultNumberFormat] - ); - - const headerContent = useMemo(() => { - if (onlyField == null || headerChildren != null) { - return ( - <> - {headerChildren} - {onlyField == null && eventsCountViewEventsButton} - - ); - } else { - return null; - } - }, [onlyField, headerChildren, eventsCountViewEventsButton]); - - return ( - - ); -}; - -EventsByDatasetComponent.displayName = 'EventsByDatasetComponent'; - -export const EventsByDataset = React.memo(EventsByDatasetComponent); diff --git a/x-pack/plugins/siem/public/pages/overview/overview_empty/index.tsx b/x-pack/plugins/siem/public/pages/overview/overview_empty/index.tsx deleted file mode 100644 index 1325826f172c71..00000000000000 --- a/x-pack/plugins/siem/public/pages/overview/overview_empty/index.tsx +++ /dev/null @@ -1,35 +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 * as i18nCommon from '../../common/translations'; -import { EmptyPage } from '../../../components/empty_page'; -import { useKibana } from '../../../lib/kibana'; - -const OverviewEmptyComponent: React.FC = () => { - const { http, docLinks } = useKibana().services; - const basePath = http.basePath.get(); - - return ( - - ); -}; - -OverviewEmptyComponent.displayName = 'OverviewEmptyComponent'; - -export const OverviewEmpty = React.memo(OverviewEmptyComponent); diff --git a/x-pack/plugins/siem/public/pages/overview/sidebar/index.tsx b/x-pack/plugins/siem/public/pages/overview/sidebar/index.tsx deleted file mode 100644 index 3797eae2bb8536..00000000000000 --- a/x-pack/plugins/siem/public/pages/overview/sidebar/index.tsx +++ /dev/null @@ -1,32 +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, { useState } from 'react'; - -import { FilterMode as RecentTimelinesFilterMode } from '../../../components/recent_timelines/types'; -import { FilterMode as RecentCasesFilterMode } from '../../../components/recent_cases/types'; - -import { Sidebar } from './sidebar'; - -export const StatefulSidebar = React.memo(() => { - const [recentTimelinesFilterBy, setRecentTimelinesFilterBy] = useState( - 'favorites' - ); - const [recentCasesFilterBy, setRecentCasesFilterBy] = useState( - 'recentlyCreated' - ); - - return ( - - ); -}); - -StatefulSidebar.displayName = 'StatefulSidebar'; diff --git a/x-pack/plugins/siem/public/pages/overview/signals_by_category/index.tsx b/x-pack/plugins/siem/public/pages/overview/signals_by_category/index.tsx deleted file mode 100644 index e5863effa906d4..00000000000000 --- a/x-pack/plugins/siem/public/pages/overview/signals_by_category/index.tsx +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useCallback } from 'react'; - -import { SignalsHistogramPanel } from '../../detection_engine/components/signals_histogram_panel'; -import { signalsHistogramOptions } from '../../detection_engine/components/signals_histogram_panel/config'; -import { useSignalIndex } from '../../../containers/detection_engine/signals/use_signal_index'; -import { SetAbsoluteRangeDatePicker } from '../../network/types'; -import { Filter, IIndexPattern, Query } from '../../../../../../../src/plugins/data/public'; -import { inputsModel } from '../../../store'; -import { InputsModelId } from '../../../store/inputs/constants'; -import * as i18n from '../translations'; -import { UpdateDateRange } from '../../../components/charts/common'; - -const DEFAULT_QUERY: Query = { query: '', language: 'kuery' }; -const DEFAULT_STACK_BY = 'signal.rule.threat.tactic.name'; -const NO_FILTERS: Filter[] = []; - -interface Props { - deleteQuery?: ({ id }: { id: string }) => void; - filters?: Filter[]; - from: number; - headerChildren?: React.ReactNode; - indexPattern: IIndexPattern; - /** Override all defaults, and only display this field */ - onlyField?: string; - query?: Query; - setAbsoluteRangeDatePicker: SetAbsoluteRangeDatePicker; - setAbsoluteRangeDatePickerTarget?: InputsModelId; - setQuery: (params: { - id: string; - inspect: inputsModel.InspectQuery | null; - loading: boolean; - refetch: inputsModel.Refetch; - }) => void; - to: number; -} - -const SignalsByCategoryComponent: React.FC = ({ - deleteQuery, - filters = NO_FILTERS, - from, - headerChildren, - onlyField, - query = DEFAULT_QUERY, - setAbsoluteRangeDatePicker, - setAbsoluteRangeDatePickerTarget = 'global', - setQuery, - to, -}) => { - const { signalIndexName } = useSignalIndex(); - const updateDateRangeCallback = useCallback( - ({ x }) => { - if (!x) { - return; - } - const [min, max] = x; - setAbsoluteRangeDatePicker({ id: setAbsoluteRangeDatePickerTarget, from: min, to: max }); - }, - [setAbsoluteRangeDatePicker] - ); - - const defaultStackByOption = - signalsHistogramOptions.find(o => o.text === DEFAULT_STACK_BY) ?? signalsHistogramOptions[0]; - - return ( - - ); -}; - -SignalsByCategoryComponent.displayName = 'SignalsByCategoryComponent'; - -export const SignalsByCategory = React.memo(SignalsByCategoryComponent); diff --git a/x-pack/plugins/siem/public/pages/timelines/index.tsx b/x-pack/plugins/siem/public/pages/timelines/index.tsx deleted file mode 100644 index 343be5cbe3839e..00000000000000 --- a/x-pack/plugins/siem/public/pages/timelines/index.tsx +++ /dev/null @@ -1,71 +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 { ApolloConsumer } from 'react-apollo'; -import { Switch, Route, Redirect } from 'react-router-dom'; - -import { ChromeBreadcrumb } from '../../../../../../src/core/public'; - -import { TimelineType } from '../../../common/types/timeline'; -import { TAB_TIMELINES, TAB_TEMPLATES } from '../../components/open_timeline/translations'; -import { getTimelinesUrl } from '../../components/link_to'; -import { TimelineRouteSpyState } from '../../utils/route/types'; - -import { SiemPageName } from '../home/types'; - -import { TimelinesPage } from './timelines_page'; -import { PAGE_TITLE } from './translations'; -import { appendSearch } from '../../components/link_to/helpers'; -const timelinesPagePath = `/:pageName(${SiemPageName.timelines})/:tabName(${TimelineType.default}|${TimelineType.template})`; -const timelinesDefaultPath = `/${SiemPageName.timelines}/${TimelineType.default}`; - -const TabNameMappedToI18nKey: Record = { - [TimelineType.default]: TAB_TIMELINES, - [TimelineType.template]: TAB_TEMPLATES, -}; - -export const getBreadcrumbs = ( - params: TimelineRouteSpyState, - search: string[] -): ChromeBreadcrumb[] => { - let breadcrumb = [ - { - text: PAGE_TITLE, - href: `${getTimelinesUrl(appendSearch(search[1]))}`, - }, - ]; - - const tabName = params?.tabName; - if (!tabName) return breadcrumb; - - breadcrumb = [ - ...breadcrumb, - { - text: TabNameMappedToI18nKey[tabName], - href: '', - }, - ]; - return breadcrumb; -}; - -export const Timelines = React.memo(() => { - return ( - - - {client => } - - ( - - )} - /> - - ); -}); - -Timelines.displayName = 'Timelines'; diff --git a/x-pack/plugins/siem/public/plugin.tsx b/x-pack/plugins/siem/public/plugin.tsx index f4310e1b073ab3..cc46025ddc4a6a 100644 --- a/x-pack/plugins/siem/public/plugin.tsx +++ b/x-pack/plugins/siem/public/plugin.tsx @@ -30,9 +30,9 @@ import { } from '../../triggers_actions_ui/public'; import { SecurityPluginSetup } from '../../security/public'; import { APP_ID, APP_NAME, APP_PATH, APP_ICON } from '../common/constants'; -import { initTelemetry } from './lib/telemetry'; -import { KibanaServices } from './lib/kibana/services'; -import { serviceNowActionType, jiraActionType } from './lib/connectors'; +import { initTelemetry } from './common/lib/telemetry'; +import { KibanaServices } from './common/lib/kibana/services'; +import { serviceNowActionType, jiraActionType } from './common/lib/connectors'; export interface SetupPlugins { home: HomePublicPluginSetup; @@ -87,6 +87,53 @@ export class Plugin implements IPlugin { + const [coreStart, startPlugins] = await core.getStartServices(); + const { renderApp } = await import('./app'); + const services = { + ...coreStart, + ...startPlugins, + security: plugins.security, + } as StartServices; + + const alertsSubPlugin = new (await import('./alerts')).Alerts(); + const casesSubPlugin = new (await import('./cases')).Cases(); + const hostsSubPlugin = new (await import('./hosts')).Hosts(); + const networkSubPlugin = new (await import('./network')).Network(); + const overviewSubPlugin = new (await import('./overview')).Overview(); + const timelinesSubPlugin = new (await import('./timelines')).Timelines(); + + const alertsStart = alertsSubPlugin.start(); + const casesStart = casesSubPlugin.start(); + const hostsStart = hostsSubPlugin.start(); + const networkStart = networkSubPlugin.start(); + const overviewStart = overviewSubPlugin.start(); + const timelinesStart = timelinesSubPlugin.start(); + + return renderApp(services, params, { + routes: [ + ...alertsStart.routes, + ...casesStart.routes, + ...hostsStart.routes, + ...networkStart.routes, + ...overviewStart.routes, + ...timelinesStart.routes, + ], + store: { + initialState: { + ...hostsStart.store.initialState, + ...networkStart.store.initialState, + ...timelinesStart.store.initialState, + }, + reducer: { + ...hostsStart.store.reducer, + ...networkStart.store.reducer, + ...timelinesStart.store.reducer, + }, + }, + }); + }; + core.application.register({ id: APP_ID, title: APP_NAME, @@ -94,15 +141,7 @@ export class Plugin implements IPlugin = ({ history }) => ( - - - - - - - - - - - - -); - -export const PageRouter = memo(PageRouterComponent); diff --git a/x-pack/plugins/siem/public/store/actions.ts b/x-pack/plugins/siem/public/store/actions.ts deleted file mode 100644 index 12da695d2966d9..00000000000000 --- a/x-pack/plugins/siem/public/store/actions.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. - */ - -export { appActions } from './app'; -export { dragAndDropActions } from './drag_and_drop'; -export { hostsActions } from './hosts'; -export { inputsActions } from './inputs'; -export { networkActions } from './network'; -export { timelineActions } from './timeline'; diff --git a/x-pack/plugins/siem/public/store/drag_and_drop/actions.ts b/x-pack/plugins/siem/public/store/drag_and_drop/actions.ts deleted file mode 100644 index 5d3cdc5a126f92..00000000000000 --- a/x-pack/plugins/siem/public/store/drag_and_drop/actions.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import actionCreatorFactory from 'typescript-fsa'; - -import { DataProvider } from '../../components/timeline/data_providers/data_provider'; - -const actionCreator = actionCreatorFactory('x-pack/siem/local/drag_and_drop'); - -export const registerProvider = actionCreator<{ provider: DataProvider }>('REGISTER_PROVIDER'); - -export const unRegisterProvider = actionCreator<{ id: string }>('UNREGISTER_PROVIDER'); - -export const noProviderFound = actionCreator<{ id: string }>('NO_PROVIDER_FOUND'); diff --git a/x-pack/plugins/siem/public/store/drag_and_drop/model.ts b/x-pack/plugins/siem/public/store/drag_and_drop/model.ts deleted file mode 100644 index 6b6491b32a1d0e..00000000000000 --- a/x-pack/plugins/siem/public/store/drag_and_drop/model.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. - */ -import { DataProvider } from '../../components/timeline/data_providers/data_provider'; - -export interface IdToDataProvider { - [id: string]: DataProvider; -} - -export interface DragAndDropModel { - dataProviders: IdToDataProvider; -} diff --git a/x-pack/plugins/siem/public/store/drag_and_drop/reducer.test.ts b/x-pack/plugins/siem/public/store/drag_and_drop/reducer.test.ts deleted file mode 100644 index e779b990b590e5..00000000000000 --- a/x-pack/plugins/siem/public/store/drag_and_drop/reducer.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { DataProvider } from '../../components/timeline/data_providers/data_provider'; -import { mockDataProviders } from '../../components/timeline/data_providers/mock/mock_data_providers'; - -import { IdToDataProvider } from './model'; -import { registerProviderHandler, unRegisterProviderHandler } from './reducer'; - -const dataProviders: IdToDataProvider = mockDataProviders.reduce( - (acc, provider) => ({ - ...acc, - [provider.id]: provider, - }), - {} -); - -describe('reducer', () => { - describe('#registerProviderHandler', () => { - test('it registers the data provider', () => { - const provider: DataProvider = { - ...mockDataProviders[0], - id: 'abcd', - name: 'Provider abcd', - }; - - expect(registerProviderHandler({ provider, dataProviders })).toEqual({ - ...dataProviders, - [provider.id]: provider, - }); - }); - }); - - describe('#unRegisterProviderHandler', () => { - test('it un-registers the data provider', () => { - const id = mockDataProviders[0].id; - - const expected = unRegisterProviderHandler({ id, dataProviders }); - - expect(Object.keys(expected)).not.toContain(id); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/store/drag_and_drop/reducer.ts b/x-pack/plugins/siem/public/store/drag_and_drop/reducer.ts deleted file mode 100644 index d5d49f3a0a1b1f..00000000000000 --- a/x-pack/plugins/siem/public/store/drag_and_drop/reducer.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { omit } from 'lodash/fp'; -import { reducerWithInitialState } from 'typescript-fsa-reducers'; - -import { DataProvider } from '../../components/timeline/data_providers/data_provider'; - -import { registerProvider, unRegisterProvider } from './actions'; -import { DragAndDropModel, IdToDataProvider } from './model'; - -export type DragAndDropState = DragAndDropModel; - -export const initialDragAndDropState: DragAndDropState = { dataProviders: {} }; - -interface RegisterProviderHandlerParams { - provider: DataProvider; - dataProviders: IdToDataProvider; -} - -export const registerProviderHandler = ({ - provider, - dataProviders, -}: RegisterProviderHandlerParams): IdToDataProvider => ({ - ...dataProviders, - [provider.id]: provider, -}); - -interface UnRegisterProviderHandlerParams { - id: string; - dataProviders: IdToDataProvider; -} - -export const unRegisterProviderHandler = ({ - id, - dataProviders, -}: UnRegisterProviderHandlerParams): IdToDataProvider => omit(id, dataProviders); - -export const dragAndDropReducer = reducerWithInitialState(initialDragAndDropState) - .case(registerProvider, (state, { provider }) => ({ - ...state, - dataProviders: registerProviderHandler({ provider, dataProviders: state.dataProviders }), - })) - .case(unRegisterProvider, (state, { id }) => ({ - ...state, - dataProviders: unRegisterProviderHandler({ id, dataProviders: state.dataProviders }), - })) - .build(); diff --git a/x-pack/plugins/siem/public/store/epic.ts b/x-pack/plugins/siem/public/store/epic.ts deleted file mode 100644 index 336960588f48c6..00000000000000 --- a/x-pack/plugins/siem/public/store/epic.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. - */ - -import { combineEpics } from 'redux-observable'; -import { createTimelineEpic } from './timeline/epic'; -import { createTimelineFavoriteEpic } from './timeline/epic_favorite'; -import { createTimelineNoteEpic } from './timeline/epic_note'; -import { createTimelinePinnedEventEpic } from './timeline/epic_pinned_event'; - -export const createRootEpic = () => - combineEpics( - createTimelineEpic(), - createTimelineFavoriteEpic(), - createTimelineNoteEpic(), - createTimelinePinnedEventEpic() - ); diff --git a/x-pack/plugins/siem/public/store/hosts/helpers.test.ts b/x-pack/plugins/siem/public/store/hosts/helpers.test.ts deleted file mode 100644 index a4eddb31b3e31e..00000000000000 --- a/x-pack/plugins/siem/public/store/hosts/helpers.test.ts +++ /dev/null @@ -1,127 +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 { Direction, HostsFields } from '../../graphql/types'; -import { DEFAULT_TABLE_LIMIT } from '../constants'; -import { HostsModel, HostsTableType, HostsType } from './model'; -import { setHostsQueriesActivePageToZero } from './helpers'; - -export const mockHostsState: HostsModel = { - page: { - queries: { - [HostsTableType.authentications]: { - activePage: 5, - limit: DEFAULT_TABLE_LIMIT, - }, - [HostsTableType.hosts]: { - activePage: 9, - direction: Direction.desc, - limit: DEFAULT_TABLE_LIMIT, - sortField: HostsFields.lastSeen, - }, - [HostsTableType.events]: { - activePage: 4, - limit: DEFAULT_TABLE_LIMIT, - }, - [HostsTableType.uncommonProcesses]: { - activePage: 8, - limit: DEFAULT_TABLE_LIMIT, - }, - [HostsTableType.anomalies]: null, - [HostsTableType.alerts]: { - activePage: 4, - limit: DEFAULT_TABLE_LIMIT, - }, - }, - }, - details: { - queries: { - [HostsTableType.authentications]: { - activePage: 5, - limit: DEFAULT_TABLE_LIMIT, - }, - [HostsTableType.hosts]: { - activePage: 9, - direction: Direction.desc, - limit: DEFAULT_TABLE_LIMIT, - sortField: HostsFields.lastSeen, - }, - [HostsTableType.events]: { - activePage: 4, - limit: DEFAULT_TABLE_LIMIT, - }, - [HostsTableType.uncommonProcesses]: { - activePage: 8, - limit: DEFAULT_TABLE_LIMIT, - }, - [HostsTableType.anomalies]: null, - [HostsTableType.alerts]: { - activePage: 4, - limit: DEFAULT_TABLE_LIMIT, - }, - }, - }, -}; - -describe('Hosts redux store', () => { - describe('#setHostsQueriesActivePageToZero', () => { - test('set activePage to zero for all queries in hosts page ', () => { - expect(setHostsQueriesActivePageToZero(mockHostsState, HostsType.page)).toEqual({ - allHosts: { - activePage: 0, - direction: 'desc', - limit: 10, - sortField: 'lastSeen', - }, - anomalies: null, - authentications: { - activePage: 0, - limit: 10, - }, - events: { - activePage: 0, - limit: 10, - }, - uncommonProcesses: { - activePage: 0, - limit: 10, - }, - alerts: { - activePage: 0, - limit: 10, - }, - }); - }); - - test('set activePage to zero for all queries in host details ', () => { - expect(setHostsQueriesActivePageToZero(mockHostsState, HostsType.details)).toEqual({ - allHosts: { - activePage: 0, - direction: 'desc', - limit: 10, - sortField: 'lastSeen', - }, - anomalies: null, - authentications: { - activePage: 0, - limit: 10, - }, - events: { - activePage: 0, - limit: 10, - }, - uncommonProcesses: { - activePage: 0, - limit: 10, - }, - alerts: { - activePage: 0, - limit: 10, - }, - }); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/store/hosts/helpers.ts b/x-pack/plugins/siem/public/store/hosts/helpers.ts deleted file mode 100644 index f6b5596b382f63..00000000000000 --- a/x-pack/plugins/siem/public/store/hosts/helpers.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { DEFAULT_TABLE_ACTIVE_PAGE } from '../constants'; - -import { HostsModel, HostsTableType, Queries, HostsType } from './model'; - -export const setHostPageQueriesActivePageToZero = (state: HostsModel): Queries => ({ - ...state.page.queries, - [HostsTableType.authentications]: { - ...state.page.queries[HostsTableType.authentications], - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - }, - [HostsTableType.hosts]: { - ...state.page.queries[HostsTableType.hosts], - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - }, - [HostsTableType.events]: { - ...state.page.queries[HostsTableType.events], - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - }, - [HostsTableType.uncommonProcesses]: { - ...state.page.queries[HostsTableType.uncommonProcesses], - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - }, - [HostsTableType.alerts]: { - ...state.page.queries[HostsTableType.alerts], - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - }, -}); - -export const setHostDetailsQueriesActivePageToZero = (state: HostsModel): Queries => ({ - ...state.details.queries, - [HostsTableType.authentications]: { - ...state.details.queries[HostsTableType.authentications], - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - }, - [HostsTableType.hosts]: { - ...state.details.queries[HostsTableType.hosts], - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - }, - [HostsTableType.events]: { - ...state.details.queries[HostsTableType.events], - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - }, - [HostsTableType.uncommonProcesses]: { - ...state.details.queries[HostsTableType.uncommonProcesses], - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - }, - [HostsTableType.alerts]: { - ...state.page.queries[HostsTableType.alerts], - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - }, -}); - -export const setHostsQueriesActivePageToZero = (state: HostsModel, type: HostsType): Queries => { - if (type === HostsType.page) { - return setHostPageQueriesActivePageToZero(state); - } else if (type === HostsType.details) { - return setHostDetailsQueriesActivePageToZero(state); - } - throw new Error(`HostsType ${type} is unknown`); -}; diff --git a/x-pack/plugins/siem/public/store/hosts/index.ts b/x-pack/plugins/siem/public/store/hosts/index.ts deleted file mode 100644 index 93bdde791a7acf..00000000000000 --- a/x-pack/plugins/siem/public/store/hosts/index.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 * as hostsActions from './actions'; -import * as hostsModel from './model'; -import * as hostsSelectors from './selectors'; - -export { hostsActions, hostsModel, hostsSelectors }; -export * from './reducer'; diff --git a/x-pack/plugins/siem/public/store/hosts/reducer.ts b/x-pack/plugins/siem/public/store/hosts/reducer.ts deleted file mode 100644 index 53fe9a3ea6a2c2..00000000000000 --- a/x-pack/plugins/siem/public/store/hosts/reducer.ts +++ /dev/null @@ -1,143 +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 { reducerWithInitialState } from 'typescript-fsa-reducers'; - -import { Direction, HostsFields } from '../../graphql/types'; -import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from '../constants'; - -import { - setHostDetailsTablesActivePageToZero, - setHostTablesActivePageToZero, - updateHostsSort, - updateTableActivePage, - updateTableLimit, -} from './actions'; -import { - setHostPageQueriesActivePageToZero, - setHostDetailsQueriesActivePageToZero, -} from './helpers'; -import { HostsModel, HostsTableType } from './model'; - -export type HostsState = HostsModel; - -export const initialHostsState: HostsState = { - page: { - queries: { - [HostsTableType.authentications]: { - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - limit: DEFAULT_TABLE_LIMIT, - }, - [HostsTableType.hosts]: { - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - direction: Direction.desc, - limit: DEFAULT_TABLE_LIMIT, - sortField: HostsFields.lastSeen, - }, - [HostsTableType.events]: { - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - limit: DEFAULT_TABLE_LIMIT, - }, - [HostsTableType.uncommonProcesses]: { - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - limit: DEFAULT_TABLE_LIMIT, - }, - [HostsTableType.anomalies]: null, - [HostsTableType.alerts]: { - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - limit: DEFAULT_TABLE_LIMIT, - }, - }, - }, - details: { - queries: { - [HostsTableType.authentications]: { - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - limit: DEFAULT_TABLE_LIMIT, - }, - [HostsTableType.hosts]: { - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - direction: Direction.desc, - limit: DEFAULT_TABLE_LIMIT, - sortField: HostsFields.lastSeen, - }, - [HostsTableType.events]: { - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - limit: DEFAULT_TABLE_LIMIT, - }, - [HostsTableType.uncommonProcesses]: { - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - limit: DEFAULT_TABLE_LIMIT, - }, - [HostsTableType.anomalies]: null, - [HostsTableType.alerts]: { - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - limit: DEFAULT_TABLE_LIMIT, - }, - }, - }, -}; - -export const hostsReducer = reducerWithInitialState(initialHostsState) - .case(setHostTablesActivePageToZero, state => ({ - ...state, - page: { - ...state.page, - queries: setHostPageQueriesActivePageToZero(state), - }, - details: { - ...state.details, - queries: setHostDetailsQueriesActivePageToZero(state), - }, - })) - .case(setHostDetailsTablesActivePageToZero, state => ({ - ...state, - details: { - ...state.details, - queries: setHostDetailsQueriesActivePageToZero(state), - }, - })) - .case(updateTableActivePage, (state, { activePage, hostsType, tableType }) => ({ - ...state, - [hostsType]: { - ...state[hostsType], - queries: { - ...state[hostsType].queries, - [tableType]: { - ...state[hostsType].queries[tableType], - activePage, - }, - }, - }, - })) - .case(updateTableLimit, (state, { limit, hostsType, tableType }) => ({ - ...state, - [hostsType]: { - ...state[hostsType], - queries: { - ...state[hostsType].queries, - [tableType]: { - ...state[hostsType].queries[tableType], - limit, - }, - }, - }, - })) - .case(updateHostsSort, (state, { sort, hostsType }) => ({ - ...state, - [hostsType]: { - ...state[hostsType], - queries: { - ...state[hostsType].queries, - [HostsTableType.hosts]: { - ...state[hostsType].queries[HostsTableType.hosts], - direction: sort.direction, - sortField: sort.field, - }, - }, - }, - })) - .build(); diff --git a/x-pack/plugins/siem/public/store/hosts/selectors.ts b/x-pack/plugins/siem/public/store/hosts/selectors.ts deleted file mode 100644 index e50968db31f604..00000000000000 --- a/x-pack/plugins/siem/public/store/hosts/selectors.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 { get } from 'lodash/fp'; -import { createSelector } from 'reselect'; - -import { State } from '../reducer'; - -import { GenericHostsModel, HostsType, HostsTableType } from './model'; - -const selectHosts = (state: State, hostsType: HostsType): GenericHostsModel => - get(hostsType, state.hosts); - -export const authenticationsSelector = () => - createSelector(selectHosts, hosts => hosts.queries.authentications); - -export const hostsSelector = () => - createSelector(selectHosts, hosts => hosts.queries[HostsTableType.hosts]); - -export const eventsSelector = () => createSelector(selectHosts, hosts => hosts.queries.events); - -export const uncommonProcessesSelector = () => - createSelector(selectHosts, hosts => hosts.queries.uncommonProcesses); - -export const alertsSelector = () => - createSelector(selectHosts, hosts => hosts.queries[HostsTableType.alerts]); diff --git a/x-pack/plugins/siem/public/store/inputs/actions.ts b/x-pack/plugins/siem/public/store/inputs/actions.ts deleted file mode 100644 index 04cdf5246de2ce..00000000000000 --- a/x-pack/plugins/siem/public/store/inputs/actions.ts +++ /dev/null @@ -1,86 +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 actionCreatorFactory from 'typescript-fsa'; - -import { InspectQuery, Refetch, RefetchKql } from './model'; -import { InputsModelId } from './constants'; -import { Filter, SavedQuery } from '../../../../../../src/plugins/data/public'; - -const actionCreator = actionCreatorFactory('x-pack/siem/local/inputs'); - -export const setAbsoluteRangeDatePicker = actionCreator<{ - id: InputsModelId; - from: number; - to: number; -}>('SET_ABSOLUTE_RANGE_DATE_PICKER'); - -export const setTimelineRangeDatePicker = actionCreator<{ - from: number; - to: number; -}>('SET_TIMELINE_RANGE_DATE_PICKER'); - -export const setRelativeRangeDatePicker = actionCreator<{ - id: InputsModelId; - fromStr: string; - toStr: string; - from: number; - to: number; -}>('SET_RELATIVE_RANGE_DATE_PICKER'); - -export const setDuration = actionCreator<{ id: InputsModelId; duration: number }>('SET_DURATION'); - -export const startAutoReload = actionCreator<{ id: InputsModelId }>('START_KQL_AUTO_RELOAD'); - -export const stopAutoReload = actionCreator<{ id: InputsModelId }>('STOP_KQL_AUTO_RELOAD'); - -export const setQuery = actionCreator<{ - inputId: InputsModelId; - id: string; - loading: boolean; - refetch: Refetch | RefetchKql; - inspect: InspectQuery | null; -}>('SET_QUERY'); - -export const deleteOneQuery = actionCreator<{ - inputId: InputsModelId; - id: string; -}>('DELETE_QUERY'); - -export const setInspectionParameter = actionCreator<{ - id: string; - inputId: InputsModelId; - isInspected: boolean; - selectedInspectIndex: number; -}>('SET_INSPECTION_PARAMETER'); - -export const deleteAllQuery = actionCreator<{ id: InputsModelId }>('DELETE_ALL_QUERY'); - -export const toggleTimelineLinkTo = actionCreator<{ linkToId: InputsModelId }>( - 'TOGGLE_TIMELINE_LINK_TO' -); - -export const removeTimelineLinkTo = actionCreator('REMOVE_TIMELINE_LINK_TO'); -export const addTimelineLinkTo = actionCreator<{ linkToId: InputsModelId }>('ADD_TIMELINE_LINK_TO'); - -export const removeGlobalLinkTo = actionCreator('REMOVE_GLOBAL_LINK_TO'); -export const addGlobalLinkTo = actionCreator<{ linkToId: InputsModelId }>('ADD_GLOBAL_LINK_TO'); - -export const setFilterQuery = actionCreator<{ - id: InputsModelId; - query: string | { [key: string]: unknown }; - language: string; -}>('SET_FILTER_QUERY'); - -export const setSavedQuery = actionCreator<{ - id: InputsModelId; - savedQuery: SavedQuery | undefined; -}>('SET_SAVED_QUERY'); - -export const setSearchBarFilter = actionCreator<{ - id: InputsModelId; - filters: Filter[]; -}>('SET_SEARCH_BAR_FILTER'); diff --git a/x-pack/plugins/siem/public/store/inputs/model.ts b/x-pack/plugins/siem/public/store/inputs/model.ts deleted file mode 100644 index 3e6be6ce859e59..00000000000000 --- a/x-pack/plugins/siem/public/store/inputs/model.ts +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Dispatch } from 'redux'; -import { InputsModelId } from './constants'; -import { CONSTANTS } from '../../components/url_state/constants'; -import { Query, Filter, SavedQuery } from '../../../../../../src/plugins/data/public'; - -export interface AbsoluteTimeRange { - kind: 'absolute'; - fromStr: undefined; - toStr: undefined; - from: number; - to: number; -} - -export interface RelativeTimeRange { - kind: 'relative'; - fromStr: string; - toStr: string; - from: number; - to: number; -} - -export const isRelativeTimeRange = ( - timeRange: RelativeTimeRange | AbsoluteTimeRange | URLTimeRange -): timeRange is RelativeTimeRange => timeRange.kind === 'relative'; - -export const isAbsoluteTimeRange = ( - timeRange: RelativeTimeRange | AbsoluteTimeRange | URLTimeRange -): timeRange is AbsoluteTimeRange => timeRange.kind === 'absolute'; - -export type TimeRange = AbsoluteTimeRange | RelativeTimeRange; - -export type URLTimeRange = Omit & { - from: string | TimeRange['from']; - to: string | TimeRange['to']; -}; - -export interface Policy { - kind: 'manual' | 'interval'; - duration: number; // in ms -} - -interface InspectVariables { - inspect: boolean; -} -export type RefetchWithParams = ({ inspect }: InspectVariables) => void; -export type RefetchKql = (dispatch: Dispatch) => boolean; -export type Refetch = () => void; - -export interface InspectQuery { - dsl: string[]; - response: string[]; -} - -export interface GlobalGenericQuery { - inspect: InspectQuery | null; - isInspected: boolean; - loading: boolean; - selectedInspectIndex: number; -} - -export interface GlobalGraphqlQuery extends GlobalGenericQuery { - id: string; - refetch: null | Refetch | RefetchWithParams; -} -export interface GlobalKqlQuery extends GlobalGenericQuery { - id: 'kql'; - refetch: RefetchKql; -} - -export type GlobalQuery = GlobalGraphqlQuery | GlobalKqlQuery; - -export interface InputsRange { - timerange: TimeRange; - policy: Policy; - queries: GlobalQuery[]; - linkTo: InputsModelId[]; - query: Query; - filters: Filter[]; - savedQuery?: SavedQuery; -} - -export interface LinkTo { - linkTo: InputsModelId[]; -} - -export interface InputsModel { - global: InputsRange; - timeline: InputsRange; -} -export interface UrlInputsModelInputs { - linkTo: InputsModelId[]; - [CONSTANTS.timerange]: TimeRange; -} -export interface UrlInputsModel { - global: UrlInputsModelInputs; - timeline: UrlInputsModelInputs; -} diff --git a/x-pack/plugins/siem/public/store/model.ts b/x-pack/plugins/siem/public/store/model.ts deleted file mode 100644 index 686dc096e61b01..00000000000000 --- a/x-pack/plugins/siem/public/store/model.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. - */ - -export { appModel } from './app'; -export { dragAndDropModel } from './drag_and_drop'; -export { hostsModel } from './hosts'; -export { inputsModel } from './inputs'; -export { networkModel } from './network'; -export * from './types'; diff --git a/x-pack/plugins/siem/public/store/network/actions.ts b/x-pack/plugins/siem/public/store/network/actions.ts deleted file mode 100644 index be7d9b1ad4518c..00000000000000 --- a/x-pack/plugins/siem/public/store/network/actions.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import actionCreatorFactory from 'typescript-fsa'; - -import { networkModel } from '../model'; - -const actionCreator = actionCreatorFactory('x-pack/siem/local/network'); - -export const updateNetworkTable = actionCreator<{ - networkType: networkModel.NetworkType; - tableType: networkModel.NetworkTableType | networkModel.IpDetailsTableType; - updates: networkModel.TableUpdates; -}>('UPDATE_NETWORK_TABLE'); - -export const setIpDetailsTablesActivePageToZero = actionCreator( - 'SET_IP_DETAILS_TABLES_ACTIVE_PAGE_TO_ZERO' -); - -export const setNetworkTablesActivePageToZero = actionCreator( - 'SET_NETWORK_TABLES_ACTIVE_PAGE_TO_ZERO' -); diff --git a/x-pack/plugins/siem/public/store/network/helpers.test.ts b/x-pack/plugins/siem/public/store/network/helpers.test.ts deleted file mode 100644 index 933c2f05a57ba6..00000000000000 --- a/x-pack/plugins/siem/public/store/network/helpers.test.ts +++ /dev/null @@ -1,248 +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 { - Direction, - FlowTarget, - NetworkDnsFields, - NetworkTopTablesFields, - TlsFields, - UsersFields, -} from '../../graphql/types'; -import { DEFAULT_TABLE_LIMIT } from '../constants'; -import { NetworkModel, NetworkTableType, IpDetailsTableType, NetworkType } from './model'; -import { setNetworkQueriesActivePageToZero } from './helpers'; - -export const mockNetworkState: NetworkModel = { - page: { - queries: { - [NetworkTableType.topCountriesSource]: { - activePage: 7, - limit: DEFAULT_TABLE_LIMIT, - sort: { - field: NetworkTopTablesFields.bytes_out, - direction: Direction.desc, - }, - }, - [NetworkTableType.topCountriesDestination]: { - activePage: 3, - limit: DEFAULT_TABLE_LIMIT, - sort: { - field: NetworkTopTablesFields.bytes_out, - direction: Direction.desc, - }, - }, - [NetworkTableType.topNFlowSource]: { - activePage: 7, - limit: DEFAULT_TABLE_LIMIT, - sort: { - field: NetworkTopTablesFields.bytes_out, - direction: Direction.desc, - }, - }, - [NetworkTableType.topNFlowDestination]: { - activePage: 3, - limit: DEFAULT_TABLE_LIMIT, - sort: { - field: NetworkTopTablesFields.bytes_out, - direction: Direction.desc, - }, - }, - [NetworkTableType.dns]: { - activePage: 5, - limit: DEFAULT_TABLE_LIMIT, - sort: { - field: NetworkDnsFields.uniqueDomains, - direction: Direction.desc, - }, - isPtrIncluded: false, - }, - [NetworkTableType.tls]: { - activePage: 2, - limit: DEFAULT_TABLE_LIMIT, - sort: { - field: TlsFields._id, - direction: Direction.desc, - }, - }, - [NetworkTableType.http]: { - activePage: 0, - limit: DEFAULT_TABLE_LIMIT, - sort: { direction: Direction.desc }, - }, - [NetworkTableType.alerts]: { - activePage: 0, - limit: DEFAULT_TABLE_LIMIT, - }, - }, - }, - details: { - queries: { - [IpDetailsTableType.topCountriesSource]: { - activePage: 7, - limit: DEFAULT_TABLE_LIMIT, - sort: { - field: NetworkTopTablesFields.bytes_out, - direction: Direction.desc, - }, - }, - [IpDetailsTableType.topCountriesDestination]: { - activePage: 3, - limit: DEFAULT_TABLE_LIMIT, - sort: { - field: NetworkTopTablesFields.bytes_out, - direction: Direction.desc, - }, - }, - [IpDetailsTableType.topNFlowSource]: { - activePage: 7, - limit: DEFAULT_TABLE_LIMIT, - sort: { - field: NetworkTopTablesFields.bytes_out, - direction: Direction.desc, - }, - }, - [IpDetailsTableType.topNFlowDestination]: { - activePage: 3, - limit: DEFAULT_TABLE_LIMIT, - sort: { - field: NetworkTopTablesFields.bytes_out, - direction: Direction.desc, - }, - }, - [IpDetailsTableType.tls]: { - activePage: 2, - limit: DEFAULT_TABLE_LIMIT, - sort: { - field: TlsFields._id, - direction: Direction.desc, - }, - }, - [IpDetailsTableType.users]: { - activePage: 6, - limit: DEFAULT_TABLE_LIMIT, - sort: { - field: UsersFields.name, - direction: Direction.asc, - }, - }, - [IpDetailsTableType.http]: { - activePage: 0, - limit: DEFAULT_TABLE_LIMIT, - sort: { direction: Direction.desc }, - }, - }, - flowTarget: FlowTarget.source, - }, -}; - -describe('Network redux store', () => { - describe('#setNetworkQueriesActivePageToZero', () => { - test('set activePage to zero for all queries in network page', () => { - expect(setNetworkQueriesActivePageToZero(mockNetworkState, NetworkType.page)).toEqual({ - [NetworkTableType.topNFlowSource]: { - activePage: 0, - limit: 10, - sort: { field: 'bytes_out', direction: 'desc' }, - }, - [NetworkTableType.topNFlowDestination]: { - activePage: 0, - limit: 10, - sort: { field: 'bytes_out', direction: 'desc' }, - }, - [NetworkTableType.dns]: { - activePage: 0, - limit: 10, - sort: { field: 'uniqueDomains', direction: 'desc' }, - isPtrIncluded: false, - }, - [NetworkTableType.http]: { - activePage: 0, - limit: 10, - sort: { - direction: 'desc', - }, - }, - [NetworkTableType.tls]: { - activePage: 0, - limit: 10, - sort: { - direction: 'desc', - field: '_id', - }, - }, - [NetworkTableType.topCountriesDestination]: { - activePage: 0, - limit: 10, - sort: { - direction: 'desc', - field: 'bytes_out', - }, - }, - [NetworkTableType.topCountriesSource]: { - activePage: 0, - limit: 10, - sort: { - direction: 'desc', - field: 'bytes_out', - }, - }, - [NetworkTableType.alerts]: { - activePage: 0, - limit: 10, - }, - }); - }); - - test('set activePage to zero for all queries in ip details ', () => { - expect(setNetworkQueriesActivePageToZero(mockNetworkState, NetworkType.details)).toEqual({ - [IpDetailsTableType.topNFlowSource]: { - activePage: 0, - limit: 10, - sort: { field: 'bytes_out', direction: 'desc' }, - }, - [IpDetailsTableType.topNFlowDestination]: { - activePage: 0, - limit: 10, - sort: { field: 'bytes_out', direction: 'desc' }, - }, - [IpDetailsTableType.topCountriesDestination]: { - activePage: 0, - limit: 10, - sort: { - direction: 'desc', - field: 'bytes_out', - }, - }, - [IpDetailsTableType.topCountriesSource]: { - activePage: 0, - limit: 10, - sort: { - direction: 'desc', - field: 'bytes_out', - }, - }, - [IpDetailsTableType.http]: { - activePage: 0, - limit: 10, - sort: { - direction: 'desc', - }, - }, - [IpDetailsTableType.tls]: { - activePage: 0, - limit: 10, - sort: { field: '_id', direction: 'desc' }, - }, - [IpDetailsTableType.users]: { - activePage: 0, - limit: 10, - sort: { field: 'name', direction: 'asc' }, - }, - }); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/store/network/helpers.ts b/x-pack/plugins/siem/public/store/network/helpers.ts deleted file mode 100644 index 0b3a5e65346b8f..00000000000000 --- a/x-pack/plugins/siem/public/store/network/helpers.ts +++ /dev/null @@ -1,93 +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 { - NetworkModel, - NetworkType, - NetworkTableType, - IpDetailsTableType, - NetworkQueries, - IpOverviewQueries, -} from './model'; -import { DEFAULT_TABLE_ACTIVE_PAGE } from '../constants'; - -export const setNetworkPageQueriesActivePageToZero = (state: NetworkModel): NetworkQueries => ({ - ...state.page.queries, - [NetworkTableType.topCountriesSource]: { - ...state.page.queries[NetworkTableType.topCountriesSource], - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - }, - [NetworkTableType.topCountriesDestination]: { - ...state.page.queries[NetworkTableType.topCountriesDestination], - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - }, - [NetworkTableType.topNFlowSource]: { - ...state.page.queries[NetworkTableType.topNFlowSource], - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - }, - [NetworkTableType.topNFlowDestination]: { - ...state.page.queries[NetworkTableType.topNFlowDestination], - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - }, - [NetworkTableType.dns]: { - ...state.page.queries[NetworkTableType.dns], - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - }, - [NetworkTableType.tls]: { - ...state.page.queries[NetworkTableType.tls], - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - }, - [NetworkTableType.http]: { - ...state.page.queries[NetworkTableType.http], - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - }, -}); - -export const setNetworkDetailsQueriesActivePageToZero = ( - state: NetworkModel -): IpOverviewQueries => ({ - ...state.details.queries, - [IpDetailsTableType.topCountriesSource]: { - ...state.details.queries[IpDetailsTableType.topCountriesSource], - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - }, - [IpDetailsTableType.topCountriesDestination]: { - ...state.details.queries[IpDetailsTableType.topCountriesDestination], - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - }, - [IpDetailsTableType.topNFlowSource]: { - ...state.details.queries[IpDetailsTableType.topNFlowSource], - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - }, - [IpDetailsTableType.topNFlowDestination]: { - ...state.details.queries[IpDetailsTableType.topNFlowDestination], - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - }, - [IpDetailsTableType.tls]: { - ...state.details.queries[IpDetailsTableType.tls], - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - }, - [IpDetailsTableType.users]: { - ...state.details.queries[IpDetailsTableType.users], - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - }, - [IpDetailsTableType.http]: { - ...state.details.queries[IpDetailsTableType.http], - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - }, -}); - -export const setNetworkQueriesActivePageToZero = ( - state: NetworkModel, - type: NetworkType -): NetworkQueries | IpOverviewQueries => { - if (type === NetworkType.page) { - return setNetworkPageQueriesActivePageToZero(state); - } else if (type === NetworkType.details) { - return setNetworkDetailsQueriesActivePageToZero(state); - } - throw new Error(`NetworkType ${type} is unknown`); -}; diff --git a/x-pack/plugins/siem/public/store/network/index.ts b/x-pack/plugins/siem/public/store/network/index.ts deleted file mode 100644 index dcd32fe17ac97f..00000000000000 --- a/x-pack/plugins/siem/public/store/network/index.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 * as networkActions from './actions'; -import * as networkModel from './model'; -import * as networkSelectors from './selectors'; - -export { networkActions, networkModel, networkSelectors }; -export * from './reducer'; diff --git a/x-pack/plugins/siem/public/store/network/reducer.ts b/x-pack/plugins/siem/public/store/network/reducer.ts deleted file mode 100644 index e6d7efc9cbb5f4..00000000000000 --- a/x-pack/plugins/siem/public/store/network/reducer.ts +++ /dev/null @@ -1,191 +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 { reducerWithInitialState } from 'typescript-fsa-reducers'; -import { get } from 'lodash/fp'; -import { - Direction, - FlowTarget, - NetworkDnsFields, - NetworkTopTablesFields, - TlsFields, - UsersFields, -} from '../../graphql/types'; -import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from '../constants'; - -import { - setIpDetailsTablesActivePageToZero, - setNetworkTablesActivePageToZero, - updateNetworkTable, -} from './actions'; -import { - setNetworkDetailsQueriesActivePageToZero, - setNetworkPageQueriesActivePageToZero, -} from './helpers'; -import { IpDetailsTableType, NetworkModel, NetworkTableType } from './model'; - -export type NetworkState = NetworkModel; - -export const initialNetworkState: NetworkState = { - page: { - queries: { - [NetworkTableType.topNFlowSource]: { - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - limit: DEFAULT_TABLE_LIMIT, - sort: { - field: NetworkTopTablesFields.bytes_out, - direction: Direction.desc, - }, - }, - [NetworkTableType.topNFlowDestination]: { - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - limit: DEFAULT_TABLE_LIMIT, - sort: { - field: NetworkTopTablesFields.bytes_in, - direction: Direction.desc, - }, - }, - [NetworkTableType.dns]: { - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - limit: DEFAULT_TABLE_LIMIT, - sort: { - field: NetworkDnsFields.uniqueDomains, - direction: Direction.desc, - }, - isPtrIncluded: false, - }, - [NetworkTableType.http]: { - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - limit: DEFAULT_TABLE_LIMIT, - sort: { - direction: Direction.desc, - }, - }, - [NetworkTableType.tls]: { - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - limit: DEFAULT_TABLE_LIMIT, - sort: { - field: TlsFields._id, - direction: Direction.desc, - }, - }, - [NetworkTableType.topCountriesSource]: { - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - limit: DEFAULT_TABLE_LIMIT, - sort: { - field: NetworkTopTablesFields.bytes_out, - direction: Direction.desc, - }, - }, - [NetworkTableType.topCountriesDestination]: { - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - limit: DEFAULT_TABLE_LIMIT, - sort: { - field: NetworkTopTablesFields.bytes_in, - direction: Direction.desc, - }, - }, - [NetworkTableType.alerts]: { - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - limit: DEFAULT_TABLE_LIMIT, - }, - }, - }, - details: { - queries: { - [IpDetailsTableType.http]: { - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - limit: DEFAULT_TABLE_LIMIT, - sort: { - direction: Direction.desc, - }, - }, - [IpDetailsTableType.topCountriesSource]: { - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - limit: DEFAULT_TABLE_LIMIT, - sort: { - field: NetworkTopTablesFields.bytes_out, - direction: Direction.desc, - }, - }, - [IpDetailsTableType.topCountriesDestination]: { - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - limit: DEFAULT_TABLE_LIMIT, - sort: { - field: NetworkTopTablesFields.bytes_in, - direction: Direction.desc, - }, - }, - [IpDetailsTableType.topNFlowSource]: { - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - limit: DEFAULT_TABLE_LIMIT, - sort: { - field: NetworkTopTablesFields.bytes_out, - direction: Direction.desc, - }, - }, - [IpDetailsTableType.topNFlowDestination]: { - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - limit: DEFAULT_TABLE_LIMIT, - sort: { - field: NetworkTopTablesFields.bytes_in, - direction: Direction.desc, - }, - }, - [IpDetailsTableType.tls]: { - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - limit: DEFAULT_TABLE_LIMIT, - sort: { - field: TlsFields._id, - direction: Direction.desc, - }, - }, - [IpDetailsTableType.users]: { - activePage: DEFAULT_TABLE_ACTIVE_PAGE, - limit: DEFAULT_TABLE_LIMIT, - sort: { - field: UsersFields.name, - direction: Direction.asc, - }, - }, - }, - flowTarget: FlowTarget.source, - }, -}; - -export const networkReducer = reducerWithInitialState(initialNetworkState) - .case(updateNetworkTable, (state, { networkType, tableType, updates }) => ({ - ...state, - [networkType]: { - ...state[networkType], - queries: { - ...state[networkType].queries, - [tableType]: { - ...get([networkType, 'queries', tableType], state), - ...updates, - }, - }, - }, - })) - .case(setNetworkTablesActivePageToZero, state => ({ - ...state, - page: { - ...state.page, - queries: setNetworkPageQueriesActivePageToZero(state), - }, - details: { - ...state.details, - queries: setNetworkDetailsQueriesActivePageToZero(state), - }, - })) - .case(setIpDetailsTablesActivePageToZero, state => ({ - ...state, - details: { - ...state.details, - queries: setNetworkDetailsQueriesActivePageToZero(state), - }, - })) - .build(); diff --git a/x-pack/plugins/siem/public/store/network/selectors.ts b/x-pack/plugins/siem/public/store/network/selectors.ts deleted file mode 100644 index 273eaf7c0ee7fe..00000000000000 --- a/x-pack/plugins/siem/public/store/network/selectors.ts +++ /dev/null @@ -1,88 +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 { createSelector } from 'reselect'; -import { get } from 'lodash/fp'; - -import { FlowTargetSourceDest } from '../../graphql/types'; -import { State } from '../reducer'; -import { initialNetworkState } from './reducer'; -import { - IpDetailsTableType, - NetworkDetailsModel, - NetworkPageModel, - NetworkTableType, - NetworkType, - TopCountriesQuery, - TlsQuery, - HttpQuery, -} from './model'; - -const selectNetworkPage = (state: State): NetworkPageModel => state.network.page; - -const selectNetworkDetails = (state: State): NetworkDetailsModel => state.network.details; - -// Network Page Selectors -export const dnsSelector = () => createSelector(selectNetworkPage, network => network.queries.dns); - -const selectTopNFlowByType = ( - state: State, - networkType: NetworkType, - flowTarget: FlowTargetSourceDest -) => { - const ft = flowTarget === FlowTargetSourceDest.source ? 'topNFlowSource' : 'topNFlowDestination'; - const nFlowType = - networkType === NetworkType.page ? NetworkTableType[ft] : IpDetailsTableType[ft]; - return ( - get([networkType, 'queries', nFlowType], state.network) || - get([networkType, 'queries', nFlowType], initialNetworkState) - ); -}; - -export const topNFlowSelector = () => - createSelector(selectTopNFlowByType, topNFlowQueries => topNFlowQueries); -const selectTlsByType = (state: State, networkType: NetworkType): TlsQuery => { - const tlsType = networkType === NetworkType.page ? NetworkTableType.tls : IpDetailsTableType.tls; - return ( - get([networkType, 'queries', tlsType], state.network) || - get([networkType, 'queries', tlsType], initialNetworkState) - ); -}; - -export const tlsSelector = () => createSelector(selectTlsByType, tlsQueries => tlsQueries); - -const selectTopCountriesByType = ( - state: State, - networkType: NetworkType, - flowTarget: FlowTargetSourceDest -): TopCountriesQuery => { - const ft = - flowTarget === FlowTargetSourceDest.source ? 'topCountriesSource' : 'topCountriesDestination'; - const nFlowType = - networkType === NetworkType.page ? NetworkTableType[ft] : IpDetailsTableType[ft]; - - return ( - get([networkType, 'queries', nFlowType], state.network) || - get([networkType, 'queries', nFlowType], initialNetworkState) - ); -}; - -export const topCountriesSelector = () => - createSelector(selectTopCountriesByType, topCountriesQueries => topCountriesQueries); - -const selectHttpByType = (state: State, networkType: NetworkType): HttpQuery => { - const httpType = - networkType === NetworkType.page ? NetworkTableType.http : IpDetailsTableType.http; - return ( - get([networkType, 'queries', httpType], state.network) || - get([networkType, 'queries', httpType], initialNetworkState) - ); -}; - -export const httpSelector = () => createSelector(selectHttpByType, httpQueries => httpQueries); - -export const usersSelector = () => - createSelector(selectNetworkDetails, network => network.queries.users); diff --git a/x-pack/plugins/siem/public/store/reducer.ts b/x-pack/plugins/siem/public/store/reducer.ts deleted file mode 100644 index 32554653febd55..00000000000000 --- a/x-pack/plugins/siem/public/store/reducer.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. - */ - -import { combineReducers } from 'redux'; - -import { appReducer, AppState, initialAppState } from './app'; -import { dragAndDropReducer, DragAndDropState, initialDragAndDropState } from './drag_and_drop'; -import { hostsReducer, HostsState, initialHostsState } from './hosts'; -import { createInitialInputsState, initialInputsState, inputsReducer, InputsState } from './inputs'; -import { initialNetworkState, networkReducer, NetworkState } from './network'; -import { initialTimelineState, timelineReducer } from './timeline/reducer'; -import { TimelineState } from './timeline/types'; - -export interface State { - app: AppState; - dragAndDrop: DragAndDropState; - hosts: HostsState; - inputs: InputsState; - network: NetworkState; - timeline: TimelineState; -} - -export const initialState: State = { - app: initialAppState, - dragAndDrop: initialDragAndDropState, - hosts: initialHostsState, - inputs: initialInputsState, - network: initialNetworkState, - timeline: initialTimelineState, -}; - -export const createInitialState = (): State => ({ - ...initialState, - inputs: createInitialInputsState(), -}); - -export const reducer = combineReducers({ - app: appReducer, - dragAndDrop: dragAndDropReducer, - hosts: hostsReducer, - inputs: inputsReducer, - network: networkReducer, - timeline: timelineReducer, -}); diff --git a/x-pack/plugins/siem/public/store/selectors.ts b/x-pack/plugins/siem/public/store/selectors.ts deleted file mode 100644 index b188f95ad27cfa..00000000000000 --- a/x-pack/plugins/siem/public/store/selectors.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. - */ - -export { appSelectors } from './app'; -export { dragAndDropSelectors } from './drag_and_drop'; -export { hostsSelectors } from './hosts'; -export { inputsSelectors } from './inputs'; -export { networkSelectors } from './network'; -export { timelineSelectors } from './timeline'; diff --git a/x-pack/plugins/siem/public/store/timeline/actions.ts b/x-pack/plugins/siem/public/store/timeline/actions.ts deleted file mode 100644 index 12155decf40d44..00000000000000 --- a/x-pack/plugins/siem/public/store/timeline/actions.ts +++ /dev/null @@ -1,249 +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 actionCreatorFactory from 'typescript-fsa'; - -import { Filter } from '../../../../../../src/plugins/data/public'; -import { Sort } from '../../components/timeline/body/sort'; -import { - DataProvider, - QueryOperator, -} from '../../components/timeline/data_providers/data_provider'; -import { KueryFilterQuery, SerializedFilterQuery } from '../types'; - -import { EventType, KqlMode, TimelineModel, ColumnHeaderOptions } from './model'; -import { TimelineNonEcsData } from '../../graphql/types'; - -const actionCreator = actionCreatorFactory('x-pack/siem/local/timeline'); - -export const addHistory = actionCreator<{ id: string; historyId: string }>('ADD_HISTORY'); - -export const addNote = actionCreator<{ id: string; noteId: string }>('ADD_NOTE'); - -export const addNoteToEvent = actionCreator<{ id: string; noteId: string; eventId: string }>( - 'ADD_NOTE_TO_EVENT' -); - -export const upsertColumn = actionCreator<{ - column: ColumnHeaderOptions; - id: string; - index: number; -}>('UPSERT_COLUMN'); - -export const addProvider = actionCreator<{ id: string; provider: DataProvider }>('ADD_PROVIDER'); - -export const applyDeltaToWidth = actionCreator<{ - id: string; - delta: number; - bodyClientWidthPixels: number; - minWidthPixels: number; - maxWidthPercent: number; -}>('APPLY_DELTA_TO_WIDTH'); - -export const applyDeltaToColumnWidth = actionCreator<{ - id: string; - columnId: string; - delta: number; -}>('APPLY_DELTA_TO_COLUMN_WIDTH'); - -export const createTimeline = actionCreator<{ - id: string; - dataProviders?: DataProvider[]; - dateRange?: { - start: number; - end: number; - }; - filters?: Filter[]; - columns: ColumnHeaderOptions[]; - itemsPerPage?: number; - kqlQuery?: { - filterQuery: SerializedFilterQuery | null; - filterQueryDraft: KueryFilterQuery | null; - }; - show?: boolean; - sort?: Sort; - showCheckboxes?: boolean; - showRowRenderers?: boolean; -}>('CREATE_TIMELINE'); - -export const pinEvent = actionCreator<{ id: string; eventId: string }>('PIN_EVENT'); - -export const removeColumn = actionCreator<{ - id: string; - columnId: string; -}>('REMOVE_COLUMN'); - -export const removeProvider = actionCreator<{ - id: string; - providerId: string; - andProviderId?: string; -}>('REMOVE_PROVIDER'); - -export const showTimeline = actionCreator<{ id: string; show: boolean }>('SHOW_TIMELINE'); - -export const unPinEvent = actionCreator<{ id: string; eventId: string }>('UN_PIN_EVENT'); - -export const updateTimeline = actionCreator<{ - id: string; - timeline: TimelineModel; -}>('UPDATE_TIMELINE'); - -export const addTimeline = actionCreator<{ - id: string; - timeline: TimelineModel; -}>('ADD_TIMELINE'); - -export const startTimelineSaving = actionCreator<{ - id: string; -}>('START_TIMELINE_SAVING'); - -export const endTimelineSaving = actionCreator<{ - id: string; -}>('END_TIMELINE_SAVING'); - -export const updateIsLoading = actionCreator<{ - id: string; - isLoading: boolean; -}>('UPDATE_LOADING'); - -export const updateColumns = actionCreator<{ - id: string; - columns: ColumnHeaderOptions[]; -}>('UPDATE_COLUMNS'); - -export const updateDataProviderEnabled = actionCreator<{ - id: string; - enabled: boolean; - providerId: string; - andProviderId?: string; -}>('TOGGLE_PROVIDER_ENABLED'); - -export const updateDataProviderExcluded = actionCreator<{ - id: string; - excluded: boolean; - providerId: string; - andProviderId?: string; -}>('TOGGLE_PROVIDER_EXCLUDED'); - -export const dataProviderEdited = actionCreator<{ - andProviderId?: string; - excluded: boolean; - field: string; - id: string; - operator: QueryOperator; - providerId: string; - value: string | number; -}>('DATA_PROVIDER_EDITED'); - -export const updateDataProviderKqlQuery = actionCreator<{ - id: string; - kqlQuery: string; - providerId: string; -}>('PROVIDER_EDIT_KQL_QUERY'); - -export const updateHighlightedDropAndProviderId = actionCreator<{ - id: string; - providerId: string; -}>('UPDATE_DROP_AND_PROVIDER'); - -export const updateDescription = actionCreator<{ id: string; description: string }>( - 'UPDATE_DESCRIPTION' -); - -export const updateKqlMode = actionCreator<{ id: string; kqlMode: KqlMode }>('UPDATE_KQL_MODE'); - -export const setKqlFilterQueryDraft = actionCreator<{ - id: string; - filterQueryDraft: KueryFilterQuery; -}>('SET_KQL_FILTER_QUERY_DRAFT'); - -export const applyKqlFilterQuery = actionCreator<{ - id: string; - filterQuery: SerializedFilterQuery; -}>('APPLY_KQL_FILTER_QUERY'); - -export const updateIsFavorite = actionCreator<{ id: string; isFavorite: boolean }>( - 'UPDATE_IS_FAVORITE' -); - -export const updateIsLive = actionCreator<{ id: string; isLive: boolean }>('UPDATE_IS_LIVE'); - -export const updateItemsPerPage = actionCreator<{ id: string; itemsPerPage: number }>( - 'UPDATE_ITEMS_PER_PAGE' -); - -export const updateItemsPerPageOptions = actionCreator<{ - id: string; - itemsPerPageOptions: number[]; -}>('UPDATE_ITEMS_PER_PAGE_OPTIONS'); - -export const updateTitle = actionCreator<{ id: string; title: string }>('UPDATE_TITLE'); - -export const updatePageIndex = actionCreator<{ id: string; activePage: number }>( - 'UPDATE_PAGE_INDEX' -); - -export const updateProviders = actionCreator<{ id: string; providers: DataProvider[] }>( - 'UPDATE_PROVIDERS' -); - -export const updateRange = actionCreator<{ id: string; start: number; end: number }>( - 'UPDATE_RANGE' -); - -export const updateSort = actionCreator<{ id: string; sort: Sort }>('UPDATE_SORT'); - -export const updateAutoSaveMsg = actionCreator<{ - timelineId: string | null; - newTimelineModel: TimelineModel | null; -}>('UPDATE_AUTO_SAVE'); - -export const showCallOutUnauthorizedMsg = actionCreator('SHOW_CALL_OUT_UNAUTHORIZED_MSG'); - -export const setSavedQueryId = actionCreator<{ - id: string; - savedQueryId: string | null; -}>('SET_TIMELINE_SAVED_QUERY'); - -export const setFilters = actionCreator<{ - id: string; - filters: Filter[]; -}>('SET_TIMELINE_FILTERS'); - -export const setSelected = actionCreator<{ - id: string; - eventIds: Readonly>; - isSelected: boolean; - isSelectAllChecked: boolean; -}>('SET_TIMELINE_SELECTED'); - -export const clearSelected = actionCreator<{ - id: string; -}>('CLEAR_TIMELINE_SELECTED'); - -export const setEventsLoading = actionCreator<{ - id: string; - eventIds: string[]; - isLoading: boolean; -}>('SET_TIMELINE_EVENTS_LOADING'); - -export const clearEventsLoading = actionCreator<{ - id: string; -}>('CLEAR_TIMELINE_EVENTS_LOADING'); - -export const setEventsDeleted = actionCreator<{ - id: string; - eventIds: string[]; - isDeleted: boolean; -}>('SET_TIMELINE_EVENTS_DELETED'); - -export const clearEventsDeleted = actionCreator<{ - id: string; -}>('CLEAR_TIMELINE_EVENTS_DELETED'); - -export const updateEventType = actionCreator<{ id: string; eventType: EventType }>( - 'UPDATE_EVENT_TYPE' -); diff --git a/x-pack/plugins/siem/public/store/timeline/epic.ts b/x-pack/plugins/siem/public/store/timeline/epic.ts deleted file mode 100644 index a7b8c48b45068e..00000000000000 --- a/x-pack/plugins/siem/public/store/timeline/epic.ts +++ /dev/null @@ -1,394 +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 { - get, - has, - merge as mergeObject, - set, - omit, - isObject, - toString as fpToString, -} from 'lodash/fp'; -import { Action } from 'redux'; -import { Epic } from 'redux-observable'; -import { from, Observable, empty, merge } from 'rxjs'; -import { - filter, - map, - startWith, - withLatestFrom, - debounceTime, - mergeMap, - concatMap, - delay, - takeUntil, -} from 'rxjs/operators'; - -import { esFilters, Filter, MatchAllFilter } from '../../../../../../src/plugins/data/public'; -import { TimelineType } from '../../../common/types/timeline'; -import { TimelineInput, ResponseTimeline, TimelineResult } from '../../graphql/types'; -import { AppApolloClient } from '../../lib/lib'; -import { addError } from '../app/actions'; -import { NotesById } from '../app/model'; -import { inputsModel } from '../inputs'; - -import { - applyKqlFilterQuery, - addProvider, - dataProviderEdited, - removeColumn, - removeProvider, - updateColumns, - updateEventType, - updateDataProviderEnabled, - updateDataProviderExcluded, - updateDataProviderKqlQuery, - updateDescription, - updateKqlMode, - updateProviders, - updateRange, - updateSort, - upsertColumn, - updateTimeline, - updateTitle, - updateAutoSaveMsg, - setFilters, - setSavedQueryId, - startTimelineSaving, - endTimelineSaving, - createTimeline, - addTimeline, - showCallOutUnauthorizedMsg, -} from './actions'; -import { ColumnHeaderOptions, TimelineModel } from './model'; -import { epicPersistNote, timelineNoteActionsType } from './epic_note'; -import { epicPersistPinnedEvent, timelinePinnedEventActionsType } from './epic_pinned_event'; -import { epicPersistTimelineFavorite, timelineFavoriteActionsType } from './epic_favorite'; -import { isNotNull } from './helpers'; -import { dispatcherTimelinePersistQueue } from './epic_dispatcher_timeline_persistence_queue'; -import { myEpicTimelineId } from './my_epic_timeline_id'; -import { ActionTimeline, TimelineById } from './types'; -import { persistTimeline } from '../../containers/timeline/api'; -import { ALL_TIMELINE_QUERY_ID } from '../../containers/timeline/all'; - -interface TimelineEpicDependencies { - timelineByIdSelector: (state: State) => TimelineById; - timelineTimeRangeSelector: (state: State) => inputsModel.TimeRange; - selectAllTimelineQuery: () => (state: State, id: string) => inputsModel.GlobalQuery; - selectNotesByIdSelector: (state: State) => NotesById; - apolloClient$: Observable; -} - -const timelineActionsType = [ - applyKqlFilterQuery.type, - addProvider.type, - dataProviderEdited.type, - removeColumn.type, - removeProvider.type, - setFilters.type, - setSavedQueryId.type, - updateColumns.type, - updateDataProviderEnabled.type, - updateDataProviderExcluded.type, - updateDataProviderKqlQuery.type, - updateDescription.type, - updateEventType.type, - updateKqlMode.type, - updateProviders.type, - updateSort.type, - updateTitle.type, - updateRange.type, - upsertColumn.type, -]; - -const isItAtimelineAction = (timelineId: string | undefined) => - timelineId && timelineId.toLowerCase().startsWith('timeline'); - -export const createTimelineEpic = (): Epic< - Action, - Action, - State, - TimelineEpicDependencies -> => ( - action$, - state$, - { - selectAllTimelineQuery, - selectNotesByIdSelector, - timelineByIdSelector, - timelineTimeRangeSelector, - apolloClient$, - } -) => { - const timeline$ = state$.pipe(map(timelineByIdSelector), filter(isNotNull)); - - const allTimelineQuery$ = state$.pipe( - map(state => { - const getQuery = selectAllTimelineQuery(); - return getQuery(state, ALL_TIMELINE_QUERY_ID); - }), - filter(isNotNull) - ); - - const notes$ = state$.pipe(map(selectNotesByIdSelector), filter(isNotNull)); - - const timelineTimeRange$ = state$.pipe(map(timelineTimeRangeSelector), filter(isNotNull)); - - return merge( - action$.pipe( - withLatestFrom(timeline$), - filter(([action, timeline]) => { - const timelineId: string = get('payload.id', action); - const timelineObj: TimelineModel = timeline[timelineId]; - if (action.type === addError.type) { - return true; - } - if (action.type === createTimeline.type && isItAtimelineAction(timelineId)) { - myEpicTimelineId.setTimelineId(null); - myEpicTimelineId.setTimelineVersion(null); - } else if (action.type === addTimeline.type && isItAtimelineAction(timelineId)) { - const addNewTimeline: TimelineModel = get('payload.timeline', action); - myEpicTimelineId.setTimelineId(addNewTimeline.savedObjectId); - myEpicTimelineId.setTimelineVersion(addNewTimeline.version); - return true; - } else if ( - timelineActionsType.includes(action.type) && - !timelineObj.isLoading && - isItAtimelineAction(timelineId) - ) { - return true; - } - return false; - }), - debounceTime(500), - mergeMap(([action]) => { - dispatcherTimelinePersistQueue.next({ action }); - return empty(); - }) - ), - dispatcherTimelinePersistQueue.pipe( - delay(500), - withLatestFrom(timeline$, apolloClient$, notes$, timelineTimeRange$), - concatMap(([objAction, timeline, apolloClient, notes, timelineTimeRange]) => { - const action: ActionTimeline = get('action', objAction); - const timelineId = myEpicTimelineId.getTimelineId(); - const version = myEpicTimelineId.getTimelineVersion(); - - if (timelineNoteActionsType.includes(action.type)) { - return epicPersistNote( - apolloClient, - action, - timeline, - notes, - action$, - timeline$, - notes$, - allTimelineQuery$ - ); - } else if (timelinePinnedEventActionsType.includes(action.type)) { - return epicPersistPinnedEvent( - apolloClient, - action, - timeline, - action$, - timeline$, - allTimelineQuery$ - ); - } else if (timelineFavoriteActionsType.includes(action.type)) { - return epicPersistTimelineFavorite( - apolloClient, - action, - timeline, - action$, - timeline$, - allTimelineQuery$ - ); - } else if (timelineActionsType.includes(action.type)) { - return from( - persistTimeline({ - timelineId, - version, - timeline: convertTimelineAsInput(timeline[action.payload.id], timelineTimeRange), - }) - ).pipe( - withLatestFrom(timeline$, allTimelineQuery$), - mergeMap(([result, recentTimeline, allTimelineQuery]) => { - const savedTimeline = recentTimeline[action.payload.id]; - const response: ResponseTimeline = get('data.persistTimeline', result); - const callOutMsg = response.code === 403 ? [showCallOutUnauthorizedMsg()] : []; - - if (allTimelineQuery.refetch != null) { - (allTimelineQuery.refetch as inputsModel.Refetch)(); - } - - return [ - response.code === 409 - ? updateAutoSaveMsg({ - timelineId: action.payload.id, - newTimelineModel: omitTypenameInTimeline(savedTimeline, response.timeline), - }) - : updateTimeline({ - id: action.payload.id, - timeline: { - ...savedTimeline, - savedObjectId: response.timeline.savedObjectId, - version: response.timeline.version, - timelineType: response.timeline.timelineType ?? TimelineType.default, - templateTimelineId: response.timeline.templateTimelineId ?? null, - templateTimelineVersion: response.timeline.templateTimelineVersion ?? null, - isSaving: false, - }, - }), - ...callOutMsg, - endTimelineSaving({ - id: action.payload.id, - }), - ]; - }), - startWith(startTimelineSaving({ id: action.payload.id })), - takeUntil( - action$.pipe( - withLatestFrom(timeline$), - filter(([checkAction, updatedTimeline]) => { - if ( - checkAction.type === endTimelineSaving.type && - updatedTimeline[get('payload.id', checkAction)].savedObjectId != null - ) { - myEpicTimelineId.setTimelineId( - updatedTimeline[get('payload.id', checkAction)].savedObjectId - ); - myEpicTimelineId.setTimelineVersion( - updatedTimeline[get('payload.id', checkAction)].version - ); - return true; - } - return false; - }) - ) - ) - ); - } - return empty(); - }) - ) - ); -}; - -const timelineInput: TimelineInput = { - columns: null, - dataProviders: null, - description: null, - eventType: null, - filters: null, - kqlMode: null, - kqlQuery: null, - title: null, - timelineType: TimelineType.default, - templateTimelineVersion: null, - templateTimelineId: null, - dateRange: null, - savedQueryId: null, - sort: null, -}; - -export const convertTimelineAsInput = ( - timeline: TimelineModel, - timelineTimeRange: inputsModel.TimeRange -): TimelineInput => - Object.keys(timelineInput).reduce((acc, key) => { - if (has(key, timeline)) { - if (key === 'kqlQuery') { - return set(`${key}.filterQuery`, get(`${key}.filterQuery`, timeline), acc); - } else if (key === 'dateRange') { - return set(`${key}`, { start: timelineTimeRange.from, end: timelineTimeRange.to }, acc); - } else if (key === 'columns' && get(key, timeline) != null) { - return set( - key, - get(key, timeline).map((col: ColumnHeaderOptions) => omit(['width', '__typename'], col)), - acc - ); - } else if (key === 'filters' && get(key, timeline) != null) { - const filters = get(key, timeline); - return set( - key, - filters != null - ? filters.map((myFilter: Filter) => { - const basicFilter = omit(['$state'], myFilter); - return { - ...basicFilter, - meta: { - ...basicFilter.meta, - field: - (esFilters.isMatchAllFilter(basicFilter) || - esFilters.isPhraseFilter(basicFilter) || - esFilters.isPhrasesFilter(basicFilter) || - esFilters.isRangeFilter(basicFilter)) && - basicFilter.meta.field != null - ? convertToString(basicFilter.meta.field) - : null, - value: - basicFilter.meta.value != null - ? convertToString(basicFilter.meta.value) - : null, - params: - basicFilter.meta.params != null - ? convertToString(basicFilter.meta.params) - : null, - }, - ...(esFilters.isMatchAllFilter(basicFilter) - ? { - match_all: convertToString((basicFilter as MatchAllFilter).match_all), - } - : { match_all: null }), - ...(esFilters.isMissingFilter(basicFilter) && basicFilter.missing != null - ? { missing: convertToString(basicFilter.missing) } - : { missing: null }), - ...(esFilters.isExistsFilter(basicFilter) && basicFilter.exists != null - ? { exists: convertToString(basicFilter.exists) } - : { exists: null }), - ...((esFilters.isQueryStringFilter(basicFilter) || - get('query', basicFilter) != null) && - basicFilter.query != null - ? { query: convertToString(basicFilter.query) } - : { query: null }), - ...(esFilters.isRangeFilter(basicFilter) && basicFilter.range != null - ? { range: convertToString(basicFilter.range) } - : { range: null }), - ...(esFilters.isRangeFilter(basicFilter) && - basicFilter.script != - null /* TODO remove it when PR50713 is merged || esFilters.isPhraseFilter(basicFilter) */ - ? { script: convertToString(basicFilter.script) } - : { script: null }), - }; - }) - : [], - acc - ); - } - return set(key, get(key, timeline), acc); - } - return acc; - }, timelineInput); - -const omitTypename = (key: string, value: keyof TimelineModel) => - key === '__typename' ? undefined : value; - -const omitTypenameInTimeline = ( - oldTimeline: TimelineModel, - newTimeline: TimelineResult -): TimelineModel => JSON.parse(JSON.stringify(mergeObject(oldTimeline, newTimeline)), omitTypename); - -const convertToString = (obj: unknown) => { - try { - if (isObject(obj)) { - return JSON.stringify(obj); - } - return fpToString(obj); - } catch { - return ''; - } -}; diff --git a/x-pack/plugins/siem/public/store/timeline/helpers.ts b/x-pack/plugins/siem/public/store/timeline/helpers.ts deleted file mode 100644 index adab029c11150d..00000000000000 --- a/x-pack/plugins/siem/public/store/timeline/helpers.ts +++ /dev/null @@ -1,1325 +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 { getOr, omit, uniq, isEmpty, isEqualWith, union } from 'lodash/fp'; - -import { Filter } from '../../../../../../src/plugins/data/public'; - -import { getColumnWidthFromType } from '../../components/timeline/body/column_headers/helpers'; -import { Sort } from '../../components/timeline/body/sort'; -import { - DataProvider, - QueryOperator, - QueryMatch, -} from '../../components/timeline/data_providers/data_provider'; -import { KueryFilterQuery, SerializedFilterQuery } from '../model'; - -import { timelineDefaults } from './defaults'; -import { ColumnHeaderOptions, KqlMode, TimelineModel, EventType } from './model'; -import { TimelineById, TimelineState } from './types'; -import { TimelineNonEcsData } from '../../graphql/types'; - -const EMPTY_TIMELINE_BY_ID: TimelineById = {}; // stable reference - -export const isNotNull = (value: T | null): value is T => value !== null; - -export const initialTimelineState: TimelineState = { - timelineById: EMPTY_TIMELINE_BY_ID, - autoSavedWarningMsg: { - timelineId: null, - newTimelineModel: null, - }, - showCallOutUnauthorizedMsg: false, -}; - -interface AddTimelineHistoryParams { - id: string; - historyId: string; - timelineById: TimelineById; -} - -export const addTimelineHistory = ({ - id, - historyId, - timelineById, -}: AddTimelineHistoryParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - historyIds: uniq([...timeline.historyIds, historyId]), - }, - }; -}; - -interface AddTimelineNoteParams { - id: string; - noteId: string; - timelineById: TimelineById; -} - -export const addTimelineNote = ({ - id, - noteId, - timelineById, -}: AddTimelineNoteParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - noteIds: [...timeline.noteIds, noteId], - }, - }; -}; - -interface AddTimelineNoteToEventParams { - id: string; - noteId: string; - eventId: string; - timelineById: TimelineById; -} - -export const addTimelineNoteToEvent = ({ - id, - noteId, - eventId, - timelineById, -}: AddTimelineNoteToEventParams): TimelineById => { - const timeline = timelineById[id]; - const existingNoteIds = getOr([], `eventIdToNoteIds.${eventId}`, timeline); - - return { - ...timelineById, - [id]: { - ...timeline, - eventIdToNoteIds: { - ...timeline.eventIdToNoteIds, - ...{ [eventId]: uniq([...existingNoteIds, noteId]) }, - }, - }, - }; -}; - -interface AddTimelineParams { - id: string; - timeline: TimelineModel; - timelineById: TimelineById; -} - -/** - * Add a saved object timeline to the store - * and default the value to what need to be if values are null - */ -export const addTimelineToStore = ({ - id, - timeline, - timelineById, -}: AddTimelineParams): TimelineById => ({ - ...timelineById, - [id]: { - ...timeline, - isLoading: timelineById[id].isLoading, - }, -}); - -interface AddNewTimelineParams { - columns: ColumnHeaderOptions[]; - dataProviders?: DataProvider[]; - dateRange?: { - start: number; - end: number; - }; - filters?: Filter[]; - id: string; - itemsPerPage?: number; - kqlQuery?: { - filterQuery: SerializedFilterQuery | null; - filterQueryDraft: KueryFilterQuery | null; - }; - show?: boolean; - sort?: Sort; - showCheckboxes?: boolean; - showRowRenderers?: boolean; - timelineById: TimelineById; -} - -/** Adds a new `Timeline` to the provided collection of `TimelineById` */ -export const addNewTimeline = ({ - columns, - dataProviders = [], - dateRange = { start: 0, end: 0 }, - filters = timelineDefaults.filters, - id, - itemsPerPage = timelineDefaults.itemsPerPage, - kqlQuery = { filterQuery: null, filterQueryDraft: null }, - sort = timelineDefaults.sort, - show = false, - showCheckboxes = false, - showRowRenderers = true, - timelineById, -}: AddNewTimelineParams): TimelineById => ({ - ...timelineById, - [id]: { - id, - ...timelineDefaults, - columns, - dataProviders, - dateRange, - filters, - itemsPerPage, - kqlQuery, - sort, - show, - savedObjectId: null, - version: null, - isSaving: false, - isLoading: false, - showCheckboxes, - showRowRenderers, - }, -}); - -interface PinTimelineEventParams { - id: string; - eventId: string; - timelineById: TimelineById; -} - -export const pinTimelineEvent = ({ - id, - eventId, - timelineById, -}: PinTimelineEventParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - pinnedEventIds: { - ...timeline.pinnedEventIds, - ...{ [eventId]: true }, - }, - }, - }; -}; - -interface UpdateShowTimelineProps { - id: string; - show: boolean; - timelineById: TimelineById; -} - -export const updateTimelineShowTimeline = ({ - id, - show, - timelineById, -}: UpdateShowTimelineProps): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - show, - }, - }; -}; - -interface ApplyDeltaToCurrentWidthParams { - id: string; - delta: number; - bodyClientWidthPixels: number; - minWidthPixels: number; - maxWidthPercent: number; - timelineById: TimelineById; -} - -export const applyDeltaToCurrentWidth = ({ - id, - delta, - bodyClientWidthPixels, - minWidthPixels, - maxWidthPercent, - timelineById, -}: ApplyDeltaToCurrentWidthParams): TimelineById => { - const timeline = timelineById[id]; - - const requestedWidth = timeline.width + delta * -1; // raw change in width - const maxWidthPixels = (maxWidthPercent / 100) * bodyClientWidthPixels; - const clampedWidth = Math.min(requestedWidth, maxWidthPixels); - const width = Math.max(minWidthPixels, clampedWidth); // if the clamped width is smaller than the min, use the min - - return { - ...timelineById, - [id]: { - ...timeline, - width, - }, - }; -}; - -const queryMatchCustomizer = (dp1: QueryMatch, dp2: QueryMatch) => { - if (dp1.field === dp2.field && dp1.value === dp2.value && dp1.operator === dp2.operator) { - return true; - } - return false; -}; - -const addAndToProviderInTimeline = ( - id: string, - provider: DataProvider, - timeline: TimelineModel, - timelineById: TimelineById -): TimelineById => { - const alreadyExistsProviderIndex = timeline.dataProviders.findIndex( - p => p.id === timeline.highlightedDropAndProviderId - ); - const newProvider = timeline.dataProviders[alreadyExistsProviderIndex]; - const alreadyExistsAndProviderIndex = newProvider.and.findIndex(p => p.id === provider.id); - const { and, ...andProvider } = provider; - - if ( - isEqualWith(queryMatchCustomizer, newProvider.queryMatch, andProvider.queryMatch) || - (alreadyExistsAndProviderIndex === -1 && - newProvider.and.filter(itemAndProvider => - isEqualWith(queryMatchCustomizer, itemAndProvider.queryMatch, andProvider.queryMatch) - ).length > 0) - ) { - return timelineById; - } - - const dataProviders = [ - ...timeline.dataProviders.slice(0, alreadyExistsProviderIndex), - { - ...timeline.dataProviders[alreadyExistsProviderIndex], - and: - alreadyExistsAndProviderIndex > -1 - ? [ - ...newProvider.and.slice(0, alreadyExistsAndProviderIndex), - andProvider, - ...newProvider.and.slice(alreadyExistsAndProviderIndex + 1), - ] - : [...newProvider.and, andProvider], - }, - ...timeline.dataProviders.slice(alreadyExistsProviderIndex + 1), - ]; - - return { - ...timelineById, - [id]: { - ...timeline, - dataProviders, - }, - }; -}; - -const addProviderToTimeline = ( - id: string, - provider: DataProvider, - timeline: TimelineModel, - timelineById: TimelineById -): TimelineById => { - const alreadyExistsAtIndex = timeline.dataProviders.findIndex(p => p.id === provider.id); - - if (alreadyExistsAtIndex > -1 && !isEmpty(timeline.dataProviders[alreadyExistsAtIndex].and)) { - provider.id = `${provider.id}-${ - timeline.dataProviders.filter(p => p.id === provider.id).length - }`; - } - - const dataProviders = - alreadyExistsAtIndex > -1 && isEmpty(timeline.dataProviders[alreadyExistsAtIndex].and) - ? [ - ...timeline.dataProviders.slice(0, alreadyExistsAtIndex), - provider, - ...timeline.dataProviders.slice(alreadyExistsAtIndex + 1), - ] - : [...timeline.dataProviders, provider]; - - return { - ...timelineById, - [id]: { - ...timeline, - dataProviders, - }, - }; -}; - -interface AddTimelineColumnParams { - column: ColumnHeaderOptions; - id: string; - index: number; - timelineById: TimelineById; -} - -/** - * Adds or updates a column. When updating a column, it will be moved to the - * new index - */ -export const upsertTimelineColumn = ({ - column, - id, - index, - timelineById, -}: AddTimelineColumnParams): TimelineById => { - const timeline = timelineById[id]; - const alreadyExistsAtIndex = timeline.columns.findIndex(c => c.id === column.id); - - if (alreadyExistsAtIndex !== -1) { - // remove the existing entry and add the new one at the specified index - const reordered = timeline.columns.filter(c => c.id !== column.id); - reordered.splice(index, 0, column); // ⚠️ mutation - - return { - ...timelineById, - [id]: { - ...timeline, - columns: reordered, - }, - }; - } - - // add the new entry at the specified index - const columns = [...timeline.columns]; - columns.splice(index, 0, column); // ⚠️ mutation - - return { - ...timelineById, - [id]: { - ...timeline, - columns, - }, - }; -}; - -interface RemoveTimelineColumnParams { - id: string; - columnId: string; - timelineById: TimelineById; -} - -export const removeTimelineColumn = ({ - id, - columnId, - timelineById, -}: RemoveTimelineColumnParams): TimelineById => { - const timeline = timelineById[id]; - - const columns = timeline.columns.filter(c => c.id !== columnId); - - return { - ...timelineById, - [id]: { - ...timeline, - columns, - }, - }; -}; - -interface ApplyDeltaToTimelineColumnWidth { - id: string; - columnId: string; - delta: number; - timelineById: TimelineById; -} - -export const applyDeltaToTimelineColumnWidth = ({ - id, - columnId, - delta, - timelineById, -}: ApplyDeltaToTimelineColumnWidth): TimelineById => { - const timeline = timelineById[id]; - - const columnIndex = timeline.columns.findIndex(c => c.id === columnId); - if (columnIndex === -1) { - // the column was not found - return { - ...timelineById, - [id]: { - ...timeline, - }, - }; - } - const minWidthPixels = getColumnWidthFromType(timeline.columns[columnIndex].type!); - const requestedWidth = timeline.columns[columnIndex].width + delta; // raw change in width - const width = Math.max(minWidthPixels, requestedWidth); // if the requested width is smaller than the min, use the min - - const columnWithNewWidth = { - ...timeline.columns[columnIndex], - width, - }; - - const columns = [ - ...timeline.columns.slice(0, columnIndex), - columnWithNewWidth, - ...timeline.columns.slice(columnIndex + 1), - ]; - - return { - ...timelineById, - [id]: { - ...timeline, - columns, - }, - }; -}; - -interface AddTimelineProviderParams { - id: string; - provider: DataProvider; - timelineById: TimelineById; -} - -export const addTimelineProvider = ({ - id, - provider, - timelineById, -}: AddTimelineProviderParams): TimelineById => { - const timeline = timelineById[id]; - - if (timeline.highlightedDropAndProviderId !== '') { - return addAndToProviderInTimeline(id, provider, timeline, timelineById); - } else { - return addProviderToTimeline(id, provider, timeline, timelineById); - } -}; - -interface ApplyKqlFilterQueryDraftParams { - id: string; - filterQuery: SerializedFilterQuery; - timelineById: TimelineById; -} - -export const applyKqlFilterQueryDraft = ({ - id, - filterQuery, - timelineById, -}: ApplyKqlFilterQueryDraftParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - kqlQuery: { - ...timeline.kqlQuery, - filterQuery, - }, - }, - }; -}; - -interface UpdateTimelineKqlModeParams { - id: string; - kqlMode: KqlMode; - timelineById: TimelineById; -} - -export const updateTimelineKqlMode = ({ - id, - kqlMode, - timelineById, -}: UpdateTimelineKqlModeParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - kqlMode, - }, - }; -}; - -interface UpdateKqlFilterQueryDraftParams { - id: string; - filterQueryDraft: KueryFilterQuery; - timelineById: TimelineById; -} - -export const updateKqlFilterQueryDraft = ({ - id, - filterQueryDraft, - timelineById, -}: UpdateKqlFilterQueryDraftParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - kqlQuery: { - ...timeline.kqlQuery, - filterQueryDraft, - }, - }, - }; -}; - -interface UpdateTimelineColumnsParams { - id: string; - columns: ColumnHeaderOptions[]; - timelineById: TimelineById; -} - -export const updateTimelineColumns = ({ - id, - columns, - timelineById, -}: UpdateTimelineColumnsParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - columns, - }, - }; -}; - -interface UpdateTimelineDescriptionParams { - id: string; - description: string; - timelineById: TimelineById; -} - -export const updateTimelineDescription = ({ - id, - description, - timelineById, -}: UpdateTimelineDescriptionParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - description: description.endsWith(' ') ? `${description.trim()} ` : description.trim(), - }, - }; -}; - -interface UpdateTimelineTitleParams { - id: string; - title: string; - timelineById: TimelineById; -} - -export const updateTimelineTitle = ({ - id, - title, - timelineById, -}: UpdateTimelineTitleParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - title: title.endsWith(' ') ? `${title.trim()} ` : title.trim(), - }, - }; -}; - -interface UpdateTimelineEventTypeParams { - id: string; - eventType: EventType; - timelineById: TimelineById; -} - -export const updateTimelineEventType = ({ - id, - eventType, - timelineById, -}: UpdateTimelineEventTypeParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - eventType, - }, - }; -}; - -interface UpdateTimelineIsFavoriteParams { - id: string; - isFavorite: boolean; - timelineById: TimelineById; -} - -export const updateTimelineIsFavorite = ({ - id, - isFavorite, - timelineById, -}: UpdateTimelineIsFavoriteParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - isFavorite, - }, - }; -}; - -interface UpdateTimelineIsLiveParams { - id: string; - isLive: boolean; - timelineById: TimelineById; -} - -export const updateTimelineIsLive = ({ - id, - isLive, - timelineById, -}: UpdateTimelineIsLiveParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - isLive, - }, - }; -}; - -interface UpdateTimelineProvidersParams { - id: string; - providers: DataProvider[]; - timelineById: TimelineById; -} - -export const updateTimelineProviders = ({ - id, - providers, - timelineById, -}: UpdateTimelineProvidersParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - dataProviders: providers, - }, - }; -}; - -interface UpdateTimelineRangeParams { - id: string; - start: number; - end: number; - timelineById: TimelineById; -} - -export const updateTimelineRange = ({ - id, - start, - end, - timelineById, -}: UpdateTimelineRangeParams): TimelineById => { - const timeline = timelineById[id]; - return { - ...timelineById, - [id]: { - ...timeline, - dateRange: { - start, - end, - }, - }, - }; -}; - -interface UpdateTimelineSortParams { - id: string; - sort: Sort; - timelineById: TimelineById; -} - -export const updateTimelineSort = ({ - id, - sort, - timelineById, -}: UpdateTimelineSortParams): TimelineById => { - const timeline = timelineById[id]; - return { - ...timelineById, - [id]: { - ...timeline, - sort, - }, - }; -}; - -const updateEnabledAndProvider = ( - andProviderId: string, - enabled: boolean, - providerId: string, - timeline: TimelineModel -) => - timeline.dataProviders.map(provider => - provider.id === providerId - ? { - ...provider, - and: provider.and.map(andProvider => - andProvider.id === andProviderId ? { ...andProvider, enabled } : andProvider - ), - } - : provider - ); - -const updateEnabledProvider = (enabled: boolean, providerId: string, timeline: TimelineModel) => - timeline.dataProviders.map(provider => - provider.id === providerId - ? { - ...provider, - enabled, - } - : provider - ); - -interface UpdateTimelineProviderEnabledParams { - id: string; - providerId: string; - enabled: boolean; - timelineById: TimelineById; - andProviderId?: string; -} - -export const updateTimelineProviderEnabled = ({ - id, - providerId, - enabled, - timelineById, - andProviderId, -}: UpdateTimelineProviderEnabledParams): TimelineById => { - const timeline = timelineById[id]; - return { - ...timelineById, - [id]: { - ...timeline, - dataProviders: andProviderId - ? updateEnabledAndProvider(andProviderId, enabled, providerId, timeline) - : updateEnabledProvider(enabled, providerId, timeline), - }, - }; -}; - -const updateExcludedAndProvider = ( - andProviderId: string, - excluded: boolean, - providerId: string, - timeline: TimelineModel -) => - timeline.dataProviders.map(provider => - provider.id === providerId - ? { - ...provider, - and: provider.and.map(andProvider => - andProvider.id === andProviderId ? { ...andProvider, excluded } : andProvider - ), - } - : provider - ); - -const updateExcludedProvider = (excluded: boolean, providerId: string, timeline: TimelineModel) => - timeline.dataProviders.map(provider => - provider.id === providerId - ? { - ...provider, - excluded, - } - : provider - ); - -interface UpdateTimelineProviderExcludedParams { - id: string; - providerId: string; - excluded: boolean; - timelineById: TimelineById; - andProviderId?: string; -} - -export const updateTimelineProviderExcluded = ({ - id, - providerId, - excluded, - timelineById, - andProviderId, -}: UpdateTimelineProviderExcludedParams): TimelineById => { - const timeline = timelineById[id]; - return { - ...timelineById, - [id]: { - ...timeline, - dataProviders: andProviderId - ? updateExcludedAndProvider(andProviderId, excluded, providerId, timeline) - : updateExcludedProvider(excluded, providerId, timeline), - }, - }; -}; - -const updateProviderProperties = ({ - excluded, - field, - operator, - providerId, - timeline, - value, -}: { - excluded: boolean; - field: string; - operator: QueryOperator; - providerId: string; - timeline: TimelineModel; - value: string | number; -}) => - timeline.dataProviders.map(provider => - provider.id === providerId - ? { - ...provider, - excluded, - queryMatch: { - ...provider.queryMatch, - field, - displayField: field, - value, - displayValue: value, - operator, - }, - } - : provider - ); - -const updateAndProviderProperties = ({ - andProviderId, - excluded, - field, - operator, - providerId, - timeline, - value, -}: { - andProviderId: string; - excluded: boolean; - field: string; - operator: QueryOperator; - providerId: string; - timeline: TimelineModel; - value: string | number; -}) => - timeline.dataProviders.map(provider => - provider.id === providerId - ? { - ...provider, - and: provider.and.map(andProvider => - andProvider.id === andProviderId - ? { - ...andProvider, - excluded, - queryMatch: { - ...andProvider.queryMatch, - field, - displayField: field, - value, - displayValue: value, - operator, - }, - } - : andProvider - ), - } - : provider - ); - -interface UpdateTimelineProviderEditPropertiesParams { - andProviderId?: string; - excluded: boolean; - field: string; - id: string; - operator: QueryOperator; - providerId: string; - timelineById: TimelineById; - value: string | number; -} - -export const updateTimelineProviderProperties = ({ - andProviderId, - excluded, - field, - id, - operator, - providerId, - timelineById, - value, -}: UpdateTimelineProviderEditPropertiesParams): TimelineById => { - const timeline = timelineById[id]; - return { - ...timelineById, - [id]: { - ...timeline, - dataProviders: andProviderId - ? updateAndProviderProperties({ - andProviderId, - excluded, - field, - operator, - providerId, - timeline, - value, - }) - : updateProviderProperties({ - excluded, - field, - operator, - providerId, - timeline, - value, - }), - }, - }; -}; - -interface UpdateTimelineProviderKqlQueryParams { - id: string; - providerId: string; - kqlQuery: string; - timelineById: TimelineById; -} - -export const updateTimelineProviderKqlQuery = ({ - id, - providerId, - kqlQuery, - timelineById, -}: UpdateTimelineProviderKqlQueryParams): TimelineById => { - const timeline = timelineById[id]; - return { - ...timelineById, - [id]: { - ...timeline, - dataProviders: timeline.dataProviders.map(provider => - provider.id === providerId ? { ...provider, ...{ kqlQuery } } : provider - ), - }, - }; -}; - -interface UpdateTimelineItemsPerPageParams { - id: string; - itemsPerPage: number; - timelineById: TimelineById; -} - -export const updateTimelineItemsPerPage = ({ - id, - itemsPerPage, - timelineById, -}: UpdateTimelineItemsPerPageParams) => { - const timeline = timelineById[id]; - return { - ...timelineById, - [id]: { - ...timeline, - itemsPerPage, - }, - }; -}; - -interface UpdateTimelinePageIndexParams { - id: string; - activePage: number; - timelineById: TimelineById; -} - -export const updateTimelinePageIndex = ({ - id, - activePage, - timelineById, -}: UpdateTimelinePageIndexParams) => { - const timeline = timelineById[id]; - return { - ...timelineById, - [id]: { - ...timeline, - activePage, - }, - }; -}; - -interface UpdateTimelinePerPageOptionsParams { - id: string; - itemsPerPageOptions: number[]; - timelineById: TimelineById; -} - -export const updateTimelinePerPageOptions = ({ - id, - itemsPerPageOptions, - timelineById, -}: UpdateTimelinePerPageOptionsParams) => { - const timeline = timelineById[id]; - return { - ...timelineById, - [id]: { - ...timeline, - itemsPerPageOptions, - }, - }; -}; - -const removeAndProvider = (andProviderId: string, providerId: string, timeline: TimelineModel) => { - const providerIndex = timeline.dataProviders.findIndex(p => p.id === providerId); - const providerAndIndex = timeline.dataProviders[providerIndex].and.findIndex( - p => p.id === andProviderId - ); - return [ - ...timeline.dataProviders.slice(0, providerIndex), - { - ...timeline.dataProviders[providerIndex], - and: [ - ...timeline.dataProviders[providerIndex].and.slice(0, providerAndIndex), - ...timeline.dataProviders[providerIndex].and.slice(providerAndIndex + 1), - ], - }, - ...timeline.dataProviders.slice(providerIndex + 1), - ]; -}; - -const removeProvider = (providerId: string, timeline: TimelineModel) => { - const providerIndex = timeline.dataProviders.findIndex(p => p.id === providerId); - return [ - ...timeline.dataProviders.slice(0, providerIndex), - ...(timeline.dataProviders[providerIndex].and.length - ? [ - { - ...timeline.dataProviders[providerIndex].and.slice(0, 1)[0], - and: [...timeline.dataProviders[providerIndex].and.slice(1)], - }, - ] - : []), - ...timeline.dataProviders.slice(providerIndex + 1), - ]; -}; - -interface RemoveTimelineProviderParams { - id: string; - providerId: string; - timelineById: TimelineById; - andProviderId?: string; -} - -export const removeTimelineProvider = ({ - id, - providerId, - timelineById, - andProviderId, -}: RemoveTimelineProviderParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - dataProviders: andProviderId - ? removeAndProvider(andProviderId, providerId, timeline) - : removeProvider(providerId, timeline), - }, - }; -}; - -interface SetDeletedTimelineEventsParams { - id: string; - eventIds: string[]; - isDeleted: boolean; - timelineById: TimelineById; -} - -export const setDeletedTimelineEvents = ({ - id, - eventIds, - isDeleted, - timelineById, -}: SetDeletedTimelineEventsParams): TimelineById => { - const timeline = timelineById[id]; - - const deletedEventIds = isDeleted - ? union(timeline.deletedEventIds, eventIds) - : timeline.deletedEventIds.filter(currentEventId => !eventIds.includes(currentEventId)); - - const selectedEventIds = Object.fromEntries( - Object.entries(timeline.selectedEventIds).filter( - ([selectedEventId]) => !deletedEventIds.includes(selectedEventId) - ) - ); - - const isSelectAllChecked = - Object.keys(selectedEventIds).length > 0 ? timeline.isSelectAllChecked : false; - - return { - ...timelineById, - [id]: { - ...timeline, - deletedEventIds, - selectedEventIds, - isSelectAllChecked, - }, - }; -}; - -interface SetLoadingTimelineEventsParams { - id: string; - eventIds: string[]; - isLoading: boolean; - timelineById: TimelineById; -} - -export const setLoadingTimelineEvents = ({ - id, - eventIds, - isLoading, - timelineById, -}: SetLoadingTimelineEventsParams): TimelineById => { - const timeline = timelineById[id]; - - const loadingEventIds = isLoading - ? union(timeline.loadingEventIds, eventIds) - : timeline.loadingEventIds.filter(currentEventId => !eventIds.includes(currentEventId)); - - return { - ...timelineById, - [id]: { - ...timeline, - loadingEventIds, - }, - }; -}; - -interface SetSelectedTimelineEventsParams { - id: string; - eventIds: Record; - isSelectAllChecked: boolean; - isSelected: boolean; - timelineById: TimelineById; -} - -export const setSelectedTimelineEvents = ({ - id, - eventIds, - isSelectAllChecked = false, - isSelected, - timelineById, -}: SetSelectedTimelineEventsParams): TimelineById => { - const timeline = timelineById[id]; - - const selectedEventIds = isSelected - ? { ...timeline.selectedEventIds, ...eventIds } - : omit(Object.keys(eventIds), timeline.selectedEventIds); - - return { - ...timelineById, - [id]: { - ...timeline, - selectedEventIds, - isSelectAllChecked, - }, - }; -}; - -interface UnPinTimelineEventParams { - id: string; - eventId: string; - timelineById: TimelineById; -} - -export const unPinTimelineEvent = ({ - id, - eventId, - timelineById, -}: UnPinTimelineEventParams): TimelineById => { - const timeline = timelineById[id]; - return { - ...timelineById, - [id]: { - ...timeline, - pinnedEventIds: omit(eventId, timeline.pinnedEventIds), - }, - }; -}; - -interface UpdateHighlightedDropAndProviderIdParams { - id: string; - providerId: string; - timelineById: TimelineById; -} - -export const updateHighlightedDropAndProvider = ({ - id, - providerId, - timelineById, -}: UpdateHighlightedDropAndProviderIdParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - highlightedDropAndProviderId: providerId, - }, - }; -}; - -interface UpdateSavedQueryParams { - id: string; - savedQueryId: string | null; - timelineById: TimelineById; -} - -export const updateSavedQuery = ({ - id, - savedQueryId, - timelineById, -}: UpdateSavedQueryParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - savedQueryId, - }, - }; -}; - -interface UpdateFiltersParams { - id: string; - filters: Filter[]; - timelineById: TimelineById; -} - -export const updateFilters = ({ id, filters, timelineById }: UpdateFiltersParams): TimelineById => { - const timeline = timelineById[id]; - - return { - ...timelineById, - [id]: { - ...timeline, - filters, - }, - }; -}; diff --git a/x-pack/plugins/siem/public/store/timeline/index.ts b/x-pack/plugins/siem/public/store/timeline/index.ts deleted file mode 100644 index a1c51905e8d0bf..00000000000000 --- a/x-pack/plugins/siem/public/store/timeline/index.ts +++ /dev/null @@ -1,10 +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 timelineActions from './actions'; -import * as timelineSelectors from './selectors'; - -export { timelineActions, timelineSelectors }; diff --git a/x-pack/plugins/siem/public/store/timeline/model.ts b/x-pack/plugins/siem/public/store/timeline/model.ts deleted file mode 100644 index 54e19812634ac2..00000000000000 --- a/x-pack/plugins/siem/public/store/timeline/model.ts +++ /dev/null @@ -1,161 +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 { Filter } from '../../../../../../src/plugins/data/public'; - -import { TimelineTypeLiteralWithNull } from '../../../common/types/timeline'; - -import { DataProvider } from '../../components/timeline/data_providers/data_provider'; -import { Sort } from '../../components/timeline/body/sort'; -import { PinnedEvent, TimelineNonEcsData } from '../../graphql/types'; -import { KueryFilterQuery, SerializedFilterQuery } from '../model'; - -export const DEFAULT_PAGE_COUNT = 2; // Eui Pager will not render unless this is a minimum of 2 pages -export type KqlMode = 'filter' | 'search'; -export type EventType = 'all' | 'raw' | 'signal'; - -export type ColumnHeaderType = 'not-filtered' | 'text-filter'; - -/** Uniquely identifies a column */ -export type ColumnId = string; - -/** The specification of a column header */ -export interface ColumnHeaderOptions { - aggregatable?: boolean; - category?: string; - columnHeaderType: ColumnHeaderType; - description?: string; - example?: string; - format?: string; - id: ColumnId; - label?: string; - linkField?: string; - placeholder?: string; - type?: string; - width: number; -} - -export interface TimelineModel { - /** The columns displayed in the timeline */ - columns: ColumnHeaderOptions[]; - /** The sources of the event data shown in the timeline */ - dataProviders: DataProvider[]; - /** Events to not be rendered **/ - deletedEventIds: string[]; - /** A summary of the events and notes in this timeline */ - description: string; - /** Typoe of event you want to see in this timeline */ - eventType?: EventType; - /** A map of events in this timeline to the chronologically ordered notes (in this timeline) associated with the event */ - eventIdToNoteIds: Record; - filters?: Filter[]; - /** The chronological history of actions related to this timeline */ - historyIds: string[]; - /** The chronological history of actions related to this timeline */ - highlightedDropAndProviderId: string; - /** Uniquely identifies the timeline */ - id: string; - /** If selectAll checkbox in header is checked **/ - isSelectAllChecked: boolean; - /** Events to be rendered as loading **/ - loadingEventIds: string[]; - savedObjectId: string | null; - /** When true, this timeline was marked as "favorite" by the user */ - isFavorite: boolean; - /** When true, the timeline will update as new data arrives */ - isLive: boolean; - /** The number of items to show in a single page of results */ - itemsPerPage: number; - /** Displays a series of choices that when selected, become the value of `itemsPerPage` */ - itemsPerPageOptions: number[]; - /** determines the behavior of the KQL bar */ - kqlMode: KqlMode; - /** the KQL query in the KQL bar */ - kqlQuery: { - filterQuery: SerializedFilterQuery | null; - filterQueryDraft: KueryFilterQuery | null; - }; - /** Title */ - title: string; - /** timelineType: default | template */ - timelineType: TimelineTypeLiteralWithNull; - /** an unique id for template timeline */ - templateTimelineId: string | null; - /** null for default timeline, number for template timeline */ - templateTimelineVersion: number | null; - /** Notes added to the timeline itself. Notes added to events are stored (separately) in `eventIdToNote` */ - noteIds: string[]; - /** Events pinned to this timeline */ - pinnedEventIds: Record; - pinnedEventsSaveObject: Record; - /** Specifies the granularity of the date range (e.g. 1 Day / Week / Month) applicable to the mini-map */ - dateRange: { - start: number; - end: number; - }; - savedQueryId?: string | null; - /** Events selected on this timeline -- eventId to TimelineNonEcsData[] mapping of data required for batch actions **/ - selectedEventIds: Record; - /** When true, show the timeline flyover */ - show: boolean; - /** When true, shows checkboxes enabling selection. Selected events store in selectedEventIds **/ - showCheckboxes: boolean; - /** When true, shows additional rowRenderers below the PlainRowRenderer **/ - showRowRenderers: boolean; - /** Specifies which column the timeline is sorted on, and the direction (ascending / descending) */ - sort: Sort; - /** Persists the UI state (width) of the timeline flyover */ - width: number; - /** timeline is saving */ - isSaving: boolean; - isLoading: boolean; - version: string | null; -} - -export type SubsetTimelineModel = Readonly< - Pick< - TimelineModel, - | 'columns' - | 'dataProviders' - | 'deletedEventIds' - | 'description' - | 'eventType' - | 'eventIdToNoteIds' - | 'highlightedDropAndProviderId' - | 'historyIds' - | 'isFavorite' - | 'isLive' - | 'isSelectAllChecked' - | 'itemsPerPage' - | 'itemsPerPageOptions' - | 'kqlMode' - | 'kqlQuery' - | 'title' - | 'timelineType' - | 'templateTimelineId' - | 'templateTimelineVersion' - | 'loadingEventIds' - | 'noteIds' - | 'pinnedEventIds' - | 'pinnedEventsSaveObject' - | 'dateRange' - | 'selectedEventIds' - | 'show' - | 'showCheckboxes' - | 'showRowRenderers' - | 'sort' - | 'width' - | 'isSaving' - | 'isLoading' - | 'savedObjectId' - | 'version' - > ->; - -export interface TimelineUrl { - id: string; - isOpen: boolean; -} diff --git a/x-pack/plugins/siem/public/store/timeline/reducer.test.ts b/x-pack/plugins/siem/public/store/timeline/reducer.test.ts deleted file mode 100644 index 42c6d6ecb0e51b..00000000000000 --- a/x-pack/plugins/siem/public/store/timeline/reducer.test.ts +++ /dev/null @@ -1,2254 +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 { cloneDeep, set } from 'lodash/fp'; - -import { TimelineType } from '../../../common/types/timeline'; - -import { - IS_OPERATOR, - DataProvider, - DataProvidersAnd, -} from '../../components/timeline/data_providers/data_provider'; -import { defaultColumnHeaderType } from '../../components/timeline/body/column_headers/default_headers'; -import { - DEFAULT_COLUMN_MIN_WIDTH, - DEFAULT_TIMELINE_WIDTH, -} from '../../components/timeline/body/constants'; -import { getColumnWidthFromType } from '../../components/timeline/body/column_headers/helpers'; -import { Direction } from '../../graphql/types'; -import { defaultHeaders } from '../../mock'; - -import { - addNewTimeline, - addTimelineProvider, - addTimelineToStore, - applyDeltaToTimelineColumnWidth, - removeTimelineColumn, - removeTimelineProvider, - updateTimelineColumns, - updateTimelineDescription, - updateTimelineItemsPerPage, - updateTimelinePerPageOptions, - updateTimelineProviderEnabled, - updateTimelineProviderExcluded, - updateTimelineProviders, - updateTimelineRange, - updateTimelineShowTimeline, - updateTimelineSort, - updateTimelineTitle, - upsertTimelineColumn, -} from './helpers'; -import { ColumnHeaderOptions } from './model'; -import { timelineDefaults } from './defaults'; -import { TimelineById } from './types'; - -const timelineByIdMock: TimelineById = { - foo: { - dataProviders: [ - { - and: [], - id: '123', - name: 'data provider 1', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', - }, - ], - columns: [], - description: '', - deletedEventIds: [], - eventIdToNoteIds: {}, - highlightedDropAndProviderId: '', - historyIds: [], - id: 'foo', - savedObjectId: null, - isFavorite: false, - isLive: false, - isSelectAllChecked: false, - isLoading: false, - itemsPerPage: 25, - itemsPerPageOptions: [10, 25, 50], - kqlMode: 'filter', - kqlQuery: { filterQuery: null, filterQueryDraft: null }, - loadingEventIds: [], - title: '', - timelineType: TimelineType.default, - templateTimelineVersion: null, - templateTimelineId: null, - noteIds: [], - pinnedEventIds: {}, - pinnedEventsSaveObject: {}, - dateRange: { - start: 0, - end: 0, - }, - selectedEventIds: {}, - show: true, - showCheckboxes: false, - showRowRenderers: true, - sort: { - columnId: '@timestamp', - sortDirection: Direction.desc, - }, - width: DEFAULT_TIMELINE_WIDTH, - isSaving: false, - version: null, - }, -}; - -const columnsMock: ColumnHeaderOptions[] = [ - defaultHeaders[0], - defaultHeaders[1], - defaultHeaders[2], -]; - -describe('Timeline', () => { - describe('#add saved object Timeline to store ', () => { - test('should return a timelineModel with default value and not just a timelineResult ', () => { - const update = addTimelineToStore({ - id: 'foo', - timeline: { - ...timelineByIdMock.foo, - }, - timelineById: timelineByIdMock, - }); - - expect(update).toEqual({ - foo: { - ...timelineByIdMock.foo, - show: true, - }, - }); - }); - }); - - describe('#addNewTimeline', () => { - test('should return a new reference and not the same reference', () => { - const update = addNewTimeline({ - id: 'bar', - columns: defaultHeaders, - timelineById: timelineByIdMock, - }); - expect(update).not.toBe(timelineByIdMock); - }); - - test('should add a new timeline', () => { - const update = addNewTimeline({ - id: 'bar', - columns: timelineDefaults.columns, - timelineById: timelineByIdMock, - }); - expect(update).toEqual({ - foo: timelineByIdMock.foo, - bar: set('id', 'bar', timelineDefaults), - }); - }); - - test('should add the specified columns to the timeline', () => { - const barWithEmptyColumns = set('id', 'bar', timelineDefaults); - const barWithPopulatedColumns = set('columns', defaultHeaders, barWithEmptyColumns); - - const update = addNewTimeline({ - id: 'bar', - columns: defaultHeaders, - timelineById: timelineByIdMock, - }); - expect(update).toEqual({ - foo: timelineByIdMock.foo, - bar: barWithPopulatedColumns, - }); - }); - }); - - describe('#updateTimelineShowTimeline', () => { - test('should return a new reference and not the same reference', () => { - const update = updateTimelineShowTimeline({ - id: 'foo', - show: false, - timelineById: timelineByIdMock, - }); - expect(update).not.toBe(timelineByIdMock); - }); - - test('should change show from true to false', () => { - const update = updateTimelineShowTimeline({ - id: 'foo', - show: false, // value we are changing from true to false - timelineById: timelineByIdMock, - }); - expect(update).toEqual(set('foo.show', false, timelineByIdMock)); - }); - }); - - describe('#upsertTimelineColumn', () => { - let timelineById: TimelineById = {}; - let columns: ColumnHeaderOptions[] = []; - let columnToAdd: ColumnHeaderOptions; - - beforeEach(() => { - timelineById = cloneDeep(timelineByIdMock); - columns = cloneDeep(columnsMock); - columnToAdd = { - category: 'event', - columnHeaderType: defaultColumnHeaderType, - description: - 'The action captured by the event.\nThis describes the information in the event. It is more specific than `event.category`. Examples are `group-add`, `process-started`, `file-created`. The value is normally defined by the implementer.', - example: 'user-password-change', - id: 'event.action', - type: 'keyword', - aggregatable: true, - width: DEFAULT_COLUMN_MIN_WIDTH, - }; - }); - - test('should return a new reference and not the same reference', () => { - const update = upsertTimelineColumn({ - column: columnToAdd, - id: 'foo', - index: 0, - timelineById, - }); - - expect(update).not.toBe(timelineById); - }); - - test('should add a new column to an empty collection of columns', () => { - const expectedColumns = [columnToAdd]; - const update = upsertTimelineColumn({ - column: columnToAdd, - id: 'foo', - index: 0, - timelineById, - }); - - expect(update).toEqual(set('foo.columns', expectedColumns, timelineById)); - }); - - test('should add a new column to an existing collection of columns at the beginning of the collection', () => { - const expectedColumns = [columnToAdd, ...columns]; - const mockWithExistingColumns = set('foo.columns', columns, timelineById); - - const update = upsertTimelineColumn({ - column: columnToAdd, - id: 'foo', - index: 0, - timelineById: mockWithExistingColumns, - }); - - expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); - }); - - test('should add a new column to an existing collection of columns in the middle of the collection', () => { - const expectedColumns = [columns[0], columnToAdd, columns[1], columns[2]]; - const mockWithExistingColumns = set('foo.columns', columns, timelineById); - - const update = upsertTimelineColumn({ - column: columnToAdd, - id: 'foo', - index: 1, - timelineById: mockWithExistingColumns, - }); - - expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); - }); - - test('should add a new column to an existing collection of columns at the end of the collection', () => { - const expectedColumns = [...columns, columnToAdd]; - const mockWithExistingColumns = set('foo.columns', columns, timelineById); - - const update = upsertTimelineColumn({ - column: columnToAdd, - id: 'foo', - index: expectedColumns.length - 1, - timelineById: mockWithExistingColumns, - }); - - expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); - }); - - columns.forEach((column, i) => { - test(`should upsert (NOT add a new column) a column when already exists at the same index (${i})`, () => { - const mockWithExistingColumns = set('foo.columns', columns, timelineById); - - const update = upsertTimelineColumn({ - column, - id: 'foo', - index: i, - timelineById: mockWithExistingColumns, - }); - - expect(update).toEqual(set('foo.columns', columns, mockWithExistingColumns)); - }); - }); - - test('should allow the 1st column to be moved to the 2nd column', () => { - const expectedColumns = [columns[1], columns[0], columns[2]]; - const mockWithExistingColumns = set('foo.columns', columns, timelineById); - - const update = upsertTimelineColumn({ - column: columns[0], - id: 'foo', - index: 1, - timelineById: mockWithExistingColumns, - }); - - expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); - }); - - test('should allow the 1st column to be moved to the 3rd column', () => { - const expectedColumns = [columns[1], columns[2], columns[0]]; - const mockWithExistingColumns = set('foo.columns', columns, timelineById); - - const update = upsertTimelineColumn({ - column: columns[0], - id: 'foo', - index: 2, - timelineById: mockWithExistingColumns, - }); - - expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); - }); - - test('should allow the 2nd column to be moved to the 1st column', () => { - const expectedColumns = [columns[1], columns[0], columns[2]]; - const mockWithExistingColumns = set('foo.columns', columns, timelineById); - - const update = upsertTimelineColumn({ - column: columns[1], - id: 'foo', - index: 0, - timelineById: mockWithExistingColumns, - }); - - expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); - }); - - test('should allow the 2nd column to be moved to the 3rd column', () => { - const expectedColumns = [columns[0], columns[2], columns[1]]; - const mockWithExistingColumns = set('foo.columns', columns, timelineById); - - const update = upsertTimelineColumn({ - column: columns[1], - id: 'foo', - index: 2, - timelineById: mockWithExistingColumns, - }); - - expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); - }); - - test('should allow the 3rd column to be moved to the 1st column', () => { - const expectedColumns = [columns[2], columns[0], columns[1]]; - const mockWithExistingColumns = set('foo.columns', columns, timelineById); - - const update = upsertTimelineColumn({ - column: columns[2], - id: 'foo', - index: 0, - timelineById: mockWithExistingColumns, - }); - - expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); - }); - - test('should allow the 3rd column to be moved to the 2nd column', () => { - const expectedColumns = [columns[0], columns[2], columns[1]]; - const mockWithExistingColumns = set('foo.columns', columns, timelineById); - - const update = upsertTimelineColumn({ - column: columns[2], - id: 'foo', - index: 1, - timelineById: mockWithExistingColumns, - }); - - expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); - }); - }); - - describe('#addTimelineProvider', () => { - test('should return a new reference and not the same reference', () => { - const update = addTimelineProvider({ - id: 'foo', - provider: { - and: [], - id: '567', - name: 'data provider 2', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', - }, - timelineById: timelineByIdMock, - }); - expect(update).not.toBe(timelineByIdMock); - }); - - test('should add a new timeline provider', () => { - const providerToAdd: DataProvider = { - and: [], - id: '567', - name: 'data provider 2', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', - }; - const update = addTimelineProvider({ - id: 'foo', - provider: providerToAdd, - timelineById: timelineByIdMock, - }); - const addedDataProvider = timelineByIdMock.foo.dataProviders.concat(providerToAdd); - expect(update).toEqual(set('foo.dataProviders', addedDataProvider, timelineByIdMock)); - }); - - test('should NOT add a new timeline provider if it already exists and the attributes "and" is empty', () => { - const providerToAdd: DataProvider = { - and: [], - id: '123', - name: 'data provider 1', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', - }; - const update = addTimelineProvider({ - id: 'foo', - provider: providerToAdd, - timelineById: timelineByIdMock, - }); - expect(update).toEqual(timelineByIdMock); - }); - - test('should add a new timeline provider if it already exists and the attributes "and" is NOT empty', () => { - const myMockTimelineByIdMock = cloneDeep(timelineByIdMock); - myMockTimelineByIdMock.foo.dataProviders[0].and = [ - { - id: '456', - name: 'and data provider 1', - enabled: true, - excluded: false, - kqlQuery: '', - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - }, - ]; - const providerToAdd: DataProvider = { - and: [], - id: '123', - name: 'data provider 1', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', - }; - const update = addTimelineProvider({ - id: 'foo', - provider: providerToAdd, - timelineById: myMockTimelineByIdMock, - }); - expect(update).toEqual(set('foo.dataProviders[1]', providerToAdd, myMockTimelineByIdMock)); - }); - - test('should UPSERT an existing timeline provider if it already exists', () => { - const providerToAdd: DataProvider = { - and: [], - id: '123', - name: 'my name changed', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - excluded: false, - kqlQuery: '', - }; - const update = addTimelineProvider({ - id: 'foo', - provider: providerToAdd, - timelineById: timelineByIdMock, - }); - expect(update).toEqual(set('foo.dataProviders[0].name', 'my name changed', timelineByIdMock)); - }); - }); - - describe('#removeTimelineColumn', () => { - test('should return a new reference and not the same reference', () => { - // pre-populate a new mock with existing columns: - const mockWithExistingColumns = set('foo.columns', columnsMock, timelineByIdMock); - - const update = removeTimelineColumn({ - id: 'foo', - columnId: columnsMock[0].id, - timelineById: mockWithExistingColumns, - }); - - expect(update).not.toBe(timelineByIdMock); - }); - - test('should remove just the first column when the id matches', () => { - const expectedColumns = [columnsMock[1], columnsMock[2]]; - - // pre-populate a new mock with existing columns: - const mockWithExistingColumns = set('foo.columns', columnsMock, timelineByIdMock); - - const update = removeTimelineColumn({ - id: 'foo', - columnId: columnsMock[0].id, - timelineById: mockWithExistingColumns, - }); - - expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); - }); - - test('should remove just the last column when the id matches', () => { - const expectedColumns = [columnsMock[0], columnsMock[1]]; - - // pre-populate a new mock with existing columns: - const mockWithExistingColumns = set('foo.columns', columnsMock, timelineByIdMock); - - const update = removeTimelineColumn({ - id: 'foo', - columnId: columnsMock[2].id, - timelineById: mockWithExistingColumns, - }); - - expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); - }); - - test('should remove just the middle column when the id matches', () => { - const expectedColumns = [columnsMock[0], columnsMock[2]]; - - // pre-populate a new mock with existing columns: - const mockWithExistingColumns = set('foo.columns', columnsMock, timelineByIdMock); - - const update = removeTimelineColumn({ - id: 'foo', - columnId: columnsMock[1].id, - timelineById: mockWithExistingColumns, - }); - - expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); - }); - - test('should not modify the columns if the id to remove was not found', () => { - const expectedColumns = cloneDeep(columnsMock); - - // pre-populate a new mock with existing columns: - const mockWithExistingColumns = set('foo.columns', columnsMock, timelineByIdMock); - - const update = removeTimelineColumn({ - id: 'foo', - columnId: 'does.not.exist', - timelineById: mockWithExistingColumns, - }); - - expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); - }); - }); - - describe('#applyDeltaToColumnWidth', () => { - test('should return a new reference and not the same reference', () => { - const delta = 50; - // pre-populate a new mock with existing columns: - const mockWithExistingColumns = set('foo.columns', columnsMock, timelineByIdMock); - - const update = applyDeltaToTimelineColumnWidth({ - id: 'foo', - columnId: columnsMock[0].id, - delta, - timelineById: mockWithExistingColumns, - }); - - expect(update).not.toBe(timelineByIdMock); - }); - - test('should update (just) the specified column of type `date` when the id matches, and the result of applying the delta is greater than the min width for a date column', () => { - const aDateColumn = columnsMock[0]; - const delta = 50; - const expectedToHaveNewWidth = { - ...aDateColumn, - width: getColumnWidthFromType(aDateColumn.type!) + delta, - }; - const expectedColumns = [expectedToHaveNewWidth, columnsMock[1], columnsMock[2]]; - - // pre-populate a new mock with existing columns: - const mockWithExistingColumns = set('foo.columns', columnsMock, timelineByIdMock); - - const update = applyDeltaToTimelineColumnWidth({ - id: 'foo', - columnId: aDateColumn.id, - delta, - timelineById: mockWithExistingColumns, - }); - - expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); - }); - - test('should NOT update (just) the specified column of type `date` when the id matches, because the result of applying the delta is less than the min width for a date column', () => { - const aDateColumn = columnsMock[0]; - const delta = -50; // this will be less than the min - const expectedToHaveNewWidth = { - ...aDateColumn, - width: getColumnWidthFromType(aDateColumn.type!), // we expect the minimum - }; - const expectedColumns = [expectedToHaveNewWidth, columnsMock[1], columnsMock[2]]; - - // pre-populate a new mock with existing columns: - const mockWithExistingColumns = set('foo.columns', columnsMock, timelineByIdMock); - - const update = applyDeltaToTimelineColumnWidth({ - id: 'foo', - columnId: aDateColumn.id, - delta, - timelineById: mockWithExistingColumns, - }); - - expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); - }); - - test('should update (just) the specified non-date column when the id matches, and the result of applying the delta is greater than the min width for the column', () => { - const aNonDateColumn = columnsMock[1]; - const delta = 50; - const expectedToHaveNewWidth = { - ...aNonDateColumn, - width: getColumnWidthFromType(aNonDateColumn.type!) + delta, - }; - const expectedColumns = [columnsMock[0], expectedToHaveNewWidth, columnsMock[2]]; - - // pre-populate a new mock with existing columns: - const mockWithExistingColumns = set('foo.columns', columnsMock, timelineByIdMock); - - const update = applyDeltaToTimelineColumnWidth({ - id: 'foo', - columnId: aNonDateColumn.id, - delta, - timelineById: mockWithExistingColumns, - }); - - expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); - }); - - test('should NOT update the specified non-date column when the id matches, because the result of applying the delta is less than the min width for the column', () => { - const aNonDateColumn = columnsMock[1]; - const delta = -50; - const expectedToHaveNewWidth = { - ...aNonDateColumn, - width: getColumnWidthFromType(aNonDateColumn.type!), - }; - const expectedColumns = [columnsMock[0], expectedToHaveNewWidth, columnsMock[2]]; - - // pre-populate a new mock with existing columns: - const mockWithExistingColumns = set('foo.columns', columnsMock, timelineByIdMock); - - const update = applyDeltaToTimelineColumnWidth({ - id: 'foo', - columnId: aNonDateColumn.id, - delta, - timelineById: mockWithExistingColumns, - }); - - expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); - }); - }); - - describe('#addAndProviderToTimelineProvider', () => { - test('should add a new and provider to an existing timeline provider', () => { - const providerToAdd: DataProvider = { - and: [], - id: '567', - name: 'data provider 2', - enabled: true, - queryMatch: { - field: 'handsome', - value: 'garrett', - operator: IS_OPERATOR, - }, - excluded: false, - kqlQuery: '', - }; - - const newTimeline = addTimelineProvider({ - id: 'foo', - provider: providerToAdd, - timelineById: timelineByIdMock, - }); - - newTimeline.foo.highlightedDropAndProviderId = '567'; - - const andProviderToAdd: DataProvider = { - and: [], - id: '568', - name: 'And Data Provider', - enabled: true, - queryMatch: { - field: 'smart', - value: 'frank', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', - }; - - const update = addTimelineProvider({ - id: 'foo', - provider: andProviderToAdd, - timelineById: newTimeline, - }); - const indexProvider = update.foo.dataProviders.findIndex(i => i.id === '567'); - const addedAndDataProvider = update.foo.dataProviders[indexProvider].and[0]; - const { and, ...expectedResult } = andProviderToAdd; - expect(addedAndDataProvider).toEqual(expectedResult); - newTimeline.foo.highlightedDropAndProviderId = ''; - }); - - test('should add another and provider because it is not a duplicate', () => { - const providerToAdd: DataProvider = { - and: [ - { - id: '568', - name: 'And Data Provider', - enabled: true, - queryMatch: { - field: 'smart', - value: 'garrett', - operator: IS_OPERATOR, - }, - excluded: false, - kqlQuery: '', - }, - ], - id: '567', - name: 'data provider 1', - enabled: true, - queryMatch: { - field: 'handsome', - value: 'frank', - operator: IS_OPERATOR, - }, - excluded: false, - kqlQuery: '', - }; - - const newTimeline = addTimelineProvider({ - id: 'foo', - provider: providerToAdd, - timelineById: timelineByIdMock, - }); - - newTimeline.foo.highlightedDropAndProviderId = '567'; - - const andProviderToAdd: DataProvider = { - and: [], - id: '569', - name: 'And Data Provider', - enabled: true, - queryMatch: { - field: 'happy', - value: 'andrewG', - operator: IS_OPERATOR, - }, - excluded: false, - kqlQuery: '', - }; - // temporary, we will have to decouple DataProvider & DataProvidersAnd - // that's bigger a refactor than just fixing a bug - delete andProviderToAdd.and; - const update = addTimelineProvider({ - id: 'foo', - provider: andProviderToAdd, - timelineById: newTimeline, - }); - - expect(update).toEqual(set('foo.dataProviders[1].and[1]', andProviderToAdd, newTimeline)); - newTimeline.foo.highlightedDropAndProviderId = ''; - }); - - test('should NOT add another and provider because it is a duplicate', () => { - const providerToAdd: DataProvider = { - and: [ - { - id: '568', - name: 'And Data Provider', - enabled: true, - queryMatch: { - field: 'smart', - value: 'garrett', - operator: IS_OPERATOR, - }, - excluded: false, - kqlQuery: '', - }, - ], - id: '567', - name: 'data provider 1', - enabled: true, - queryMatch: { - field: 'handsome', - value: 'frank', - operator: IS_OPERATOR, - }, - excluded: false, - kqlQuery: '', - }; - - const newTimeline = addTimelineProvider({ - id: 'foo', - provider: providerToAdd, - timelineById: timelineByIdMock, - }); - - newTimeline.foo.highlightedDropAndProviderId = '567'; - - const andProviderToAdd: DataProvider = { - and: [], - id: '569', - name: 'And Data Provider', - enabled: true, - queryMatch: { - field: 'smart', - value: 'garrett', - operator: IS_OPERATOR, - }, - excluded: false, - kqlQuery: '', - }; - const update = addTimelineProvider({ - id: 'foo', - provider: andProviderToAdd, - timelineById: newTimeline, - }); - - expect(update).toEqual(newTimeline); - newTimeline.foo.highlightedDropAndProviderId = ''; - }); - }); - - describe('#updateTimelineColumns', () => { - test('should return a new reference and not the same reference', () => { - const update = updateTimelineColumns({ - id: 'foo', - columns: columnsMock, - timelineById: timelineByIdMock, - }); - expect(update).not.toBe(timelineByIdMock); - }); - - test('should update a timeline with new columns', () => { - const update = updateTimelineColumns({ - id: 'foo', - columns: columnsMock, - timelineById: timelineByIdMock, - }); - expect(update).toEqual(set('foo.columns', [...columnsMock], timelineByIdMock)); - }); - }); - - describe('#updateTimelineDescription', () => { - const newDescription = 'a new description'; - - test('should return a new reference and not the same reference', () => { - const update = updateTimelineDescription({ - id: 'foo', - description: newDescription, - timelineById: timelineByIdMock, - }); - expect(update).not.toBe(timelineByIdMock); - }); - - test('should update the timeline description', () => { - const update = updateTimelineDescription({ - id: 'foo', - description: newDescription, - timelineById: timelineByIdMock, - }); - expect(update).toEqual(set('foo.description', newDescription, timelineByIdMock)); - }); - - test('should always trim all leading whitespace and allow only one trailing space', () => { - const update = updateTimelineDescription({ - id: 'foo', - description: ' breathing room ', - timelineById: timelineByIdMock, - }); - expect(update).toEqual(set('foo.description', 'breathing room ', timelineByIdMock)); - }); - }); - - describe('#updateTimelineTitle', () => { - const newTitle = 'a new title'; - - test('should return a new reference and not the same reference', () => { - const update = updateTimelineTitle({ - id: 'foo', - title: newTitle, - timelineById: timelineByIdMock, - }); - expect(update).not.toBe(timelineByIdMock); - }); - - test('should update the timeline title', () => { - const update = updateTimelineTitle({ - id: 'foo', - title: newTitle, - timelineById: timelineByIdMock, - }); - expect(update).toEqual(set('foo.title', newTitle, timelineByIdMock)); - }); - - test('should always trim all leading whitespace and allow only one trailing space', () => { - const update = updateTimelineTitle({ - id: 'foo', - title: ' room at the back ', - timelineById: timelineByIdMock, - }); - expect(update).toEqual(set('foo.title', 'room at the back ', timelineByIdMock)); - }); - }); - - describe('#updateTimelineProviders', () => { - test('should return a new reference and not the same reference', () => { - const update = updateTimelineProviders({ - id: 'foo', - providers: [ - { - and: [], - id: '567', - name: 'data provider 2', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', - }, - ], - timelineById: timelineByIdMock, - }); - expect(update).not.toBe(timelineByIdMock); - }); - - test('should add update a timeline with new providers', () => { - const providerToAdd: DataProvider = { - and: [], - id: '567', - name: 'data provider 2', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', - }; - const update = updateTimelineProviders({ - id: 'foo', - providers: [providerToAdd], - timelineById: timelineByIdMock, - }); - expect(update).toEqual(set('foo.dataProviders', [providerToAdd], timelineByIdMock)); - }); - }); - - describe('#updateTimelineRange', () => { - test('should return a new reference and not the same reference', () => { - const update = updateTimelineRange({ - id: 'foo', - start: 23, - end: 33, - timelineById: timelineByIdMock, - }); - expect(update).not.toBe(timelineByIdMock); - }); - - test('should update the timeline range', () => { - const update = updateTimelineRange({ - id: 'foo', - start: 23, - end: 33, - timelineById: timelineByIdMock, - }); - expect(update).toEqual( - set( - 'foo.dateRange', - { - start: 23, - end: 33, - }, - timelineByIdMock - ) - ); - }); - }); - - describe('#updateTimelineSort', () => { - test('should return a new reference and not the same reference', () => { - const update = updateTimelineSort({ - id: 'foo', - sort: { - columnId: 'some column', - sortDirection: Direction.desc, - }, - timelineById: timelineByIdMock, - }); - expect(update).not.toBe(timelineByIdMock); - }); - - test('should update the timeline range', () => { - const update = updateTimelineSort({ - id: 'foo', - sort: { - columnId: 'some column', - sortDirection: Direction.desc, - }, - timelineById: timelineByIdMock, - }); - expect(update).toEqual( - set( - 'foo.sort', - { columnId: 'some column', sortDirection: Direction.desc }, - timelineByIdMock - ) - ); - }); - }); - - describe('#updateTimelineProviderEnabled', () => { - test('should return a new reference and not the same reference', () => { - const update = updateTimelineProviderEnabled({ - id: 'foo', - providerId: '123', - enabled: false, // value we are updating from true to false - timelineById: timelineByIdMock, - }); - expect(update).not.toBe(timelineByIdMock); - }); - - test('should return a new reference for data provider and not the same reference of data provider', () => { - const update = updateTimelineProviderEnabled({ - id: 'foo', - providerId: '123', - enabled: false, // value we are updating from true to false - timelineById: timelineByIdMock, - }); - expect(update.foo.dataProviders).not.toBe(timelineByIdMock.foo.dataProviders); - }); - - test('should update the timeline provider enabled from true to false', () => { - const update = updateTimelineProviderEnabled({ - id: 'foo', - providerId: '123', - enabled: false, // value we are updating from true to false - timelineById: timelineByIdMock, - }); - const expected: TimelineById = { - foo: { - id: 'foo', - savedObjectId: null, - columns: [], - dataProviders: [ - { - and: [], - id: '123', - name: 'data provider 1', - enabled: false, // This value changed from true to false - excluded: false, - kqlQuery: '', - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - }, - ], - deletedEventIds: [], - description: '', - eventIdToNoteIds: {}, - highlightedDropAndProviderId: '', - historyIds: [], - isFavorite: false, - isLive: false, - isSelectAllChecked: false, - isLoading: false, - kqlMode: 'filter', - kqlQuery: { filterQuery: null, filterQueryDraft: null }, - loadingEventIds: [], - title: '', - timelineType: TimelineType.default, - templateTimelineVersion: null, - templateTimelineId: null, - noteIds: [], - dateRange: { - start: 0, - end: 0, - }, - selectedEventIds: {}, - show: true, - showRowRenderers: true, - showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: Direction.desc, - }, - pinnedEventIds: {}, - pinnedEventsSaveObject: {}, - itemsPerPage: 25, - itemsPerPageOptions: [10, 25, 50], - width: DEFAULT_TIMELINE_WIDTH, - isSaving: false, - version: null, - }, - }; - expect(update).toEqual(expected); - }); - - test('should update only one data provider and not two data providers', () => { - const multiDataProvider = timelineByIdMock.foo.dataProviders.concat({ - and: [], - id: '456', - name: 'data provider 1', - enabled: true, - excluded: false, - kqlQuery: '', - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - }); - const multiDataProviderMock = set('foo.dataProviders', multiDataProvider, timelineByIdMock); - const update = updateTimelineProviderEnabled({ - id: 'foo', - providerId: '123', - enabled: false, // value we are updating from true to false - timelineById: multiDataProviderMock, - }); - const expected: TimelineById = { - foo: { - id: 'foo', - savedObjectId: null, - columns: [], - dataProviders: [ - { - and: [], - id: '123', - name: 'data provider 1', - enabled: false, // value we are updating from true to false - excluded: false, - kqlQuery: '', - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - }, - { - and: [], - id: '456', - name: 'data provider 1', - enabled: true, - excluded: false, - kqlQuery: '', - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - }, - ], - description: '', - deletedEventIds: [], - eventIdToNoteIds: {}, - highlightedDropAndProviderId: '', - historyIds: [], - isFavorite: false, - isLive: false, - isSelectAllChecked: false, - isLoading: false, - kqlMode: 'filter', - kqlQuery: { filterQuery: null, filterQueryDraft: null }, - loadingEventIds: [], - title: '', - timelineType: TimelineType.default, - templateTimelineVersion: null, - templateTimelineId: null, - noteIds: [], - dateRange: { - start: 0, - end: 0, - }, - selectedEventIds: {}, - show: true, - showRowRenderers: true, - showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: Direction.desc, - }, - pinnedEventIds: {}, - pinnedEventsSaveObject: {}, - itemsPerPage: 25, - itemsPerPageOptions: [10, 25, 50], - width: DEFAULT_TIMELINE_WIDTH, - isSaving: false, - version: null, - }, - }; - expect(update).toEqual(expected); - }); - }); - - describe('#updateTimelineAndProviderEnabled', () => { - let timelineByIdwithAndMock: TimelineById = timelineByIdMock; - beforeEach(() => { - const providerToAdd: DataProvider = { - and: [ - { - id: '568', - name: 'And Data Provider', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', - }, - ], - id: '567', - name: 'data provider 1', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', - }; - - timelineByIdwithAndMock = addTimelineProvider({ - id: 'foo', - provider: providerToAdd, - timelineById: timelineByIdMock, - }); - }); - - test('should return a new reference and not the same reference', () => { - const update = updateTimelineProviderEnabled({ - id: 'foo', - providerId: '567', - enabled: false, // value we are updating from true to false - timelineById: timelineByIdwithAndMock, - andProviderId: '568', - }); - expect(update).not.toBe(timelineByIdwithAndMock); - }); - - test('should return a new reference for and data provider and not the same reference of data and provider', () => { - const update = updateTimelineProviderEnabled({ - id: 'foo', - providerId: '567', - enabled: false, // value we are updating from true to false - timelineById: timelineByIdwithAndMock, - andProviderId: '568', - }); - expect(update.foo.dataProviders).not.toBe(timelineByIdMock.foo.dataProviders); - }); - - test('should update the timeline and provider enabled from true to false', () => { - const update = updateTimelineProviderEnabled({ - id: 'foo', - providerId: '567', - enabled: false, // value we are updating from true to false - timelineById: timelineByIdwithAndMock, - andProviderId: '568', - }); - const indexProvider = update.foo.dataProviders.findIndex(i => i.id === '567'); - expect(update.foo.dataProviders[indexProvider].and[0].enabled).toEqual(false); - }); - - test('should update only one and data provider and not two and data providers', () => { - const indexProvider = timelineByIdwithAndMock.foo.dataProviders.findIndex( - i => i.id === '567' - ); - const multiAndDataProvider = timelineByIdwithAndMock.foo.dataProviders[ - indexProvider - ].and.concat({ - id: '456', - name: 'new and data provider', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', - }); - const multiAndDataProviderMock = set( - `foo.dataProviders[${indexProvider}].and`, - multiAndDataProvider, - timelineByIdwithAndMock - ); - const update = updateTimelineProviderEnabled({ - id: 'foo', - providerId: '567', - enabled: false, // value we are updating from true to false - timelineById: multiAndDataProviderMock, - andProviderId: '568', - }); - const oldAndProvider = update.foo.dataProviders[indexProvider].and.find(i => i.id === '568'); - const newAndProvider = update.foo.dataProviders[indexProvider].and.find(i => i.id === '456'); - expect(oldAndProvider!.enabled).toEqual(false); - expect(newAndProvider!.enabled).toEqual(true); - }); - }); - - describe('#updateTimelineProviderExcluded', () => { - test('should return a new reference and not the same reference', () => { - const update = updateTimelineProviderExcluded({ - id: 'foo', - providerId: '123', - excluded: true, // value we are updating from false to true - timelineById: timelineByIdMock, - }); - expect(update).not.toBe(timelineByIdMock); - }); - - test('should return a new reference for data provider and not the same reference of data provider', () => { - const update = updateTimelineProviderExcluded({ - id: 'foo', - providerId: '123', - excluded: true, // value we are updating from false to true - timelineById: timelineByIdMock, - }); - expect(update.foo.dataProviders).not.toBe(timelineByIdMock.foo.dataProviders); - }); - - test('should update the timeline provider excluded from true to false', () => { - const update = updateTimelineProviderExcluded({ - id: 'foo', - providerId: '123', - excluded: true, // value we are updating from false to true - timelineById: timelineByIdMock, - }); - const expected: TimelineById = { - foo: { - id: 'foo', - savedObjectId: null, - columns: [], - dataProviders: [ - { - and: [], - id: '123', - name: 'data provider 1', - enabled: true, - excluded: true, // This value changed from true to false - kqlQuery: '', - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - }, - ], - description: '', - deletedEventIds: [], - eventIdToNoteIds: {}, - highlightedDropAndProviderId: '', - historyIds: [], - isFavorite: false, - isLive: false, - isSelectAllChecked: false, - isLoading: false, - kqlMode: 'filter', - kqlQuery: { filterQuery: null, filterQueryDraft: null }, - loadingEventIds: [], - title: '', - timelineType: TimelineType.default, - templateTimelineVersion: null, - templateTimelineId: null, - noteIds: [], - dateRange: { - start: 0, - end: 0, - }, - selectedEventIds: {}, - show: true, - showRowRenderers: true, - showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: Direction.desc, - }, - pinnedEventIds: {}, - pinnedEventsSaveObject: {}, - itemsPerPage: 25, - itemsPerPageOptions: [10, 25, 50], - width: DEFAULT_TIMELINE_WIDTH, - isSaving: false, - version: null, - }, - }; - expect(update).toEqual(expected); - }); - - test('should update only one data provider and not two data providers', () => { - const multiDataProvider = timelineByIdMock.foo.dataProviders.concat({ - and: [], - id: '456', - name: 'data provider 1', - enabled: true, - excluded: false, - kqlQuery: '', - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - }); - const multiDataProviderMock = set('foo.dataProviders', multiDataProvider, timelineByIdMock); - const update = updateTimelineProviderExcluded({ - id: 'foo', - providerId: '123', - excluded: true, // value we are updating from false to true - timelineById: multiDataProviderMock, - }); - const expected: TimelineById = { - foo: { - id: 'foo', - savedObjectId: null, - columns: [], - dataProviders: [ - { - and: [], - id: '123', - name: 'data provider 1', - enabled: true, - excluded: true, // value we are updating from false to true - kqlQuery: '', - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - }, - { - and: [], - id: '456', - name: 'data provider 1', - enabled: true, - excluded: false, - kqlQuery: '', - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - }, - ], - description: '', - deletedEventIds: [], - eventIdToNoteIds: {}, - highlightedDropAndProviderId: '', - historyIds: [], - isFavorite: false, - isLive: false, - isSelectAllChecked: false, - isLoading: false, - kqlMode: 'filter', - kqlQuery: { filterQuery: null, filterQueryDraft: null }, - loadingEventIds: [], - title: '', - timelineType: TimelineType.default, - templateTimelineId: null, - templateTimelineVersion: null, - noteIds: [], - dateRange: { - start: 0, - end: 0, - }, - selectedEventIds: {}, - show: true, - showRowRenderers: true, - showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: Direction.desc, - }, - pinnedEventIds: {}, - pinnedEventsSaveObject: {}, - itemsPerPage: 25, - itemsPerPageOptions: [10, 25, 50], - width: DEFAULT_TIMELINE_WIDTH, - isSaving: false, - version: null, - }, - }; - expect(update).toEqual(expected); - }); - }); - - describe('#updateTimelineAndProviderExcluded', () => { - let timelineByIdwithAndMock: TimelineById = timelineByIdMock; - beforeEach(() => { - const providerToAdd: DataProvider = { - and: [ - { - id: '568', - name: 'And Data Provider', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', - }, - ], - id: '567', - name: 'data provider 1', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', - }; - - timelineByIdwithAndMock = addTimelineProvider({ - id: 'foo', - provider: providerToAdd, - timelineById: timelineByIdMock, - }); - }); - - test('should return a new reference and not the same reference', () => { - const update = updateTimelineProviderExcluded({ - id: 'foo', - providerId: '567', - excluded: true, // value we are updating from true to false - timelineById: timelineByIdwithAndMock, - andProviderId: '568', - }); - expect(update).not.toBe(timelineByIdwithAndMock); - }); - - test('should return a new reference for and data provider and not the same reference of data and provider', () => { - const update = updateTimelineProviderExcluded({ - id: 'foo', - providerId: '567', - excluded: true, // value we are updating from false to true - timelineById: timelineByIdwithAndMock, - andProviderId: '568', - }); - expect(update.foo.dataProviders).not.toBe(timelineByIdMock.foo.dataProviders); - }); - - test('should update the timeline and provider excluded from true to false', () => { - const update = updateTimelineProviderExcluded({ - id: 'foo', - providerId: '567', - excluded: true, // value we are updating from true to false - timelineById: timelineByIdwithAndMock, - andProviderId: '568', - }); - const indexProvider = update.foo.dataProviders.findIndex(i => i.id === '567'); - expect(update.foo.dataProviders[indexProvider].and[0].enabled).toEqual(true); - }); - - test('should update only one and data provider and not two and data providers', () => { - const indexProvider = timelineByIdwithAndMock.foo.dataProviders.findIndex( - i => i.id === '567' - ); - const multiAndDataProvider = timelineByIdwithAndMock.foo.dataProviders[ - indexProvider - ].and.concat({ - id: '456', - name: 'new and data provider', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', - }); - const multiAndDataProviderMock = set( - `foo.dataProviders[${indexProvider}].and`, - multiAndDataProvider, - timelineByIdwithAndMock - ); - const update = updateTimelineProviderExcluded({ - id: 'foo', - providerId: '567', - excluded: true, // value we are updating from true to false - timelineById: multiAndDataProviderMock, - andProviderId: '568', - }); - const oldAndProvider = update.foo.dataProviders[indexProvider].and.find(i => i.id === '568'); - const newAndProvider = update.foo.dataProviders[indexProvider].and.find(i => i.id === '456'); - expect(oldAndProvider!.excluded).toEqual(true); - expect(newAndProvider!.excluded).toEqual(false); - }); - }); - - describe('#updateTimelineItemsPerPage', () => { - test('should return a new reference and not the same reference', () => { - const update = updateTimelineItemsPerPage({ - id: 'foo', - itemsPerPage: 10, // value we are updating from 5 to 10 - timelineById: timelineByIdMock, - }); - expect(update).not.toBe(timelineByIdMock); - }); - - test('should update the items per page from 25 to 50', () => { - const update = updateTimelineItemsPerPage({ - id: 'foo', - itemsPerPage: 50, // value we are updating from 25 to 50 - timelineById: timelineByIdMock, - }); - const expected: TimelineById = { - foo: { - id: 'foo', - savedObjectId: null, - columns: [], - dataProviders: [ - { - and: [], - id: '123', - name: 'data provider 1', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', - }, - ], - description: '', - deletedEventIds: [], - eventIdToNoteIds: {}, - highlightedDropAndProviderId: '', - historyIds: [], - isFavorite: false, - isLive: false, - isSelectAllChecked: false, - isLoading: false, - kqlMode: 'filter', - kqlQuery: { filterQuery: null, filterQueryDraft: null }, - loadingEventIds: [], - title: '', - timelineType: TimelineType.default, - templateTimelineVersion: null, - templateTimelineId: null, - noteIds: [], - dateRange: { - start: 0, - end: 0, - }, - selectedEventIds: {}, - show: true, - showRowRenderers: true, - showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: Direction.desc, - }, - pinnedEventIds: {}, - pinnedEventsSaveObject: {}, - itemsPerPage: 50, - itemsPerPageOptions: [10, 25, 50], - width: DEFAULT_TIMELINE_WIDTH, - isSaving: false, - version: null, - }, - }; - expect(update).toEqual(expected); - }); - }); - - describe('#updateTimelinePerPageOptions', () => { - test('should return a new reference and not the same reference', () => { - const update = updateTimelinePerPageOptions({ - id: 'foo', - itemsPerPageOptions: [100, 200, 300], // value we are updating from [5, 10, 20] - timelineById: timelineByIdMock, - }); - expect(update).not.toBe(timelineByIdMock); - }); - - test('should update the items per page options from [10, 25, 50] to [100, 200, 300]', () => { - const update = updateTimelinePerPageOptions({ - id: 'foo', - itemsPerPageOptions: [100, 200, 300], // value we are updating from [10, 25, 50] - timelineById: timelineByIdMock, - }); - const expected: TimelineById = { - foo: { - columns: [], - dataProviders: [ - { - and: [], - id: '123', - name: 'data provider 1', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', - }, - ], - description: '', - deletedEventIds: [], - eventIdToNoteIds: {}, - highlightedDropAndProviderId: '', - historyIds: [], - isFavorite: false, - isLive: false, - isSelectAllChecked: false, - isLoading: false, - id: 'foo', - savedObjectId: null, - kqlMode: 'filter', - kqlQuery: { filterQuery: null, filterQueryDraft: null }, - loadingEventIds: [], - title: '', - timelineType: TimelineType.default, - templateTimelineVersion: null, - templateTimelineId: null, - noteIds: [], - dateRange: { - start: 0, - end: 0, - }, - selectedEventIds: {}, - show: true, - showRowRenderers: true, - showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: Direction.desc, - }, - pinnedEventIds: {}, - pinnedEventsSaveObject: {}, - itemsPerPage: 25, - itemsPerPageOptions: [100, 200, 300], // updated - width: DEFAULT_TIMELINE_WIDTH, - isSaving: false, - version: null, - }, - }; - expect(update).toEqual(expected); - }); - }); - - describe('#removeTimelineProvider', () => { - test('should return a new reference and not the same reference', () => { - const update = removeTimelineProvider({ - id: 'foo', - providerId: '123', - timelineById: timelineByIdMock, - }); - expect(update).not.toBe(timelineByIdMock); - }); - - test('should remove a timeline provider', () => { - const update = removeTimelineProvider({ - id: 'foo', - providerId: '123', - timelineById: timelineByIdMock, - }); - expect(update).toEqual(set('foo.dataProviders', [], timelineByIdMock)); - }); - - test('should remove only one data provider and not two data providers', () => { - const multiDataProvider = timelineByIdMock.foo.dataProviders.concat({ - and: [], - id: '456', - name: 'data provider 2', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', - }); - const multiDataProviderMock = set('foo.dataProviders', multiDataProvider, timelineByIdMock); - const update = removeTimelineProvider({ - id: 'foo', - providerId: '123', - timelineById: multiDataProviderMock, - }); - const expected: TimelineById = { - foo: { - columns: [], - dataProviders: [ - { - and: [], - id: '456', - name: 'data provider 2', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', - }, - ], - description: '', - deletedEventIds: [], - eventIdToNoteIds: {}, - highlightedDropAndProviderId: '', - historyIds: [], - id: 'foo', - savedObjectId: null, - isFavorite: false, - isLive: false, - isSelectAllChecked: false, - isLoading: false, - kqlMode: 'filter', - kqlQuery: { filterQuery: null, filterQueryDraft: null }, - loadingEventIds: [], - title: '', - timelineType: TimelineType.default, - templateTimelineVersion: null, - templateTimelineId: null, - noteIds: [], - dateRange: { - start: 0, - end: 0, - }, - selectedEventIds: {}, - show: true, - showRowRenderers: true, - showCheckboxes: false, - sort: { - columnId: '@timestamp', - sortDirection: Direction.desc, - }, - pinnedEventIds: {}, - pinnedEventsSaveObject: {}, - itemsPerPage: 25, - itemsPerPageOptions: [10, 25, 50], - width: DEFAULT_TIMELINE_WIDTH, - isSaving: false, - version: null, - }, - }; - expect(update).toEqual(expected); - }); - - test('should remove only first provider and not nested andProvider', () => { - const dataProviders: DataProvider[] = [ - { - and: [], - id: '111', - name: 'data provider 1', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', - }, - { - and: [], - id: '222', - name: 'data provider 2', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', - }, - { - and: [], - id: '333', - name: 'data provider 3', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', - }, - ]; - - const multiDataProviderMock = set('foo.dataProviders', dataProviders, timelineByIdMock); - - const andDataProvider: DataProvidersAnd = { - id: '211', - name: 'And Data Provider', - enabled: true, - queryMatch: { - field: '', - value: '', - operator: IS_OPERATOR, - }, - - excluded: false, - kqlQuery: '', - }; - - const nestedMultiAndDataProviderMock = set( - 'foo.dataProviders[1].and', - [andDataProvider], - multiDataProviderMock - ); - - const update = removeTimelineProvider({ - id: 'foo', - providerId: '222', - timelineById: nestedMultiAndDataProviderMock, - }); - expect(update).toEqual( - set( - 'foo.dataProviders', - [ - nestedMultiAndDataProviderMock.foo.dataProviders[0], - { ...andDataProvider, and: [] }, - nestedMultiAndDataProviderMock.foo.dataProviders[2], - ], - timelineByIdMock - ) - ); - }); - - test('should remove only the first provider and keep multiple nested andProviders', () => { - const multiDataProvider: DataProvider[] = [ - { - and: [ - { - enabled: true, - id: 'socket_closed-MSoH7GoB9v5HJNSHRYj1-user_name-root', - name: 'root', - excluded: false, - kqlQuery: '', - queryMatch: { - field: 'user.name', - value: 'root', - operator: ':', - }, - }, - { - enabled: true, - id: 'executed-yioH7GoB9v5HJNSHKnp5-auditd_result-success', - name: 'success', - excluded: false, - kqlQuery: '', - queryMatch: { - field: 'auditd.result', - value: 'success', - operator: ':', - }, - }, - ], - enabled: true, - excluded: false, - id: 'hosts-table-hostName-suricata-iowa', - name: 'suricata-iowa', - kqlQuery: '', - queryMatch: { - field: 'host.name', - value: 'suricata-iowa', - operator: ':', - }, - }, - ]; - - const multiDataProviderMock = set('foo.dataProviders', multiDataProvider, timelineByIdMock); - - const update = removeTimelineProvider({ - id: 'foo', - providerId: 'hosts-table-hostName-suricata-iowa', - timelineById: multiDataProviderMock, - }); - - expect(update).toEqual( - set( - 'foo.dataProviders', - [ - { - enabled: true, - id: 'socket_closed-MSoH7GoB9v5HJNSHRYj1-user_name-root', - name: 'root', - excluded: false, - kqlQuery: '', - queryMatch: { - field: 'user.name', - value: 'root', - operator: ':', - }, - and: [ - { - enabled: true, - id: 'executed-yioH7GoB9v5HJNSHKnp5-auditd_result-success', - name: 'success', - excluded: false, - kqlQuery: '', - queryMatch: { - field: 'auditd.result', - value: 'success', - operator: ':', - }, - }, - ], - }, - ], - timelineByIdMock - ) - ); - }); - test('should remove only the first AND provider when the first AND is deleted, and there are multiple andProviders', () => { - const multiDataProvider: DataProvider[] = [ - { - and: [ - { - enabled: true, - id: 'socket_closed-MSoH7GoB9v5HJNSHRYj1-user_name-root', - name: 'root', - excluded: false, - kqlQuery: '', - queryMatch: { - field: 'user.name', - value: 'root', - operator: ':', - }, - }, - { - enabled: true, - id: 'executed-yioH7GoB9v5HJNSHKnp5-auditd_result-success', - name: 'success', - excluded: false, - kqlQuery: '', - queryMatch: { - field: 'auditd.result', - value: 'success', - operator: ':', - }, - }, - ], - enabled: true, - excluded: false, - id: 'hosts-table-hostName-suricata-iowa', - name: 'suricata-iowa', - kqlQuery: '', - queryMatch: { - field: 'host.name', - value: 'suricata-iowa', - operator: ':', - }, - }, - ]; - - const multiDataProviderMock = set('foo.dataProviders', multiDataProvider, timelineByIdMock); - - const update = removeTimelineProvider({ - andProviderId: 'socket_closed-MSoH7GoB9v5HJNSHRYj1-user_name-root', - id: 'foo', - providerId: 'hosts-table-hostName-suricata-iowa', - timelineById: multiDataProviderMock, - }); - - expect(update).toEqual( - set( - 'foo.dataProviders', - [ - { - and: [ - { - enabled: true, - id: 'executed-yioH7GoB9v5HJNSHKnp5-auditd_result-success', - name: 'success', - excluded: false, - kqlQuery: '', - queryMatch: { - field: 'auditd.result', - value: 'success', - operator: ':', - }, - }, - ], - enabled: true, - excluded: false, - id: 'hosts-table-hostName-suricata-iowa', - name: 'suricata-iowa', - kqlQuery: '', - queryMatch: { - field: 'host.name', - value: 'suricata-iowa', - operator: ':', - }, - }, - ], - timelineByIdMock - ) - ); - }); - - test('should remove only the second AND provider when the second AND is deleted, and there are multiple andProviders', () => { - const multiDataProvider: DataProvider[] = [ - { - and: [ - { - enabled: true, - id: 'socket_closed-MSoH7GoB9v5HJNSHRYj1-user_name-root', - name: 'root', - excluded: false, - kqlQuery: '', - queryMatch: { - field: 'user.name', - value: 'root', - operator: ':', - }, - }, - { - enabled: true, - id: 'executed-yioH7GoB9v5HJNSHKnp5-auditd_result-success', - name: 'success', - excluded: false, - kqlQuery: '', - queryMatch: { - field: 'auditd.result', - value: 'success', - operator: ':', - }, - }, - ], - enabled: true, - excluded: false, - id: 'hosts-table-hostName-suricata-iowa', - name: 'suricata-iowa', - kqlQuery: '', - queryMatch: { - field: 'host.name', - value: 'suricata-iowa', - operator: ':', - }, - }, - ]; - - const multiDataProviderMock = set('foo.dataProviders', multiDataProvider, timelineByIdMock); - - const update = removeTimelineProvider({ - andProviderId: 'executed-yioH7GoB9v5HJNSHKnp5-auditd_result-success', - id: 'foo', - providerId: 'hosts-table-hostName-suricata-iowa', - timelineById: multiDataProviderMock, - }); - - expect(update).toEqual( - set( - 'foo.dataProviders', - [ - { - and: [ - { - enabled: true, - id: 'socket_closed-MSoH7GoB9v5HJNSHRYj1-user_name-root', - name: 'root', - excluded: false, - kqlQuery: '', - queryMatch: { - field: 'user.name', - value: 'root', - operator: ':', - }, - }, - ], - enabled: true, - excluded: false, - id: 'hosts-table-hostName-suricata-iowa', - name: 'suricata-iowa', - kqlQuery: '', - queryMatch: { - field: 'host.name', - value: 'suricata-iowa', - operator: ':', - }, - }, - ], - timelineByIdMock - ) - ); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/store/timeline/refetch_queries.ts b/x-pack/plugins/siem/public/store/timeline/refetch_queries.ts deleted file mode 100644 index a19f91aa530e8c..00000000000000 --- a/x-pack/plugins/siem/public/store/timeline/refetch_queries.ts +++ /dev/null @@ -1,24 +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 { allTimelinesQuery } from '../../containers/timeline/all/index.gql_query'; -import { Direction } from '../../graphql/types'; -import { DEFAULT_SORT_FIELD } from '../../components/open_timeline/constants'; - -export const refetchQueries = [ - { - query: allTimelinesQuery, - variables: { - search: '', - pageInfo: { - pageIndex: 1, - pageSize: 10, - }, - sort: { sortField: DEFAULT_SORT_FIELD, sortOrder: Direction.desc }, - onlyUserFavorite: false, - }, - }, -]; diff --git a/x-pack/plugins/siem/public/store/timeline/selectors.ts b/x-pack/plugins/siem/public/store/timeline/selectors.ts deleted file mode 100644 index 780145ebfa54c6..00000000000000 --- a/x-pack/plugins/siem/public/store/timeline/selectors.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. - */ - -import { createSelector } from 'reselect'; - -import { isFromKueryExpressionValid } from '../../lib/keury'; -import { State } from '../reducer'; - -import { TimelineModel } from './model'; -import { AutoSavedWarningMsg, TimelineById } from './types'; - -const selectTimelineById = (state: State): TimelineById => state.timeline.timelineById; - -const selectAutoSaveMsg = (state: State): AutoSavedWarningMsg => state.timeline.autoSavedWarningMsg; - -const selectCallOutUnauthorizedMsg = (state: State): boolean => - state.timeline.showCallOutUnauthorizedMsg; - -export const selectTimeline = (state: State, timelineId: string): TimelineModel => - state.timeline.timelineById[timelineId]; - -export const autoSaveMsgSelector = createSelector(selectAutoSaveMsg, autoSaveMsg => autoSaveMsg); - -export const timelineByIdSelector = createSelector( - selectTimelineById, - timelineById => timelineById -); - -export const getShowCallOutUnauthorizedMsg = () => - createSelector( - selectCallOutUnauthorizedMsg, - showCallOutUnauthorizedMsg => showCallOutUnauthorizedMsg - ); - -export const getTimelines = () => timelineByIdSelector; - -export const getTimelineByIdSelector = () => createSelector(selectTimeline, timeline => timeline); - -export const getEventsByIdSelector = () => createSelector(selectTimeline, timeline => timeline); - -export const getKqlFilterQuerySelector = () => - createSelector(selectTimeline, timeline => - timeline && - timeline.kqlQuery && - timeline.kqlQuery.filterQuery && - timeline.kqlQuery.filterQuery.kuery - ? timeline.kqlQuery.filterQuery.kuery.expression - : null - ); - -export const getKqlFilterQueryDraftSelector = () => - createSelector(selectTimeline, timeline => - timeline && timeline.kqlQuery ? timeline.kqlQuery.filterQueryDraft : null - ); - -export const getKqlFilterKuerySelector = () => - createSelector(selectTimeline, timeline => - timeline && - timeline.kqlQuery && - timeline.kqlQuery.filterQuery && - timeline.kqlQuery.filterQuery.kuery - ? timeline.kqlQuery.filterQuery.kuery - : null - ); - -export const isFilterQueryDraftValidSelector = () => - createSelector( - selectTimeline, - timeline => - timeline && - timeline.kqlQuery && - isFromKueryExpressionValid(timeline.kqlQuery.filterQueryDraft) - ); diff --git a/x-pack/plugins/siem/public/timelines/components/certificate_fingerprint/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/certificate_fingerprint/index.test.tsx new file mode 100644 index 00000000000000..16d4d3b6c7cbae --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/certificate_fingerprint/index.test.tsx @@ -0,0 +1,78 @@ +/* + * 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 { TestProviders } from '../../../common/mock'; +import { useMountAppended } from '../../../common/utils/use_mount_appended'; + +import { CertificateFingerprint } from '.'; + +describe('CertificateFingerprint', () => { + const mount = useMountAppended(); + test('renders the expected label', () => { + const wrapper = mount( + + + + ); + expect( + wrapper + .find('[data-test-subj="fingerprint-label"]') + .first() + .text() + ).toEqual('client cert'); + }); + + test('renders the fingerprint as text', () => { + const wrapper = mount( + + + + ); + expect( + wrapper + .find('[data-test-subj="certificate-fingerprint-link"]') + .first() + .text() + ).toEqual('3f4c57934e089f02ae7511200aee2d7e7aabd272'); + }); + + test('it renders a hyperlink to an external site to compare the fingerprint against a known set of signatures', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="certificate-fingerprint-link"]') + .first() + .props().href + ).toEqual( + 'https://sslbl.abuse.ch/ssl-certificates/sha1/3f4c57934e089f02ae7511200aee2d7e7aabd272' + ); + }); +}); diff --git a/x-pack/plugins/siem/public/timelines/components/certificate_fingerprint/index.tsx b/x-pack/plugins/siem/public/timelines/components/certificate_fingerprint/index.tsx new file mode 100644 index 00000000000000..dc3b6ea61b9c3c --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/certificate_fingerprint/index.tsx @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiText } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +import { DraggableBadge } from '../../../common/components/draggables'; +import { ExternalLinkIcon } from '../../../common/components/external_link_icon'; +import { CertificateFingerprintLink } from '../../../common/components/links'; + +import * as i18n from './translations'; + +export type CertificateType = 'client' | 'server'; + +export const TLS_CLIENT_CERTIFICATE_FINGERPRINT_SHA1_FIELD_NAME = + 'tls.client_certificate.fingerprint.sha1'; +export const TLS_SERVER_CERTIFICATE_FINGERPRINT_SHA1_FIELD_NAME = + 'tls.server_certificate.fingerprint.sha1'; + +const FingerprintLabel = styled.span` + margin-right: 5px; +`; + +FingerprintLabel.displayName = 'FingerprintLabel'; + +/** + * Represents a field containing a certificate fingerprint (e.g. a sha1), with + * a link to an external site, which in-turn compares the fingerprint against a + * set of known fingerprints + * Examples: + * 'tls.client_certificate.fingerprint.sha1' + * 'tls.server_certificate.fingerprint.sha1' + */ +export const CertificateFingerprint = React.memo<{ + eventId: string; + certificateType: CertificateType; + contextId: string; + fieldName: string; + value?: string | null; +}>(({ eventId, certificateType, contextId, fieldName, value }) => { + return ( + + {fieldName} + + } + value={value} + > + + {certificateType === 'client' ? i18n.CLIENT_CERT : i18n.SERVER_CERT} + + + + + ); +}); + +CertificateFingerprint.displayName = 'CertificateFingerprint'; diff --git a/x-pack/plugins/siem/public/components/certificate_fingerprint/translations.ts b/x-pack/plugins/siem/public/timelines/components/certificate_fingerprint/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/certificate_fingerprint/translations.ts rename to x-pack/plugins/siem/public/timelines/components/certificate_fingerprint/translations.ts diff --git a/x-pack/plugins/siem/public/timelines/components/duration/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/duration/index.test.tsx new file mode 100644 index 00000000000000..8063921668c90f --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/duration/index.test.tsx @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { TestProviders } from '../../../common/mock'; +import { ONE_MILLISECOND_AS_NANOSECONDS } from '../formatted_duration/helpers'; +import { useMountAppended } from '../../../common/utils/use_mount_appended'; + +import { Duration } from '.'; + +describe('Duration', () => { + const mount = useMountAppended(); + + test('it renders the expected formatted duration', () => { + const wrapper = mount( + + + + ); + expect( + wrapper + .find('[data-test-subj="formatted-duration"]') + .first() + .text() + ).toEqual('1ms'); + }); +}); diff --git a/x-pack/plugins/siem/public/timelines/components/duration/index.tsx b/x-pack/plugins/siem/public/timelines/components/duration/index.tsx new file mode 100644 index 00000000000000..1106ee63a03cba --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/duration/index.tsx @@ -0,0 +1,35 @@ +/* + * 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 { DefaultDraggable } from '../../../common/components/draggables'; +import { FormattedDuration } from '../formatted_duration'; + +export const EVENT_DURATION_FIELD_NAME = 'event.duration'; + +/** + * Renders draggable text containing the value of a field representing a + * duration of time, (e.g. `event.duration`) + */ +export const Duration = React.memo<{ + contextId: string; + eventId: string; + fieldName: string; + value?: string | null; +}>(({ contextId, eventId, fieldName, value }) => ( + + + +)); + +Duration.displayName = 'Duration'; diff --git a/x-pack/plugins/siem/public/timelines/components/edit_data_provider/helpers.test.tsx b/x-pack/plugins/siem/public/timelines/components/edit_data_provider/helpers.test.tsx new file mode 100644 index 00000000000000..87388960dc5d85 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/edit_data_provider/helpers.test.tsx @@ -0,0 +1,295 @@ +/* + * 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 { mockBrowserFields } from '../../../common/containers/source/mock'; +import { EXISTS_OPERATOR, IS_OPERATOR } from '../timeline/data_providers/data_provider'; + +import { + getCategorizedFieldNames, + getExcludedFromSelection, + getFieldNames, + getQueryOperatorFromSelection, + selectionsAreValid, +} from './helpers'; + +import * as i18n from './translations'; + +describe('helpers', () => { + describe('getFieldNames', () => { + test('it should return the expected field names in a category', () => { + expect(getFieldNames(mockBrowserFields.auditd)).toEqual([ + 'auditd.data.a0', + 'auditd.data.a1', + 'auditd.data.a2', + ]); + }); + }); + + describe('getCategorizedFieldNames', () => { + test('it should return the expected field names grouped by category', () => { + expect(getCategorizedFieldNames(mockBrowserFields)).toEqual([ + { + label: 'agent', + options: [ + { label: 'agent.ephemeral_id' }, + { label: 'agent.hostname' }, + { label: 'agent.id' }, + { label: 'agent.name' }, + ], + }, + { + label: 'auditd', + options: [ + { label: 'auditd.data.a0' }, + { label: 'auditd.data.a1' }, + { label: 'auditd.data.a2' }, + ], + }, + { label: 'base', options: [{ label: '@timestamp' }] }, + { + label: 'client', + options: [ + { label: 'client.address' }, + { label: 'client.bytes' }, + { label: 'client.domain' }, + { label: 'client.geo.country_iso_code' }, + ], + }, + { + label: 'cloud', + options: [{ label: 'cloud.account.id' }, { label: 'cloud.availability_zone' }], + }, + { + label: 'container', + options: [ + { label: 'container.id' }, + { label: 'container.image.name' }, + { label: 'container.image.tag' }, + ], + }, + { + label: 'destination', + options: [ + { label: 'destination.address' }, + { label: 'destination.bytes' }, + { label: 'destination.domain' }, + { label: 'destination.ip' }, + { label: 'destination.port' }, + ], + }, + { label: 'event', options: [{ label: 'event.end' }] }, + { label: 'source', options: [{ label: 'source.ip' }, { label: 'source.port' }] }, + ]); + }); + }); + + describe('selectionsAreValid', () => { + test('it should return true when the selected field and operator are valid', () => { + expect( + selectionsAreValid({ + browserFields: mockBrowserFields, + selectedField: [ + { + label: 'destination.bytes', + }, + ], + selectedOperator: [ + { + label: 'is', + }, + ], + }) + ).toBe(true); + }); + + test('it should return false when the selected field is empty', () => { + expect( + selectionsAreValid({ + browserFields: mockBrowserFields, + selectedField: [ + { + label: '', + }, + ], + selectedOperator: [ + { + label: 'is', + }, + ], + }) + ).toBe(false); + }); + + test('it should return false when the selected field is unknown', () => { + expect( + selectionsAreValid({ + browserFields: mockBrowserFields, + selectedField: [ + { + label: 'invalid-field', + }, + ], + selectedOperator: [ + { + label: 'is', + }, + ], + }) + ).toBe(false); + }); + + test('it should return false when the selected operator is empty', () => { + expect( + selectionsAreValid({ + browserFields: mockBrowserFields, + selectedField: [ + { + label: 'destination.bytes', + }, + ], + selectedOperator: [ + { + label: '', + }, + ], + }) + ).toBe(false); + }); + + test('it should return false when the selected operator is unknown', () => { + expect( + selectionsAreValid({ + browserFields: mockBrowserFields, + selectedField: [ + { + label: 'destination.bytes', + }, + ], + selectedOperator: [ + { + label: 'invalid-operator', + }, + ], + }) + ).toBe(false); + }); + }); + + describe('getQueryOperatorFromSelection', () => { + const validSelections = [ + { + operator: i18n.IS, + expected: IS_OPERATOR, + }, + { + operator: i18n.IS_NOT, + expected: IS_OPERATOR, + }, + { + operator: i18n.EXISTS, + expected: EXISTS_OPERATOR, + }, + { + operator: i18n.DOES_NOT_EXIST, + expected: EXISTS_OPERATOR, + }, + ]; + + validSelections.forEach(({ operator, expected }) => { + test(`it should the expected operator given "${operator}", a valid selection`, () => { + expect( + getQueryOperatorFromSelection([ + { + label: operator, + }, + ]) + ).toEqual(expected); + }); + }); + + test('it should default to the "is" operator given an empty selection', () => { + expect( + getQueryOperatorFromSelection([ + { + label: '', + }, + ]) + ).toEqual(IS_OPERATOR); + }); + + test('it should default to the "is" operator given an invalid selection', () => { + expect( + getQueryOperatorFromSelection([ + { + label: 'invalid', + }, + ]) + ).toEqual(IS_OPERATOR); + }); + }); + + describe('getExcludedFromSelection', () => { + test('it returns false when the selected operator is empty', () => { + expect( + getExcludedFromSelection([ + { + label: '', + }, + ]) + ).toBe(false); + }); + + test('it returns false when the "is" operator is selected', () => { + expect( + getExcludedFromSelection([ + { + label: i18n.IS, + }, + ]) + ).toBe(false); + }); + + test('it returns false when the "exists" operator is selected', () => { + expect( + getExcludedFromSelection([ + { + label: i18n.EXISTS, + }, + ]) + ).toBe(false); + }); + + test('it returns false when an unknown selection is made', () => { + expect( + getExcludedFromSelection([ + { + label: 'an unknown selection', + }, + ]) + ).toBe(false); + }); + + test('it returns true when "is not" is selected', () => { + expect( + getExcludedFromSelection([ + { + label: i18n.IS_NOT, + }, + ]) + ).toBe(true); + }); + + test('it returns true when "does not exist" is selected', () => { + expect( + getExcludedFromSelection([ + { + label: i18n.DOES_NOT_EXIST, + }, + ]) + ).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/timelines/components/edit_data_provider/helpers.tsx b/x-pack/plugins/siem/public/timelines/components/edit_data_provider/helpers.tsx new file mode 100644 index 00000000000000..03eb4f9bb515e3 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/edit_data_provider/helpers.tsx @@ -0,0 +1,101 @@ +/* + * 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 { findIndex } from 'lodash/fp'; +import { EuiComboBoxOptionOption } from '@elastic/eui'; + +import { BrowserField, BrowserFields, getAllFieldsByName } from '../../../common/containers/source'; +import { + QueryOperator, + EXISTS_OPERATOR, + IS_OPERATOR, +} from '../timeline/data_providers/data_provider'; + +import * as i18n from './translations'; + +/** The list of operators to display in the `Operator` select */ +export const operatorLabels: EuiComboBoxOptionOption[] = [ + { + label: i18n.IS, + }, + { + label: i18n.IS_NOT, + }, + { + label: i18n.EXISTS, + }, + { + label: i18n.DOES_NOT_EXIST, + }, +]; + +/** Returns the names of fields in a category */ +export const getFieldNames = (category: Partial): string[] => + category.fields != null && Object.keys(category.fields).length > 0 + ? Object.keys(category.fields) + : []; + +/** Returns all field names by category, for display in an `EuiComboBox` */ +export const getCategorizedFieldNames = (browserFields: BrowserFields): EuiComboBoxOptionOption[] => + Object.keys(browserFields) + .sort() + .map(categoryId => ({ + label: categoryId, + options: getFieldNames(browserFields[categoryId]).map(fieldId => ({ + label: fieldId, + })), + })); + +/** Returns true if the specified field name is valid */ +export const selectionsAreValid = ({ + browserFields, + selectedField, + selectedOperator, +}: { + browserFields: BrowserFields; + selectedField: EuiComboBoxOptionOption[]; + selectedOperator: EuiComboBoxOptionOption[]; +}): boolean => { + const fieldId = selectedField.length > 0 ? selectedField[0].label : ''; + const operator = selectedOperator.length > 0 ? selectedOperator[0].label : ''; + + const fieldIsValid = getAllFieldsByName(browserFields)[fieldId] != null; + const operatorIsValid = findIndex(o => o.label === operator, operatorLabels) !== -1; + + return fieldIsValid && operatorIsValid; +}; + +/** Returns a `QueryOperator` based on the user's Operator selection */ +export const getQueryOperatorFromSelection = ( + selectedOperator: EuiComboBoxOptionOption[] +): QueryOperator => { + const selection = selectedOperator.length > 0 ? selectedOperator[0].label : ''; + + switch (selection) { + case i18n.IS: // fall through + case i18n.IS_NOT: + return IS_OPERATOR; + case i18n.EXISTS: // fall through + case i18n.DOES_NOT_EXIST: + return EXISTS_OPERATOR; + default: + return IS_OPERATOR; + } +}; + +/** + * Returns `true` when the search excludes results that match the specified data provider + */ +export const getExcludedFromSelection = (selectedOperator: EuiComboBoxOptionOption[]): boolean => { + const selection = selectedOperator.length > 0 ? selectedOperator[0].label : ''; + + switch (selection) { + case i18n.IS_NOT: // fall through + case i18n.DOES_NOT_EXIST: + return true; + default: + return false; + } +}; diff --git a/x-pack/plugins/siem/public/timelines/components/edit_data_provider/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/edit_data_provider/index.test.tsx new file mode 100644 index 00000000000000..035d1bb59796e7 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/edit_data_provider/index.test.tsx @@ -0,0 +1,428 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; + +import { mockBrowserFields } from '../../../common/containers/source/mock'; +import { TestProviders } from '../../../common/mock'; +import { IS_OPERATOR, EXISTS_OPERATOR } from '../timeline/data_providers/data_provider'; + +import { StatefulEditDataProvider } from '.'; + +interface HasIsDisabled { + isDisabled: boolean; +} + +describe('StatefulEditDataProvider', () => { + const field = 'client.address'; + const timelineId = 'test'; + const value = 'test-host'; + + test('it renders the current field', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="field"]') + .first() + .text() + ).toEqual(field); + }); + + test('it renders the expected placeholder for the current field when field is empty', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="field"]') + .first() + .props().placeholder + ).toEqual('Select a field'); + }); + + test('it renders the "is" operator in a humanized format', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="operator"]') + .first() + .text() + ).toEqual('is'); + }); + + test('it renders the negated "is" operator in a humanized format when isExcluded is true', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="operator"]') + .first() + .text() + ).toEqual('is not'); + }); + + test('it renders the "exists" operator in human-readable format', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="operator"]') + .first() + .text() + ).toEqual('exists'); + }); + + test('it renders the negated "exists" operator in a humanized format when isExcluded is true', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="operator"]') + .first() + .text() + ).toEqual('does not exist'); + }); + + test('it renders the current value when the operator is "is"', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="value"]') + .first() + .props().value + ).toEqual(value); + }); + + test('it renders the current value when the type of value is an array', () => { + const reallyAnArray = ([value] as unknown) as string; + + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="value"]') + .first() + .props().value + ).toEqual(value); + }); + + test('it does NOT render the current value when the operator is "is not" (isExcluded is true)', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="value"]') + .first() + .props().value + ).toEqual(value); + }); + + test('it renders the expected placeholder when value is empty', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="value"]') + .first() + .props().placeholder + ).toEqual('value'); + }); + + test('it does NOT render value when the operator is "exists"', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="value"]').exists()).toBe(false); + }); + + test('it does NOT render value when the operator is "not exists" (isExcluded is true)', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="value"]').exists()).toBe(false); + }); + + test('it does NOT disable the save button when field is valid', () => { + const wrapper = mount( + + + + ); + + const props = wrapper + .find('[data-test-subj="save"]') + .first() + .props() as HasIsDisabled; + + expect(props.isDisabled).toBe(false); + }); + + test('it disables the save button when field is invalid because it is empty', () => { + const wrapper = mount( + + + + ); + + const props = wrapper + .find('[data-test-subj="save"]') + .first() + .props() as HasIsDisabled; + + expect(props.isDisabled).toBe(true); + }); + + test('it disables the save button when field is invalid because it is not contained in the browser fields', () => { + const wrapper = mount( + + + + ); + + const props = wrapper + .find('[data-test-subj="save"]') + .first() + .props() as HasIsDisabled; + + expect(props.isDisabled).toBe(true); + }); + + test('it invokes onDataProviderEdited with the expected values when the user clicks the save button', () => { + const onDataProviderEdited = jest.fn(); + + const wrapper = mount( + + + + ); + + wrapper + .find('[data-test-subj="save"]') + .first() + .simulate('click'); + + wrapper.update(); + + expect(onDataProviderEdited).toBeCalledWith({ + andProviderId: undefined, + excluded: false, + field: 'client.address', + id: 'test', + operator: ':', + providerId: 'hosts-table-hostName-test-host', + value: 'test-host', + }); + }); +}); diff --git a/x-pack/plugins/siem/public/timelines/components/edit_data_provider/index.tsx b/x-pack/plugins/siem/public/timelines/components/edit_data_provider/index.tsx new file mode 100644 index 00000000000000..95f3ec3b316493 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/edit_data_provider/index.tsx @@ -0,0 +1,253 @@ +/* + * 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 { noop } from 'lodash/fp'; +import { + EuiButton, + EuiComboBox, + EuiComboBoxOptionOption, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiPanel, + EuiSpacer, + EuiToolTip, +} from '@elastic/eui'; +import React, { useEffect, useState, useCallback } from 'react'; +import styled from 'styled-components'; + +import { BrowserFields } from '../../../common/containers/source'; +import { OnDataProviderEdited } from '../timeline/events'; +import { QueryOperator } from '../timeline/data_providers/data_provider'; + +import { + getCategorizedFieldNames, + getExcludedFromSelection, + getQueryOperatorFromSelection, + operatorLabels, + selectionsAreValid, +} from './helpers'; + +import * as i18n from './translations'; + +const EDIT_DATA_PROVIDER_WIDTH = 400; +const FIELD_COMBO_BOX_WIDTH = 195; +const OPERATOR_COMBO_BOX_WIDTH = 160; +const SAVE_CLASS_NAME = 'edit-data-provider-save'; +const VALUE_INPUT_CLASS_NAME = 'edit-data-provider-value'; + +export const HeaderContainer = styled.div` + width: ${EDIT_DATA_PROVIDER_WIDTH}; +`; + +HeaderContainer.displayName = 'HeaderContainer'; + +interface Props { + andProviderId?: string; + browserFields: BrowserFields; + field: string; + isExcluded: boolean; + onDataProviderEdited: OnDataProviderEdited; + operator: QueryOperator; + providerId: string; + timelineId: string; + value: string | number; +} + +const sanatizeValue = (value: string | number): string => + Array.isArray(value) ? `${value[0]}` : `${value}`; // fun fact: value should never be an array + +export const getInitialOperatorLabel = ( + isExcluded: boolean, + operator: QueryOperator +): EuiComboBoxOptionOption[] => { + if (operator === ':') { + return isExcluded ? [{ label: i18n.IS_NOT }] : [{ label: i18n.IS }]; + } else { + return isExcluded ? [{ label: i18n.DOES_NOT_EXIST }] : [{ label: i18n.EXISTS }]; + } +}; + +export const StatefulEditDataProvider = React.memo( + ({ + andProviderId, + browserFields, + field, + isExcluded, + onDataProviderEdited, + operator, + providerId, + timelineId, + value, + }) => { + const [updatedField, setUpdatedField] = useState([{ label: field }]); + const [updatedOperator, setUpdatedOperator] = useState( + getInitialOperatorLabel(isExcluded, operator) + ); + const [updatedValue, setUpdatedValue] = useState(value); + + /** Focuses the Value input if it is visible, falling back to the Save button if it's not */ + const focusInput = () => { + const elements = document.getElementsByClassName(VALUE_INPUT_CLASS_NAME); + + if (elements.length > 0) { + (elements[0] as HTMLElement).focus(); // this cast is required because focus() does not exist on every `Element` returned by `getElementsByClassName` + } else { + const saveElements = document.getElementsByClassName(SAVE_CLASS_NAME); + + if (saveElements.length > 0) { + (saveElements[0] as HTMLElement).focus(); + } + } + }; + + const onFieldSelected = useCallback((selectedField: EuiComboBoxOptionOption[]) => { + setUpdatedField(selectedField); + + focusInput(); + }, []); + + const onOperatorSelected = useCallback((operatorSelected: EuiComboBoxOptionOption[]) => { + setUpdatedOperator(operatorSelected); + + focusInput(); + }, []); + + const onValueChange = useCallback((e: React.ChangeEvent) => { + setUpdatedValue(e.target.value); + }, []); + + const disableScrolling = () => { + const x = + window.pageXOffset !== undefined + ? window.pageXOffset + : (document.documentElement || document.body.parentNode || document.body).scrollLeft; + + const y = + window.pageYOffset !== undefined + ? window.pageYOffset + : (document.documentElement || document.body.parentNode || document.body).scrollTop; + + window.onscroll = () => window.scrollTo(x, y); + }; + + const enableScrolling = () => { + window.onscroll = () => noop; + }; + + useEffect(() => { + disableScrolling(); + focusInput(); + return () => { + enableScrolling(); + }; + }, []); + + return ( + + + + + + + 0 ? updatedField[0].label : null}> + + + + + + + + + + + + + + + + + + {updatedOperator.length > 0 && + updatedOperator[0].label !== i18n.EXISTS && + updatedOperator[0].label !== i18n.DOES_NOT_EXIST ? ( + + + + + + ) : null} + + + + + + + + + { + onDataProviderEdited({ + andProviderId, + excluded: getExcludedFromSelection(updatedOperator), + field: updatedField.length > 0 ? updatedField[0].label : '', + id: timelineId, + operator: getQueryOperatorFromSelection(updatedOperator), + providerId, + value: updatedValue, + }); + }} + size="s" + > + {i18n.SAVE} + + + + + + + ); + } +); + +StatefulEditDataProvider.displayName = 'StatefulEditDataProvider'; diff --git a/x-pack/plugins/siem/public/components/edit_data_provider/translations.ts b/x-pack/plugins/siem/public/timelines/components/edit_data_provider/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/edit_data_provider/translations.ts rename to x-pack/plugins/siem/public/timelines/components/edit_data_provider/translations.ts diff --git a/x-pack/plugins/siem/public/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/field_renderers/field_renderers.test.tsx b/x-pack/plugins/siem/public/timelines/components/field_renderers/field_renderers.test.tsx similarity index 97% rename from x-pack/plugins/siem/public/components/field_renderers/field_renderers.test.tsx rename to x-pack/plugins/siem/public/timelines/components/field_renderers/field_renderers.test.tsx index 88d03d8db67611..b853a978c15e2d 100644 --- a/x-pack/plugins/siem/public/components/field_renderers/field_renderers.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/field_renderers/field_renderers.test.tsx @@ -7,9 +7,9 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { FlowTarget, GetIpOverviewQuery, HostEcsFields } from '../../graphql/types'; -import { TestProviders } from '../../mock'; -import { getEmptyValue } from '../empty_value'; +import { FlowTarget, GetIpOverviewQuery, HostEcsFields } from '../../../graphql/types'; +import { TestProviders } from '../../../common/mock'; +import { getEmptyValue } from '../../../common/components/empty_value'; import { autonomousSystemRenderer, @@ -22,8 +22,8 @@ import { DEFAULT_MORE_MAX_HEIGHT, MoreContainer, } from './field_renderers'; -import { mockData } from '../page/network/ip_overview/mock'; -import { useMountAppended } from '../../utils/use_mount_appended'; +import { mockData } from '../../../network/components/ip_overview/mock'; +import { useMountAppended } from '../../../common/utils/use_mount_appended'; type AutonomousSystem = GetIpOverviewQuery.AutonomousSystem; diff --git a/x-pack/plugins/siem/public/components/field_renderers/field_renderers.tsx b/x-pack/plugins/siem/public/timelines/components/field_renderers/field_renderers.tsx similarity index 92% rename from x-pack/plugins/siem/public/components/field_renderers/field_renderers.tsx rename to x-pack/plugins/siem/public/timelines/components/field_renderers/field_renderers.tsx index 222eef515958c4..1d53299c0975e9 100644 --- a/x-pack/plugins/siem/public/components/field_renderers/field_renderers.tsx +++ b/x-pack/plugins/siem/public/timelines/components/field_renderers/field_renderers.tsx @@ -10,14 +10,24 @@ import { getOr } from 'lodash/fp'; import React, { Fragment, useState } from 'react'; import styled from 'styled-components'; -import { AutonomousSystem, FlowTarget, HostEcsFields, IpOverviewData } from '../../graphql/types'; -import { escapeDataProviderId } from '../drag_and_drop/helpers'; -import { DefaultDraggable } from '../draggables'; -import { getEmptyTagValue } from '../empty_value'; -import { FormattedRelativePreferenceDate } from '../formatted_date'; -import { HostDetailsLink, ReputationLink, WhoIsLink, ReputationLinkSetting } from '../links'; -import { Spacer } from '../page'; -import * as i18n from '../page/network/ip_overview/translations'; +import { + AutonomousSystem, + FlowTarget, + HostEcsFields, + IpOverviewData, +} from '../../../graphql/types'; +import { escapeDataProviderId } from '../../../common/components/drag_and_drop/helpers'; +import { DefaultDraggable } from '../../../common/components/draggables'; +import { getEmptyTagValue } from '../../../common/components/empty_value'; +import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date'; +import { + HostDetailsLink, + ReputationLink, + WhoIsLink, + ReputationLinkSetting, +} from '../../../common/components/links'; +import { Spacer } from '../../../common/components/page'; +import * as i18n from '../../../network/components/ip_overview/translations'; const DraggableContainerFlexGroup = styled(EuiFlexGroup)` flex-grow: unset; diff --git a/x-pack/plugins/siem/public/components/fields_browser/categories_pane.test.tsx b/x-pack/plugins/siem/public/timelines/components/fields_browser/categories_pane.test.tsx similarity index 96% rename from x-pack/plugins/siem/public/components/fields_browser/categories_pane.test.tsx rename to x-pack/plugins/siem/public/timelines/components/fields_browser/categories_pane.test.tsx index 361a0789135e41..9a1f9a9d073577 100644 --- a/x-pack/plugins/siem/public/components/fields_browser/categories_pane.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/fields_browser/categories_pane.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; -import { mockBrowserFields } from '../../containers/source/mock'; +import { mockBrowserFields } from '../../../common/containers/source/mock'; import { CATEGORY_PANE_WIDTH } from './helpers'; import { CategoriesPane } from './categories_pane'; diff --git a/x-pack/plugins/siem/public/components/fields_browser/categories_pane.tsx b/x-pack/plugins/siem/public/timelines/components/fields_browser/categories_pane.tsx similarity index 97% rename from x-pack/plugins/siem/public/components/fields_browser/categories_pane.tsx rename to x-pack/plugins/siem/public/timelines/components/fields_browser/categories_pane.tsx index d6972625821cf3..93407e43739105 100644 --- a/x-pack/plugins/siem/public/components/fields_browser/categories_pane.tsx +++ b/x-pack/plugins/siem/public/timelines/components/fields_browser/categories_pane.tsx @@ -8,7 +8,7 @@ import { EuiInMemoryTable, EuiTitle } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; -import { BrowserFields } from '../../containers/source'; +import { BrowserFields } from '../../../common/containers/source'; import { FieldBrowserProps } from './types'; import { getCategoryColumns } from './category_columns'; diff --git a/x-pack/plugins/siem/public/components/fields_browser/category.test.tsx b/x-pack/plugins/siem/public/timelines/components/fields_browser/category.test.tsx similarity index 94% rename from x-pack/plugins/siem/public/components/fields_browser/category.test.tsx rename to x-pack/plugins/siem/public/timelines/components/fields_browser/category.test.tsx index 38eaf43977fa21..177ce5648e79b1 100644 --- a/x-pack/plugins/siem/public/components/fields_browser/category.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/fields_browser/category.test.tsx @@ -6,13 +6,13 @@ import React from 'react'; -import { mockBrowserFields } from '../../containers/source/mock'; +import { mockBrowserFields } from '../../../common/containers/source/mock'; import { Category } from './category'; import { getFieldItems } from './field_items'; import { FIELDS_PANE_WIDTH } from './helpers'; -import { TestProviders } from '../../mock'; -import { useMountAppended } from '../../utils/use_mount_appended'; +import { TestProviders } from '../../../common/mock'; +import { useMountAppended } from '../../../common/utils/use_mount_appended'; import * as i18n from './translations'; diff --git a/x-pack/plugins/siem/public/components/fields_browser/category.tsx b/x-pack/plugins/siem/public/timelines/components/fields_browser/category.tsx similarity index 96% rename from x-pack/plugins/siem/public/components/fields_browser/category.tsx rename to x-pack/plugins/siem/public/timelines/components/fields_browser/category.tsx index 9d2a7da9b2d00f..fc916930394494 100644 --- a/x-pack/plugins/siem/public/components/fields_browser/category.tsx +++ b/x-pack/plugins/siem/public/timelines/components/fields_browser/category.tsx @@ -8,7 +8,7 @@ import { EuiInMemoryTable } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; -import { BrowserFields } from '../../containers/source'; +import { BrowserFields } from '../../../common/containers/source'; import { CategoryTitle } from './category_title'; import { FieldItem, getFieldColumns } from './field_items'; diff --git a/x-pack/plugins/siem/public/components/fields_browser/category_columns.test.tsx b/x-pack/plugins/siem/public/timelines/components/fields_browser/category_columns.test.tsx similarity index 98% rename from x-pack/plugins/siem/public/components/fields_browser/category_columns.test.tsx rename to x-pack/plugins/siem/public/timelines/components/fields_browser/category_columns.test.tsx index e116209ba5d6a1..ec2156bb609fd6 100644 --- a/x-pack/plugins/siem/public/components/fields_browser/category_columns.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/fields_browser/category_columns.test.tsx @@ -7,7 +7,7 @@ import { mount } from 'enzyme'; import React from 'react'; -import { mockBrowserFields } from '../../containers/source/mock'; +import { mockBrowserFields } from '../../../common/containers/source/mock'; import { CATEGORY_PANE_WIDTH, getFieldCount } from './helpers'; import { CategoriesPane } from './categories_pane'; diff --git a/x-pack/plugins/siem/public/components/fields_browser/category_columns.tsx b/x-pack/plugins/siem/public/timelines/components/fields_browser/category_columns.tsx similarity index 92% rename from x-pack/plugins/siem/public/components/fields_browser/category_columns.tsx rename to x-pack/plugins/siem/public/timelines/components/fields_browser/category_columns.tsx index 7133e9b848c5ca..2e952dc24dbd82 100644 --- a/x-pack/plugins/siem/public/components/fields_browser/category_columns.tsx +++ b/x-pack/plugins/siem/public/timelines/components/fields_browser/category_columns.tsx @@ -10,12 +10,12 @@ import { EuiIcon, EuiFlexGroup, EuiFlexItem, EuiLink, EuiText, EuiToolTip } from import React, { useContext } from 'react'; import styled from 'styled-components'; -import { BrowserFields } from '../../containers/source'; -import { getColumnsWithTimestamp } from '../event_details/helpers'; -import { CountBadge } from '../page'; +import { BrowserFields } from '../../../common/containers/source'; +import { getColumnsWithTimestamp } from '../../../common/components/event_details/helpers'; +import { CountBadge } from '../../../common/components/page'; import { OnUpdateColumns } from '../timeline/events'; import { TimelineContext } from '../timeline/timeline_context'; -import { WithHoverActions } from '../with_hover_actions'; +import { WithHoverActions } from '../../../common/components/with_hover_actions'; import { LoadingSpinner, getCategoryPaneCategoryClassName, getFieldCount } from './helpers'; import * as i18n from './translations'; diff --git a/x-pack/plugins/siem/public/components/fields_browser/category_title.test.tsx b/x-pack/plugins/siem/public/timelines/components/fields_browser/category_title.test.tsx similarity index 96% rename from x-pack/plugins/siem/public/components/fields_browser/category_title.test.tsx rename to x-pack/plugins/siem/public/timelines/components/fields_browser/category_title.test.tsx index 792e0342a6d592..8ad9cea9b29414 100644 --- a/x-pack/plugins/siem/public/components/fields_browser/category_title.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/fields_browser/category_title.test.tsx @@ -7,7 +7,7 @@ import { mount } from 'enzyme'; import React from 'react'; -import { mockBrowserFields } from '../../containers/source/mock'; +import { mockBrowserFields } from '../../../common/containers/source/mock'; import { CategoryTitle } from './category_title'; import { getFieldCount } from './helpers'; diff --git a/x-pack/plugins/siem/public/components/fields_browser/category_title.tsx b/x-pack/plugins/siem/public/timelines/components/fields_browser/category_title.tsx similarity index 93% rename from x-pack/plugins/siem/public/components/fields_browser/category_title.tsx rename to x-pack/plugins/siem/public/timelines/components/fields_browser/category_title.tsx index cd14cef328a7e6..c8d59f5c0dfa41 100644 --- a/x-pack/plugins/siem/public/components/fields_browser/category_title.tsx +++ b/x-pack/plugins/siem/public/timelines/components/fields_browser/category_title.tsx @@ -8,9 +8,9 @@ import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; -import { BrowserFields } from '../../containers/source'; +import { BrowserFields } from '../../../common/containers/source'; import { getFieldBrowserCategoryTitleClassName, getFieldCount } from './helpers'; -import { CountBadge } from '../page'; +import { CountBadge } from '../../../common/components/page'; const CountBadgeContainer = styled.div` position: relative; diff --git a/x-pack/plugins/siem/public/components/fields_browser/field_browser.test.tsx b/x-pack/plugins/siem/public/timelines/components/fields_browser/field_browser.test.tsx similarity index 98% rename from x-pack/plugins/siem/public/components/fields_browser/field_browser.test.tsx rename to x-pack/plugins/siem/public/timelines/components/fields_browser/field_browser.test.tsx index 9214fd5f2540ce..d4a6d85c7ccddf 100644 --- a/x-pack/plugins/siem/public/components/fields_browser/field_browser.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/fields_browser/field_browser.test.tsx @@ -7,8 +7,8 @@ import { mount } from 'enzyme'; import React from 'react'; -import { mockBrowserFields } from '../../containers/source/mock'; -import { TestProviders } from '../../mock'; +import { mockBrowserFields } from '../../../common/containers/source/mock'; +import { TestProviders } from '../../../common/mock'; import { FieldsBrowser } from './field_browser'; import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from './helpers'; diff --git a/x-pack/plugins/siem/public/components/fields_browser/field_browser.tsx b/x-pack/plugins/siem/public/timelines/components/fields_browser/field_browser.tsx similarity index 98% rename from x-pack/plugins/siem/public/components/fields_browser/field_browser.tsx rename to x-pack/plugins/siem/public/timelines/components/fields_browser/field_browser.tsx index 02aeab74f8babf..c255bd062bb4cb 100644 --- a/x-pack/plugins/siem/public/components/fields_browser/field_browser.tsx +++ b/x-pack/plugins/siem/public/timelines/components/fields_browser/field_browser.tsx @@ -9,8 +9,8 @@ import React, { useEffect, useCallback } from 'react'; import { noop } from 'lodash/fp'; import styled from 'styled-components'; -import { BrowserFields } from '../../containers/source'; -import { ColumnHeaderOptions } from '../../store/timeline/model'; +import { BrowserFields } from '../../../common/containers/source'; +import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; import { CategoriesPane } from './categories_pane'; import { FieldsPane } from './fields_pane'; import { Header } from './header'; diff --git a/x-pack/plugins/siem/public/components/fields_browser/field_items.test.tsx b/x-pack/plugins/siem/public/timelines/components/fields_browser/field_items.test.tsx similarity index 97% rename from x-pack/plugins/siem/public/components/fields_browser/field_items.test.tsx rename to x-pack/plugins/siem/public/timelines/components/fields_browser/field_items.test.tsx index 226b56dad8c4f2..3b9e5368ff196a 100644 --- a/x-pack/plugins/siem/public/components/fields_browser/field_items.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/fields_browser/field_items.test.tsx @@ -7,16 +7,16 @@ import { omit } from 'lodash/fp'; import React from 'react'; -import { mockBrowserFields } from '../../containers/source/mock'; -import { TestProviders } from '../../mock'; -import { ColumnHeaderOptions } from '../../store/timeline/model'; +import { mockBrowserFields } from '../../../common/containers/source/mock'; +import { TestProviders } from '../../../common/mock'; +import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; import { defaultColumnHeaderType } from '../timeline/body/column_headers/default_headers'; import { DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../timeline/body/constants'; import { Category } from './category'; import { getFieldColumns, getFieldItems } from './field_items'; import { FIELDS_PANE_WIDTH } from './helpers'; -import { useMountAppended } from '../../utils/use_mount_appended'; +import { useMountAppended } from '../../../common/utils/use_mount_appended'; const selectedCategoryId = 'base'; const selectedCategoryFields = mockBrowserFields[selectedCategoryId].fields; diff --git a/x-pack/plugins/siem/public/components/fields_browser/field_items.tsx b/x-pack/plugins/siem/public/timelines/components/fields_browser/field_items.tsx similarity index 85% rename from x-pack/plugins/siem/public/components/fields_browser/field_items.tsx rename to x-pack/plugins/siem/public/timelines/components/fields_browser/field_items.tsx index 62f9297c38ef52..9abcc909a161f4 100644 --- a/x-pack/plugins/siem/public/components/fields_browser/field_items.tsx +++ b/x-pack/plugins/siem/public/timelines/components/fields_browser/field_items.tsx @@ -12,19 +12,27 @@ import React from 'react'; import { Draggable } from 'react-beautiful-dnd'; import styled from 'styled-components'; -import { BrowserField, BrowserFields } from '../../containers/source'; -import { ColumnHeaderOptions } from '../../store/timeline/model'; -import { DragEffects } from '../drag_and_drop/draggable_wrapper'; -import { DroppableWrapper } from '../drag_and_drop/droppable_wrapper'; -import { getDraggableFieldId, getDroppableId, DRAG_TYPE_FIELD } from '../drag_and_drop/helpers'; -import { DraggableFieldBadge } from '../draggables/field_badge'; -import { getEmptyValue } from '../empty_value'; -import { getColumnsWithTimestamp, getExampleText, getIconFromType } from '../event_details/helpers'; -import { SelectableText } from '../selectable_text'; +import { BrowserField, BrowserFields } from '../../../common/containers/source'; +import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; +import { DragEffects } from '../../../common/components/drag_and_drop/draggable_wrapper'; +import { DroppableWrapper } from '../../../common/components/drag_and_drop/droppable_wrapper'; +import { + getDraggableFieldId, + getDroppableId, + DRAG_TYPE_FIELD, +} from '../../../common/components/drag_and_drop/helpers'; +import { DraggableFieldBadge } from '../../../common/components/draggables/field_badge'; +import { getEmptyValue } from '../../../common/components/empty_value'; +import { + getColumnsWithTimestamp, + getExampleText, + getIconFromType, +} from '../../../common/components/event_details/helpers'; +import { SelectableText } from '../../../common/components/selectable_text'; import { defaultColumnHeaderType } from '../timeline/body/column_headers/default_headers'; import { DEFAULT_COLUMN_MIN_WIDTH } from '../timeline/body/constants'; import { OnUpdateColumns } from '../timeline/events'; -import { TruncatableText } from '../truncatable_text'; +import { TruncatableText } from '../../../common/components/truncatable_text'; import { FieldName } from './field_name'; import * as i18n from './translations'; diff --git a/x-pack/plugins/siem/public/components/fields_browser/field_name.test.tsx b/x-pack/plugins/siem/public/timelines/components/fields_browser/field_name.test.tsx similarity index 91% rename from x-pack/plugins/siem/public/components/fields_browser/field_name.test.tsx rename to x-pack/plugins/siem/public/timelines/components/fields_browser/field_name.test.tsx index 31f1e7678aa451..473dd9eca4d1e9 100644 --- a/x-pack/plugins/siem/public/components/fields_browser/field_name.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/fields_browser/field_name.test.tsx @@ -7,9 +7,9 @@ import { mount } from 'enzyme'; import React from 'react'; -import { mockBrowserFields } from '../../containers/source/mock'; -import { TestProviders } from '../../mock'; -import { getColumnsWithTimestamp } from '../event_details/helpers'; +import { mockBrowserFields } from '../../../common/containers/source/mock'; +import { TestProviders } from '../../../common/mock'; +import { getColumnsWithTimestamp } from '../../../common/components/event_details/helpers'; import { FieldName } from './field_name'; diff --git a/x-pack/plugins/siem/public/components/fields_browser/field_name.tsx b/x-pack/plugins/siem/public/timelines/components/fields_browser/field_name.tsx similarity index 93% rename from x-pack/plugins/siem/public/components/fields_browser/field_name.tsx rename to x-pack/plugins/siem/public/timelines/components/fields_browser/field_name.tsx index fc9633b6f87485..4043623f5d4a49 100644 --- a/x-pack/plugins/siem/public/components/fields_browser/field_name.tsx +++ b/x-pack/plugins/siem/public/timelines/components/fields_browser/field_name.tsx @@ -8,13 +8,13 @@ import { EuiButtonIcon, EuiHighlight, EuiIcon, EuiText, EuiToolTip } from '@elas import React, { useCallback, useContext, useState, useMemo } from 'react'; import styled from 'styled-components'; -import { ColumnHeaderOptions } from '../../store/timeline/model'; +import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; import { OnUpdateColumns } from '../timeline/events'; import { TimelineContext } from '../timeline/timeline_context'; -import { WithHoverActions } from '../with_hover_actions'; +import { WithHoverActions } from '../../../common/components/with_hover_actions'; import { LoadingSpinner } from './helpers'; import * as i18n from './translations'; -import { DraggableWrapperHoverContent } from '../drag_and_drop/draggable_wrapper_hover_content'; +import { DraggableWrapperHoverContent } from '../../../common/components/drag_and_drop/draggable_wrapper_hover_content'; /** * The name of a (draggable) field diff --git a/x-pack/plugins/siem/public/components/fields_browser/fields_pane.test.tsx b/x-pack/plugins/siem/public/timelines/components/fields_browser/fields_pane.test.tsx similarity index 94% rename from x-pack/plugins/siem/public/components/fields_browser/fields_pane.test.tsx rename to x-pack/plugins/siem/public/timelines/components/fields_browser/fields_pane.test.tsx index f3ec87a96d46b6..be77b62d2d0a4b 100644 --- a/x-pack/plugins/siem/public/components/fields_browser/fields_pane.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/fields_browser/fields_pane.test.tsx @@ -6,9 +6,9 @@ import React from 'react'; -import { mockBrowserFields } from '../../containers/source/mock'; -import { TestProviders } from '../../mock'; -import { useMountAppended } from '../../utils/use_mount_appended'; +import { mockBrowserFields } from '../../../common/containers/source/mock'; +import { TestProviders } from '../../../common/mock'; +import { useMountAppended } from '../../../common/utils/use_mount_appended'; import { FIELDS_PANE_WIDTH } from './helpers'; import { FieldsPane } from './fields_pane'; diff --git a/x-pack/plugins/siem/public/components/fields_browser/fields_pane.tsx b/x-pack/plugins/siem/public/timelines/components/fields_browser/fields_pane.tsx similarity index 95% rename from x-pack/plugins/siem/public/components/fields_browser/fields_pane.tsx rename to x-pack/plugins/siem/public/timelines/components/fields_browser/fields_pane.tsx index 354b2ae5e5eb83..9829a63101f823 100644 --- a/x-pack/plugins/siem/public/components/fields_browser/fields_pane.tsx +++ b/x-pack/plugins/siem/public/timelines/components/fields_browser/fields_pane.tsx @@ -8,8 +8,8 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; -import { BrowserFields } from '../../containers/source'; -import { ColumnHeaderOptions } from '../../store/timeline/model'; +import { BrowserFields } from '../../../common/containers/source'; +import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; import { Category } from './category'; import { FieldBrowserProps } from './types'; diff --git a/x-pack/plugins/siem/public/components/fields_browser/header.test.tsx b/x-pack/plugins/siem/public/timelines/components/fields_browser/header.test.tsx similarity index 98% rename from x-pack/plugins/siem/public/components/fields_browser/header.test.tsx rename to x-pack/plugins/siem/public/timelines/components/fields_browser/header.test.tsx index 2abc2fd1046e06..ca05d075e56168 100644 --- a/x-pack/plugins/siem/public/components/fields_browser/header.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/fields_browser/header.test.tsx @@ -6,8 +6,8 @@ import { mount } from 'enzyme'; import React from 'react'; -import { mockBrowserFields } from '../../containers/source/mock'; -import { TestProviders } from '../../mock'; +import { mockBrowserFields } from '../../../common/containers/source/mock'; +import { TestProviders } from '../../../common/mock'; import { defaultHeaders } from '../timeline/body/column_headers/default_headers'; import { Header } from './header'; diff --git a/x-pack/plugins/siem/public/components/fields_browser/header.tsx b/x-pack/plugins/siem/public/timelines/components/fields_browser/header.tsx similarity index 93% rename from x-pack/plugins/siem/public/components/fields_browser/header.tsx rename to x-pack/plugins/siem/public/timelines/components/fields_browser/header.tsx index ccf6ec67521b0b..1136b7c8d0dc42 100644 --- a/x-pack/plugins/siem/public/components/fields_browser/header.tsx +++ b/x-pack/plugins/siem/public/timelines/components/fields_browser/header.tsx @@ -15,10 +15,10 @@ import { import React, { useCallback } from 'react'; import styled from 'styled-components'; -import { BrowserFields } from '../../containers/source'; -import { signalsHeaders } from '../../pages/detection_engine/components/signals/default_config'; -import { alertsHeaders } from '../alerts_viewer/default_headers'; -import { defaultHeaders as eventsDefaultHeaders } from '../events_viewer/default_headers'; +import { BrowserFields } from '../../../common/containers/source'; +import { signalsHeaders } from '../../../alerts/components/signals/default_config'; +import { alertsHeaders } from '../../../common/components/alerts_viewer/default_headers'; +import { defaultHeaders as eventsDefaultHeaders } from '../../../common/components/events_viewer/default_headers'; import { defaultHeaders } from '../timeline/body/column_headers/default_headers'; import { OnUpdateColumns } from '../timeline/events'; import { useTimelineTypeContext } from '../timeline/timeline_context'; diff --git a/x-pack/plugins/siem/public/timelines/components/fields_browser/helpers.test.tsx b/x-pack/plugins/siem/public/timelines/components/fields_browser/helpers.test.tsx new file mode 100644 index 00000000000000..0e1b00dd9b8642 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/fields_browser/helpers.test.tsx @@ -0,0 +1,376 @@ +/* + * 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 { mockBrowserFields } from '../../../common/containers/source/mock'; + +import { + categoryHasFields, + createVirtualCategory, + getCategoryPaneCategoryClassName, + getFieldBrowserCategoryTitleClassName, + getFieldBrowserSearchInputClassName, + getFieldCount, + filterBrowserFieldsByFieldName, +} from './helpers'; +import { BrowserFields } from '../../../common/containers/source'; + +const timelineId = 'test'; + +describe('helpers', () => { + describe('getCategoryPaneCategoryClassName', () => { + test('it returns the expected class name', () => { + const categoryId = 'auditd'; + + expect(getCategoryPaneCategoryClassName({ categoryId, timelineId })).toEqual( + 'field-browser-category-pane-auditd-test' + ); + }); + }); + + describe('getFieldBrowserCategoryTitleClassName', () => { + test('it returns the expected class name', () => { + const categoryId = 'auditd'; + + expect(getFieldBrowserCategoryTitleClassName({ categoryId, timelineId })).toEqual( + 'field-browser-category-title-auditd-test' + ); + }); + }); + + describe('getFieldBrowserSearchInputClassName', () => { + test('it returns the expected class name', () => { + expect(getFieldBrowserSearchInputClassName(timelineId)).toEqual( + 'field-browser-search-input-test' + ); + }); + }); + + describe('categoryHasFields', () => { + test('it returns false if the category fields property is undefined', () => { + expect(categoryHasFields({})).toBe(false); + }); + + test('it returns false if the category fields property is empty', () => { + expect(categoryHasFields({ fields: {} })).toBe(false); + }); + + test('it returns true if the category has one field', () => { + expect( + categoryHasFields({ + fields: { + 'auditd.data.a0': { + aggregatable: true, + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a0', + searchable: true, + type: 'string', + }, + }, + }) + ).toBe(true); + }); + + test('it returns true if the category has multiple fields', () => { + expect( + categoryHasFields({ + fields: { + 'agent.ephemeral_id': { + aggregatable: true, + category: 'agent', + description: + 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', + example: '8a4f500f', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.ephemeral_id', + searchable: true, + type: 'string', + }, + 'agent.hostname': { + aggregatable: true, + category: 'agent', + description: null, + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.hostname', + searchable: true, + type: 'string', + }, + }, + }) + ).toBe(true); + }); + }); + + describe('getFieldCount', () => { + test('it returns 0 if the category fields property is undefined', () => { + expect(getFieldCount({})).toEqual(0); + }); + + test('it returns 0 if the category fields property is empty', () => { + expect(getFieldCount({ fields: {} })).toEqual(0); + }); + + test('it returns 1 if the category has one field', () => { + expect( + getFieldCount({ + fields: { + 'auditd.data.a0': { + aggregatable: true, + category: 'auditd', + description: null, + example: null, + format: '', + indexes: ['auditbeat'], + name: 'auditd.data.a0', + searchable: true, + type: 'string', + }, + }, + }) + ).toEqual(1); + }); + + test('it returns the correct count when category has multiple fields', () => { + expect( + getFieldCount({ + fields: { + 'agent.ephemeral_id': { + aggregatable: true, + category: 'agent', + description: + 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', + example: '8a4f500f', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.ephemeral_id', + searchable: true, + type: 'string', + }, + 'agent.hostname': { + aggregatable: true, + category: 'agent', + description: null, + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.hostname', + searchable: true, + type: 'string', + }, + }, + }) + ).toEqual(2); + }); + }); + + describe('filterBrowserFieldsByFieldName', () => { + test('it returns an empty collection when browserFields is empty', () => { + expect(filterBrowserFieldsByFieldName({ browserFields: {}, substring: '' })).toEqual({}); + }); + + test('it returns an empty collection when browserFields is empty and substring is non empty', () => { + expect( + filterBrowserFieldsByFieldName({ browserFields: {}, substring: 'nothing to match' }) + ).toEqual({}); + }); + + test('it returns an empty collection when browserFields is NOT empty and substring does not match any fields', () => { + expect( + filterBrowserFieldsByFieldName({ + browserFields: mockBrowserFields, + substring: 'nothing to match', + }) + ).toEqual({}); + }); + + test('it returns the original collection when browserFields is NOT empty and substring is empty', () => { + expect( + filterBrowserFieldsByFieldName({ + browserFields: mockBrowserFields, + substring: '', + }) + ).toEqual(mockBrowserFields); + }); + + test('it returns (only) non-empty categories, where each category contains only the fields matching the substring', () => { + const filtered: BrowserFields = { + agent: { + fields: { + 'agent.ephemeral_id': { + aggregatable: true, + category: 'agent', + description: + 'Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but `agent.id` does not.', + example: '8a4f500f', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.ephemeral_id', + searchable: true, + type: 'string', + }, + 'agent.id': { + aggregatable: true, + category: 'agent', + description: + 'Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.', + example: '8a4f500d', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.id', + searchable: true, + type: 'string', + }, + }, + }, + cloud: { + fields: { + 'cloud.account.id': { + aggregatable: true, + category: 'cloud', + description: + 'The cloud account or organization id used to identify different entities in a multi-tenant environment. Examples: AWS account id, Google Cloud ORG Id, or other unique identifier.', + example: '666777888999', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'cloud.account.id', + searchable: true, + type: 'string', + }, + }, + }, + container: { + fields: { + 'container.id': { + aggregatable: true, + category: 'container', + description: 'Unique container id.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'container.id', + searchable: true, + type: 'string', + }, + }, + }, + }; + + expect( + filterBrowserFieldsByFieldName({ + browserFields: mockBrowserFields, + substring: 'id', + }) + ).toEqual(filtered); + }); + }); + + describe('createVirtualCategory', () => { + test('it combines the specified fields into a virtual category when the input ONLY contains field names that contain dots (e.g. agent.hostname)', () => { + const expectedMatchingFields = { + fields: { + 'agent.hostname': { + aggregatable: true, + category: 'agent', + description: null, + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.hostname', + searchable: true, + type: 'string', + }, + 'client.domain': { + aggregatable: true, + category: 'client', + description: 'Client domain.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.domain', + searchable: true, + type: 'string', + }, + 'client.geo.country_iso_code': { + aggregatable: true, + category: 'client', + description: 'Country ISO code.', + example: 'CA', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.geo.country_iso_code', + searchable: true, + type: 'string', + }, + }, + }; + + const fieldIds = ['agent.hostname', 'client.domain', 'client.geo.country_iso_code']; + + expect( + createVirtualCategory({ + browserFields: mockBrowserFields, + fieldIds, + }) + ).toEqual(expectedMatchingFields); + }); + + test('it combines the specified fields into a virtual category when the input includes field names from the base category that do NOT contain dots (e.g. @timestamp)', () => { + const expectedMatchingFields = { + fields: { + 'agent.hostname': { + aggregatable: true, + category: 'agent', + description: null, + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'agent.hostname', + searchable: true, + type: 'string', + }, + '@timestamp': { + aggregatable: true, + category: 'base', + description: + 'Date/time when the event originated. For log events this is the date/time when the event was generated, and not when it was read. Required field for all events.', + example: '2016-05-23T08:05:34.853Z', + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: '@timestamp', + searchable: true, + type: 'date', + }, + 'client.domain': { + aggregatable: true, + category: 'client', + description: 'Client domain.', + example: null, + format: '', + indexes: ['auditbeat', 'filebeat', 'packetbeat'], + name: 'client.domain', + searchable: true, + type: 'string', + }, + }, + }; + + const fieldIds = ['agent.hostname', '@timestamp', 'client.domain']; + + expect( + createVirtualCategory({ + browserFields: mockBrowserFields, + fieldIds, + }) + ).toEqual(expectedMatchingFields); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/timelines/components/fields_browser/helpers.tsx b/x-pack/plugins/siem/public/timelines/components/fields_browser/helpers.tsx new file mode 100644 index 00000000000000..d176e68bc8414e --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/fields_browser/helpers.tsx @@ -0,0 +1,143 @@ +/* + * 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 { EuiLoadingSpinner } from '@elastic/eui'; +import { filter, get, pickBy } from 'lodash/fp'; +import styled from 'styled-components'; + +import { BrowserField, BrowserFields } from '../../../common/containers/source'; +import { + DEFAULT_CATEGORY_NAME, + defaultHeaders, +} from '../timeline/body/column_headers/default_headers'; + +export const LoadingSpinner = styled(EuiLoadingSpinner)` + cursor: pointer; + position: relative; + top: 3px; +`; + +LoadingSpinner.displayName = 'LoadingSpinner'; + +export const CATEGORY_PANE_WIDTH = 200; +export const DESCRIPTION_COLUMN_WIDTH = 300; +export const FIELD_COLUMN_WIDTH = 200; +export const FIELD_BROWSER_WIDTH = 900; +export const FIELD_BROWSER_HEIGHT = 300; +export const FIELDS_PANE_WIDTH = 670; +export const HEADER_HEIGHT = 40; +export const PANES_FLEX_GROUP_WIDTH = CATEGORY_PANE_WIDTH + FIELDS_PANE_WIDTH + 10; +export const SEARCH_INPUT_WIDTH = 850; +export const TABLE_HEIGHT = 260; +export const TYPE_COLUMN_WIDTH = 50; + +/** + * Returns the CSS class name for the title of a category shown in the left + * side field browser + */ +export const getCategoryPaneCategoryClassName = ({ + categoryId, + timelineId, +}: { + categoryId: string; + timelineId: string; +}): string => `field-browser-category-pane-${categoryId}-${timelineId}`; + +/** + * Returns the CSS class name for the title of a category shown in the right + * side of field browser + */ +export const getFieldBrowserCategoryTitleClassName = ({ + categoryId, + timelineId, +}: { + categoryId: string; + timelineId: string; +}): string => `field-browser-category-title-${categoryId}-${timelineId}`; + +/** Returns the class name for a field browser search input */ +export const getFieldBrowserSearchInputClassName = (timelineId: string): string => + `field-browser-search-input-${timelineId}`; + +/** Returns true if the specified category has at least one field */ +export const categoryHasFields = (category: Partial): boolean => + category.fields != null && Object.keys(category.fields).length > 0; + +/** Returns the count of fields in the specified category */ +export const getFieldCount = (category: Partial | undefined): number => + category != null && category.fields != null ? Object.keys(category.fields).length : 0; + +/** + * Filters the specified `BrowserFields` to return a new collection where every + * category contains at least one field name that matches the specified substring. + */ +export const filterBrowserFieldsByFieldName = ({ + browserFields, + substring, +}: { + browserFields: BrowserFields; + substring: string; +}): BrowserFields => { + const trimmedSubstring = substring.trim(); + + // filter each category such that it only contains fields with field names + // that contain the specified substring: + const filteredBrowserFields: BrowserFields = Object.keys(browserFields).reduce( + (filteredCategories, categoryId) => ({ + ...filteredCategories, + [categoryId]: { + ...browserFields[categoryId], + fields: filter( + f => f.name != null && f.name.includes(trimmedSubstring), + browserFields[categoryId].fields + ).reduce((filtered, field) => ({ ...filtered, [field.name!]: field }), {}), + }, + }), + {} + ); + + // only pick non-empty categories from the filtered browser fields + const nonEmptyCategories: BrowserFields = pickBy( + category => categoryHasFields(category), + filteredBrowserFields + ); + + return nonEmptyCategories; +}; + +/** + * Returns a "virtual" category (e.g. default ECS) from the specified fieldIds + */ +export const createVirtualCategory = ({ + browserFields, + fieldIds, +}: { + browserFields: BrowserFields; + fieldIds: string[]; +}): Partial => ({ + fields: fieldIds.reduce>>>((fields, fieldId) => { + const splitId = fieldId.split('.'); // source.geo.city_name -> [source, geo, city_name] + + return { + ...fields, + [fieldId]: { + ...get([splitId.length > 1 ? splitId[0] : 'base', 'fields', fieldId], browserFields), + name: fieldId, + }, + }; + }, {}), +}); + +/** Merges the specified browser fields with the default category (i.e. `default ECS`) */ +export const mergeBrowserFieldsWithDefaultCategory = ( + browserFields: BrowserFields +): BrowserFields => ({ + ...browserFields, + [DEFAULT_CATEGORY_NAME]: createVirtualCategory({ + browserFields, + fieldIds: defaultHeaders.map(header => header.id), + }), +}); diff --git a/x-pack/plugins/siem/public/timelines/components/fields_browser/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/fields_browser/index.test.tsx new file mode 100644 index 00000000000000..798fa53e607ed2 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/fields_browser/index.test.tsx @@ -0,0 +1,275 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; +import { ActionCreator } from 'typescript-fsa'; + +import { mockBrowserFields } from '../../../common/containers/source/mock'; +import { TestProviders } from '../../../common/mock'; +import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; + +import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from './helpers'; + +import { StatefulFieldsBrowserComponent } from '.'; + +// Suppress warnings about "react-beautiful-dnd" until we migrate to @testing-library/react +/* eslint-disable no-console */ +const originalError = console.error; +const originalWarn = console.warn; +beforeAll(() => { + console.warn = jest.fn(); + console.error = jest.fn(); +}); +afterAll(() => { + console.error = originalError; + console.warn = originalWarn; +}); + +const removeColumnMock = (jest.fn() as unknown) as ActionCreator<{ + id: string; + columnId: string; +}>; + +const upsertColumnMock = (jest.fn() as unknown) as ActionCreator<{ + column: ColumnHeaderOptions; + id: string; + index: number; +}>; + +describe('StatefulFieldsBrowser', () => { + const timelineId = 'test'; + + test('it renders the Fields button, which displays the fields browser on click', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="show-field-browser"]') + .first() + .text() + ).toEqual('Columns'); + }); + + describe('toggleShow', () => { + test('it does NOT render the fields browser until the Fields button is clicked', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="fields-browser-container"]').exists()).toBe(false); + }); + + test('it renders the fields browser when the Fields button is clicked', () => { + const wrapper = mount( + + + + ); + + wrapper + .find('[data-test-subj="show-field-browser"]') + .first() + .simulate('click'); + + expect(wrapper.find('[data-test-subj="fields-browser-container"]').exists()).toBe(true); + }); + }); + + describe('updateSelectedCategoryId', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + test('it updates the selectedCategoryId state, which makes the category bold, when the user clicks a category name in the left hand side of the field browser', () => { + const wrapper = mount( + + + + ); + + wrapper + .find('[data-test-subj="show-field-browser"]') + .first() + .simulate('click'); + + wrapper + .find(`.field-browser-category-pane-auditd-${timelineId}`) + .first() + .simulate('click'); + + wrapper.update(); + expect( + wrapper.find(`.field-browser-category-pane-auditd-${timelineId}`).first() + ).toHaveStyleRule('font-weight', 'bold', { modifier: '.euiText' }); + }); + + test('it updates the selectedCategoryId state according to most fields returned', () => { + const wrapper = mount( + + + + ); + + wrapper + .find('[data-test-subj="show-field-browser"]') + .first() + .simulate('click'); + expect( + wrapper.find(`.field-browser-category-pane-cloud-${timelineId}`).first() + ).toHaveStyleRule('font-weight', 'normal', { modifier: '.euiText' }); + wrapper + .find('[data-test-subj="field-search"]') + .last() + .simulate('change', { target: { value: 'cloud' } }); + + jest.runOnlyPendingTimers(); + wrapper.update(); + expect( + wrapper.find(`.field-browser-category-pane-cloud-${timelineId}`).first() + ).toHaveStyleRule('font-weight', 'bold', { modifier: '.euiText' }); + }); + }); + + test('it renders the Fields Browser button as a settings gear when the isEventViewer prop is true', () => { + const isEventViewer = true; + + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="show-field-browser-gear"]') + .first() + .exists() + ).toBe(true); + }); + + test('it does NOT render the Fields Browser button as a settings gear when the isEventViewer prop is false', () => { + const isEventViewer = false; + + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="show-field-browser-gear"]') + .first() + .exists() + ).toBe(false); + }); + + test('it does NOT render the default Fields Browser button when the isEventViewer prop is true', () => { + const isEventViewer = true; + + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="show-field-browser"]') + .first() + .exists() + ).toBe(false); + }); +}); diff --git a/x-pack/plugins/siem/public/timelines/components/fields_browser/index.tsx b/x-pack/plugins/siem/public/timelines/components/fields_browser/index.tsx new file mode 100644 index 00000000000000..11c44cce89956d --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/fields_browser/index.tsx @@ -0,0 +1,211 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButtonEmpty, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; +import { noop } from 'lodash/fp'; +import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import styled from 'styled-components'; + +import { BrowserFields } from '../../../common/containers/source'; +import { timelineActions } from '../../store/timeline'; +import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; +import { DEFAULT_CATEGORY_NAME } from '../timeline/body/column_headers/default_headers'; +import { FieldsBrowser } from './field_browser'; +import { filterBrowserFieldsByFieldName, mergeBrowserFieldsWithDefaultCategory } from './helpers'; +import * as i18n from './translations'; +import { FieldBrowserProps } from './types'; + +const fieldsButtonClassName = 'fields-button'; + +/** wait this many ms after the user completes typing before applying the filter input */ +export const INPUT_TIMEOUT = 250; + +const FieldsBrowserButtonContainer = styled.div` + position: relative; +`; + +FieldsBrowserButtonContainer.displayName = 'FieldsBrowserButtonContainer'; + +/** + * Manages the state of the field browser + */ +export const StatefulFieldsBrowserComponent = React.memo( + ({ + columnHeaders, + browserFields, + height, + isEventViewer = false, + onFieldSelected, + onUpdateColumns, + timelineId, + toggleColumn, + width, + }) => { + /** tracks the latest timeout id from `setTimeout`*/ + const inputTimeoutId = useRef(0); + + /** all field names shown in the field browser must contain this string (when specified) */ + const [filterInput, setFilterInput] = useState(''); + /** all fields in this collection have field names that match the filterInput */ + const [filteredBrowserFields, setFilteredBrowserFields] = useState(null); + /** when true, show a spinner in the input to indicate the field browser is searching for matching field names */ + const [isSearching, setIsSearching] = useState(false); + /** this category will be displayed in the right-hand pane of the field browser */ + const [selectedCategoryId, setSelectedCategoryId] = useState(DEFAULT_CATEGORY_NAME); + /** show the field browser */ + const [show, setShow] = useState(false); + useEffect(() => { + return () => { + if (inputTimeoutId.current !== 0) { + // ⚠️ mutation: cancel any remaining timers and zero-out the timer id: + clearTimeout(inputTimeoutId.current); + inputTimeoutId.current = 0; + } + }; + }, []); + + /** Shows / hides the field browser */ + const toggleShow = useCallback(() => { + setShow(!show); + }, [show]); + + /** Invoked when the user types in the filter input */ + const updateFilter = useCallback( + (newFilterInput: string) => { + setFilterInput(newFilterInput); + setIsSearching(true); + if (inputTimeoutId.current !== 0) { + clearTimeout(inputTimeoutId.current); // ⚠️ mutation: cancel any previous timers + } + // ⚠️ mutation: schedule a new timer that will apply the filter when it fires: + inputTimeoutId.current = window.setTimeout(() => { + const newFilteredBrowserFields = filterBrowserFieldsByFieldName({ + browserFields: mergeBrowserFieldsWithDefaultCategory(browserFields), + substring: newFilterInput, + }); + setFilteredBrowserFields(newFilteredBrowserFields); + setIsSearching(false); + + const newSelectedCategoryId = + newFilterInput === '' || Object.keys(newFilteredBrowserFields).length === 0 + ? DEFAULT_CATEGORY_NAME + : Object.keys(newFilteredBrowserFields) + .sort() + .reduce( + (selected, category) => + newFilteredBrowserFields[category].fields != null && + newFilteredBrowserFields[selected].fields != null && + Object.keys(newFilteredBrowserFields[category].fields!).length > + Object.keys(newFilteredBrowserFields[selected].fields!).length + ? category + : selected, + Object.keys(newFilteredBrowserFields)[0] + ); + setSelectedCategoryId(newSelectedCategoryId); + }, INPUT_TIMEOUT); + }, + [browserFields, filterInput, inputTimeoutId.current] + ); + + /** + * Invoked when the user clicks a category name in the left-hand side of + * the field browser + */ + const updateSelectedCategoryId = useCallback((categoryId: string) => { + setSelectedCategoryId(categoryId); + }, []); + + /** + * Invoked when the user clicks on the context menu to view a category's + * columns in the timeline, this function dispatches the action that + * causes the timeline display those columns. + */ + const updateColumnsAndSelectCategoryId = useCallback((columns: ColumnHeaderOptions[]) => { + onUpdateColumns(columns); // show the category columns in the timeline + }, []); + + /** Invoked when the field browser should be hidden */ + const hideFieldBrowser = useCallback(() => { + setFilterInput(''); + setFilterInput(''); + setFilteredBrowserFields(null); + setIsSearching(false); + setSelectedCategoryId(DEFAULT_CATEGORY_NAME); + setShow(false); + }, []); + // only merge in the default category if the field browser is visible + const browserFieldsWithDefaultCategory = useMemo( + () => (show ? mergeBrowserFieldsWithDefaultCategory(browserFields) : {}), + [show, browserFields] + ); + + return ( + <> + + + {isEventViewer ? ( + + ) : ( + + {i18n.FIELDS} + + )} + + + {show && ( + + )} + + + ); + } +); + +const mapDispatchToProps = { + removeColumn: timelineActions.removeColumn, + upsertColumn: timelineActions.upsertColumn, +}; + +const connector = connect(null, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const StatefulFieldsBrowser = connector(React.memo(StatefulFieldsBrowserComponent)); diff --git a/x-pack/plugins/siem/public/components/fields_browser/translations.ts b/x-pack/plugins/siem/public/timelines/components/fields_browser/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/fields_browser/translations.ts rename to x-pack/plugins/siem/public/timelines/components/fields_browser/translations.ts diff --git a/x-pack/plugins/siem/public/timelines/components/fields_browser/types.ts b/x-pack/plugins/siem/public/timelines/components/fields_browser/types.ts new file mode 100644 index 00000000000000..2b9889ec13e794 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/fields_browser/types.ts @@ -0,0 +1,37 @@ +/* + * 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 { BrowserFields } from '../../../common/containers/source'; +import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; +import { OnUpdateColumns } from '../timeline/events'; + +export type OnFieldSelected = (fieldId: string) => void; +export type OnHideFieldBrowser = () => void; + +export interface FieldBrowserProps { + /** The timeline's current column headers */ + columnHeaders: ColumnHeaderOptions[]; + /** A map of categoryId -> metadata about the fields in that category */ + browserFields: BrowserFields; + /** The height of the field browser */ + height: number; + /** When true, this Fields Browser is being used as an "events viewer" */ + isEventViewer?: boolean; + /** + * Overrides the default behavior of the `FieldBrowser` to enable + * "selection" mode, where a field is selected by clicking a button + * instead of dragging it to the timeline + */ + onFieldSelected?: OnFieldSelected; + /** Invoked when a user chooses to view a new set of columns in the timeline */ + onUpdateColumns: OnUpdateColumns; + /** The timeline associated with this field browser */ + timelineId: string; + /** Adds or removes a column to / from the timeline */ + toggleColumn: (column: ColumnHeaderOptions) => void; + /** The width of the field browser */ + width: number; +} diff --git a/x-pack/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/flyout/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/flyout/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/flyout/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/timelines/components/flyout/button/index.tsx b/x-pack/plugins/siem/public/timelines/components/flyout/button/index.tsx new file mode 100644 index 00000000000000..a80b8de4351673 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/flyout/button/index.tsx @@ -0,0 +1,148 @@ +/* + * 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 { noop } from 'lodash/fp'; +import { EuiButton, EuiNotificationBadge, EuiPanel } from '@elastic/eui'; +import { rgba } from 'polished'; +import React, { useMemo } from 'react'; +import styled from 'styled-components'; + +import { WithSource } from '../../../../common/containers/source'; +import { IS_DRAGGING_CLASS_NAME } from '../../../../common/components/drag_and_drop/helpers'; +import { DataProvider } from '../../timeline/data_providers/data_provider'; +import { flattenIntoAndGroups } from '../../timeline/data_providers/helpers'; +import { DataProviders } from '../../timeline/data_providers'; +import * as i18n from './translations'; + +export const FLYOUT_BUTTON_CLASS_NAME = 'timeline-flyout-button'; + +export const getBadgeCount = (dataProviders: DataProvider[]): number => + flattenIntoAndGroups(dataProviders).reduce((total, group) => total + group.length, 0); + +const SHOW_HIDE_TRANSLATE_X = 497; // px + +const Container = styled.div` + padding-top: 8px; + position: fixed; + right: 0px; + top: 40%; + transform: translateX(${SHOW_HIDE_TRANSLATE_X}px); + user-select: none; + width: 500px; + z-index: ${({ theme }) => theme.eui.euiZLevel9}; + + .${IS_DRAGGING_CLASS_NAME} & { + transform: none; + } + + .${FLYOUT_BUTTON_CLASS_NAME} { + border-radius: 4px 4px 0 0; + box-shadow: none; + height: 46px; + } + + .${IS_DRAGGING_CLASS_NAME} & .${FLYOUT_BUTTON_CLASS_NAME} { + color: ${({ theme }) => theme.eui.euiColorSuccess}; + background: ${({ theme }) => rgba(theme.eui.euiColorSuccess, 0.2)} !important; + border: 1px solid ${({ theme }) => theme.eui.euiColorSuccess}; + border-bottom: none; + text-decoration: none; + } +`; + +Container.displayName = 'Container'; + +const BadgeButtonContainer = styled.div` + align-items: flex-start; + display: flex; + flex-direction: row; + left: -87px; + position: absolute; + top: 34px; + transform: rotate(-90deg); +`; + +BadgeButtonContainer.displayName = 'BadgeButtonContainer'; + +const DataProvidersPanel = styled(EuiPanel)` + border-radius: 0; + padding: 0 4px 0 4px; + user-select: none; + z-index: ${({ theme }) => theme.eui.euiZLevel9}; +`; + +interface FlyoutButtonProps { + dataProviders: DataProvider[]; + onOpen: () => void; + show: boolean; + timelineId: string; +} + +export const FlyoutButton = React.memo( + ({ onOpen, show, dataProviders, timelineId }) => { + const badgeCount = useMemo(() => getBadgeCount(dataProviders), [dataProviders]); + + if (!show) { + return null; + } + + return ( + + + + {i18n.FLYOUT_BUTTON} + + + {badgeCount} + + + + + {({ browserFields }) => ( + + )} + + + + ); + }, + (prevProps, nextProps) => + prevProps.show === nextProps.show && + prevProps.dataProviders === nextProps.dataProviders && + prevProps.timelineId === nextProps.timelineId +); + +FlyoutButton.displayName = 'FlyoutButton'; diff --git a/x-pack/plugins/siem/public/components/flyout/button/translations.ts b/x-pack/plugins/siem/public/timelines/components/flyout/button/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/flyout/button/translations.ts rename to x-pack/plugins/siem/public/timelines/components/flyout/button/translations.ts diff --git a/x-pack/plugins/siem/public/timelines/components/flyout/header/index.tsx b/x-pack/plugins/siem/public/timelines/components/flyout/header/index.tsx new file mode 100644 index 00000000000000..b332260597f223 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/flyout/header/index.tsx @@ -0,0 +1,143 @@ +/* + * 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, { useCallback } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import { Dispatch } from 'redux'; + +import { isEmpty, get } from 'lodash/fp'; +import { History } from '../../../../common/lib/history'; +import { Note } from '../../../../common/lib/note'; +import { appSelectors, inputsModel, inputsSelectors, State } from '../../../../common/store'; +import { defaultHeaders } from '../../timeline/body/column_headers/default_headers'; +import { Properties } from '../../timeline/properties'; +import { appActions } from '../../../../common/store/app'; +import { inputsActions } from '../../../../common/store/inputs'; +import { timelineActions, timelineSelectors } from '../../../store/timeline'; +import { TimelineModel } from '../../../../timelines/store/timeline/model'; +import { timelineDefaults } from '../../../../timelines/store/timeline/defaults'; +import { InputsModelId } from '../../../../common/store/inputs/constants'; + +interface OwnProps { + timelineId: string; + usersViewing: string[]; +} + +type Props = OwnProps & PropsFromRedux; + +const StatefulFlyoutHeader = React.memo( + ({ + associateNote, + createTimeline, + description, + isFavorite, + isDataInTimeline, + isDatepickerLocked, + title, + noteIds, + notesById, + timelineId, + toggleLock, + updateDescription, + updateIsFavorite, + updateNote, + updateTitle, + usersViewing, + }) => { + const getNotesByIds = useCallback( + (noteIdsVar: string[]): Note[] => appSelectors.getNotes(notesById, noteIdsVar), + [notesById] + ); + return ( + + ); + } +); + +StatefulFlyoutHeader.displayName = 'StatefulFlyoutHeader'; + +const emptyHistory: History[] = []; // stable reference + +const emptyNotesId: string[] = []; // stable reference + +const makeMapStateToProps = () => { + const getTimeline = timelineSelectors.getTimelineByIdSelector(); + const getNotesByIds = appSelectors.notesByIdsSelector(); + const getGlobalInput = inputsSelectors.globalSelector(); + const mapStateToProps = (state: State, { timelineId }: OwnProps) => { + const timeline: TimelineModel = getTimeline(state, timelineId) ?? timelineDefaults; + const globalInput: inputsModel.InputsRange = getGlobalInput(state); + const { + dataProviders, + description = '', + isFavorite = false, + kqlQuery, + title = '', + noteIds = emptyNotesId, + } = timeline; + + const history = emptyHistory; // TODO: get history from store via selector + + return { + description, + notesById: getNotesByIds(state), + history, + isDataInTimeline: + !isEmpty(dataProviders) || !isEmpty(get('filterQuery.kuery.expression', kqlQuery)), + isFavorite, + isDatepickerLocked: globalInput.linkTo.includes('timeline'), + noteIds, + title, + }; + }; + return mapStateToProps; +}; + +const mapDispatchToProps = (dispatch: Dispatch, { timelineId }: OwnProps) => ({ + associateNote: (noteId: string) => dispatch(timelineActions.addNote({ id: timelineId, noteId })), + createTimeline: ({ id, show }: { id: string; show?: boolean }) => + dispatch( + timelineActions.createTimeline({ + id, + columns: defaultHeaders, + show, + }) + ), + updateDescription: ({ id, description }: { id: string; description: string }) => + dispatch(timelineActions.updateDescription({ id, description })), + updateIsFavorite: ({ id, isFavorite }: { id: string; isFavorite: boolean }) => + dispatch(timelineActions.updateIsFavorite({ id, isFavorite })), + updateIsLive: ({ id, isLive }: { id: string; isLive: boolean }) => + dispatch(timelineActions.updateIsLive({ id, isLive })), + updateNote: (note: Note) => dispatch(appActions.updateNote({ note })), + updateTitle: ({ id, title }: { id: string; title: string }) => + dispatch(timelineActions.updateTitle({ id, title })), + toggleLock: ({ linkToId }: { linkToId: InputsModelId }) => + dispatch(inputsActions.toggleTimelineLinkTo({ linkToId })), +}); + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const FlyoutHeader = connector(StatefulFlyoutHeader); diff --git a/x-pack/plugins/siem/public/components/flyout/header_with_close_button/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/flyout/header_with_close_button/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/flyout/header_with_close_button/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/flyout/header_with_close_button/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/timelines/components/flyout/header_with_close_button/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/flyout/header_with_close_button/index.test.tsx new file mode 100644 index 00000000000000..57fd61561c65bc --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/flyout/header_with_close_button/index.test.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mount, shallow } from 'enzyme'; +import React from 'react'; + +import { TestProviders } from '../../../../common/mock'; +import { FlyoutHeaderWithCloseButton } from '.'; + +describe('FlyoutHeaderWithCloseButton', () => { + test('renders correctly against snapshot', () => { + const EmptyComponent = shallow( + + + + ); + expect(EmptyComponent.find('FlyoutHeaderWithCloseButton')).toMatchSnapshot(); + }); + + test('it should invoke onClose when the close button is clicked', () => { + const closeMock = jest.fn(); + const wrapper = mount( + + + + ); + wrapper + .find('[data-test-subj="close-timeline"] button') + .first() + .simulate('click'); + + expect(closeMock).toBeCalled(); + }); +}); diff --git a/x-pack/plugins/siem/public/components/flyout/header_with_close_button/index.tsx b/x-pack/plugins/siem/public/timelines/components/flyout/header_with_close_button/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/flyout/header_with_close_button/index.tsx rename to x-pack/plugins/siem/public/timelines/components/flyout/header_with_close_button/index.tsx diff --git a/x-pack/plugins/siem/public/components/flyout/header_with_close_button/translations.ts b/x-pack/plugins/siem/public/timelines/components/flyout/header_with_close_button/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/flyout/header_with_close_button/translations.ts rename to x-pack/plugins/siem/public/timelines/components/flyout/header_with_close_button/translations.ts diff --git a/x-pack/plugins/siem/public/timelines/components/flyout/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/flyout/index.test.tsx new file mode 100644 index 00000000000000..b73f2f943bb0ae --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/flyout/index.test.tsx @@ -0,0 +1,267 @@ +/* + * 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 { mount, shallow } from 'enzyme'; +import { set } from 'lodash/fp'; +import React from 'react'; +import { ActionCreator } from 'typescript-fsa'; + +import { + apolloClientObservable, + mockGlobalState, + TestProviders, + SUB_PLUGINS_REDUCER, +} from '../../../common/mock'; +import { createStore, State } from '../../../common/store'; +import { mockDataProviders } from '../timeline/data_providers/mock/mock_data_providers'; + +import { Flyout, FlyoutComponent } from '.'; +import { FlyoutButton } from './button'; + +jest.mock('../timeline', () => ({ + // eslint-disable-next-line react/display-name + StatefulTimeline: () =>
, +})); + +const testFlyoutHeight = 980; +const usersViewing = ['elastic']; + +describe('Flyout', () => { + const state: State = mockGlobalState; + + describe('rendering', () => { + test('it renders correctly against snapshot', () => { + const wrapper = shallow( + + + + ); + expect(wrapper.find('Flyout')).toMatchSnapshot(); + }); + + test('it renders the default flyout state as a button', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="flyout-button-not-ready-to-drop"]') + .first() + .text() + ).toContain('Timeline'); + }); + + test('it does NOT render the fly out button when its state is set to flyout is true', () => { + const stateShowIsTrue = set('timeline.timelineById.test.show', true, state); + const storeShowIsTrue = createStore( + stateShowIsTrue, + SUB_PLUGINS_REDUCER, + apolloClientObservable + ); + + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="flyout-button-not-ready-to-drop"]').exists()).toEqual( + false + ); + }); + + test('it does render the data providers badge when the number is greater than 0', () => { + const stateWithDataProviders = set( + 'timeline.timelineById.test.dataProviders', + mockDataProviders, + state + ); + const storeWithDataProviders = createStore( + stateWithDataProviders, + SUB_PLUGINS_REDUCER, + apolloClientObservable + ); + + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="badge"]').exists()).toEqual(true); + }); + + test('it renders the correct number of data providers badge when the number is greater than 0', () => { + const stateWithDataProviders = set( + 'timeline.timelineById.test.dataProviders', + mockDataProviders, + state + ); + const storeWithDataProviders = createStore( + stateWithDataProviders, + SUB_PLUGINS_REDUCER, + apolloClientObservable + ); + + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="badge"]') + .first() + .text() + ).toContain('10'); + }); + + test('it hides the data providers badge when the timeline does NOT have data providers', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="badge"]') + .first() + .props().style!.visibility + ).toEqual('hidden'); + }); + + test('it does NOT hide the data providers badge when the timeline has data providers', () => { + const stateWithDataProviders = set( + 'timeline.timelineById.test.dataProviders', + mockDataProviders, + state + ); + const storeWithDataProviders = createStore( + stateWithDataProviders, + SUB_PLUGINS_REDUCER, + apolloClientObservable + ); + + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="badge"]') + .first() + .props().style!.visibility + ).toEqual('inherit'); + }); + + test('should call the onOpen when the mouse is clicked for rendering', () => { + const showTimeline = (jest.fn() as unknown) as ActionCreator<{ id: string; show: boolean }>; + const wrapper = mount( + + + + ); + + wrapper + .find('[data-test-subj="flyoutOverlay"]') + .first() + .simulate('click'); + + expect(showTimeline).toBeCalled(); + }); + }); + + describe('showFlyoutButton', () => { + test('should show the flyout button when show is true', () => { + const openMock = jest.fn(); + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="flyout-button-not-ready-to-drop"]').exists()).toEqual( + true + ); + }); + + test('should NOT show the flyout button when show is false', () => { + const openMock = jest.fn(); + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="flyout-button-not-ready-to-drop"]').exists()).toEqual( + false + ); + }); + + test('should return the flyout button with text', () => { + const openMock = jest.fn(); + const wrapper = mount( + + + + ); + expect( + wrapper + .find('[data-test-subj="flyout-button-not-ready-to-drop"]') + .first() + .text() + ).toContain('Timeline'); + }); + + test('should call the onOpen when it is clicked', () => { + const openMock = jest.fn(); + const wrapper = mount( + + + + ); + wrapper + .find('[data-test-subj="flyoutOverlay"]') + .first() + .simulate('click'); + + expect(openMock).toBeCalled(); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/timelines/components/flyout/index.tsx b/x-pack/plugins/siem/public/timelines/components/flyout/index.tsx new file mode 100644 index 00000000000000..c556c2d53f7c26 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/flyout/index.tsx @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiBadge } from '@elastic/eui'; +import React, { useCallback } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import styled from 'styled-components'; + +import { State } from '../../../common/store'; +import { DataProvider } from '../timeline/data_providers/data_provider'; +import { FlyoutButton } from './button'; +import { Pane } from './pane'; +import { timelineActions, timelineSelectors } from '../../store/timeline'; +import { DEFAULT_TIMELINE_WIDTH } from '../timeline/body/constants'; +import { StatefulTimeline } from '../timeline'; +import { TimelineById } from '../../store/timeline/types'; + +export const Badge = (styled(EuiBadge)` + position: absolute; + padding-left: 4px; + padding-right: 4px; + right: 0%; + top: 0%; + border-bottom-left-radius: 5px; +` as unknown) as typeof EuiBadge; + +Badge.displayName = 'Badge'; + +const Visible = styled.div<{ show?: boolean }>` + visibility: ${({ show }) => (show ? 'visible' : 'hidden')}; +`; + +Visible.displayName = 'Visible'; + +interface OwnProps { + flyoutHeight: number; + timelineId: string; + usersViewing: string[]; +} + +type Props = OwnProps & ProsFromRedux; + +export const FlyoutComponent = React.memo( + ({ dataProviders, flyoutHeight, show, showTimeline, timelineId, usersViewing, width }) => { + const handleClose = useCallback(() => showTimeline({ id: timelineId, show: false }), [ + showTimeline, + timelineId, + ]); + const handleOpen = useCallback(() => showTimeline({ id: timelineId, show: true }), [ + showTimeline, + timelineId, + ]); + + return ( + <> + + + + + + + + ); + } +); + +FlyoutComponent.displayName = 'FlyoutComponent'; + +const DEFAULT_DATA_PROVIDERS: DataProvider[] = []; +const DEFAULT_TIMELINE_BY_ID = {}; + +const mapStateToProps = (state: State, { timelineId }: OwnProps) => { + const timelineById: TimelineById = + timelineSelectors.timelineByIdSelector(state) ?? DEFAULT_TIMELINE_BY_ID; + /* + In case timelineById[timelineId]?.dataProviders is an empty array it will cause unnecessary rerender + of StatefulTimeline which can be expensive, so to avoid that return DEFAULT_DATA_PROVIDERS + */ + const dataProviders = timelineById[timelineId]?.dataProviders.length + ? timelineById[timelineId]?.dataProviders + : DEFAULT_DATA_PROVIDERS; + const show = timelineById[timelineId]?.show ?? false; + const width = timelineById[timelineId]?.width ?? DEFAULT_TIMELINE_WIDTH; + + return { dataProviders, show, width }; +}; + +const mapDispatchToProps = { + showTimeline: timelineActions.showTimeline, +}; + +const connector = connect(mapStateToProps, mapDispatchToProps); + +type ProsFromRedux = ConnectedProps; + +export const Flyout = connector(FlyoutComponent); + +Flyout.displayName = 'Flyout'; diff --git a/x-pack/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/flyout/pane/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/flyout/pane/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/flyout/pane/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/timelines/components/flyout/pane/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/flyout/pane/index.test.tsx new file mode 100644 index 00000000000000..29606d7685d977 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/flyout/pane/index.test.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mount, shallow } from 'enzyme'; +import React from 'react'; + +import { TestProviders } from '../../../../common/mock'; +import { Pane } from '.'; + +const testFlyoutHeight = 980; +const testWidth = 640; + +describe('Pane', () => { + test('renders correctly against snapshot', () => { + const EmptyComponent = shallow( + + + {'I am a child of flyout'} + + + ); + expect(EmptyComponent.find('Pane')).toMatchSnapshot(); + }); + + test('it should NOT let the flyout expand to take up the full width of the element that contains it', () => { + const wrapper = mount( + + + {'I am a child of flyout'} + + + ); + + expect(wrapper.find('Resizable').get(0).props.maxWidth).toEqual('95vw'); + }); + + test('it should render a resize handle', () => { + const wrapper = mount( + + + {'I am a child of flyout'} + + + ); + + expect( + wrapper + .find('[data-test-subj="flyout-resize-handle"]') + .first() + .exists() + ).toEqual(true); + }); + + test('it should render children', () => { + const wrapper = mount( + + + {'I am a mock body'} + + + ); + expect(wrapper.first().text()).toContain('I am a mock body'); + }); +}); diff --git a/x-pack/plugins/siem/public/timelines/components/flyout/pane/index.tsx b/x-pack/plugins/siem/public/timelines/components/flyout/pane/index.tsx new file mode 100644 index 00000000000000..33aca80b940fe3 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/flyout/pane/index.tsx @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlyout } from '@elastic/eui'; +import React, { useCallback, useMemo } from 'react'; +import { useDispatch } from 'react-redux'; +import styled from 'styled-components'; +import { Resizable, ResizeCallback } from 're-resizable'; + +import { TimelineResizeHandle } from './timeline_resize_handle'; +import { EventDetailsWidthProvider } from '../../../../common/components/events_viewer/event_details_width_context'; + +import * as i18n from './translations'; +import { timelineActions } from '../../../store/timeline'; + +const minWidthPixels = 550; // do not allow the flyout to shrink below this width (pixels) +const maxWidthPercent = 95; // do not allow the flyout to grow past this percentage of the view +interface FlyoutPaneComponentProps { + children: React.ReactNode; + flyoutHeight: number; + onClose: () => void; + timelineId: string; + width: number; +} + +const EuiFlyoutContainer = styled.div` + .timeline-flyout { + min-width: 150px; + width: auto; + } +`; + +const StyledResizable = styled(Resizable)` + display: flex; + flex-direction: column; +`; + +const RESIZABLE_ENABLE = { left: true }; + +const FlyoutPaneComponent: React.FC = ({ + children, + flyoutHeight, + onClose, + timelineId, + width, +}) => { + const dispatch = useDispatch(); + + const onResizeStop: ResizeCallback = useCallback( + (e, direction, ref, delta) => { + const bodyClientWidthPixels = document.body.clientWidth; + + if (delta.width) { + dispatch( + timelineActions.applyDeltaToWidth({ + bodyClientWidthPixels, + delta: -delta.width, + id: timelineId, + maxWidthPercent, + minWidthPixels, + }) + ); + } + }, + [dispatch] + ); + const resizableDefaultSize = useMemo( + () => ({ + width, + height: '100%', + }), + [] + ); + const resizableHandleComponent = useMemo( + () => ({ + left: , + }), + [flyoutHeight] + ); + + return ( + + + + {children} + + + + ); +}; + +export const Pane = React.memo(FlyoutPaneComponent); + +Pane.displayName = 'Pane'; diff --git a/x-pack/plugins/siem/public/components/flyout/pane/timeline_resize_handle.tsx b/x-pack/plugins/siem/public/timelines/components/flyout/pane/timeline_resize_handle.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/flyout/pane/timeline_resize_handle.tsx rename to x-pack/plugins/siem/public/timelines/components/flyout/pane/timeline_resize_handle.tsx diff --git a/x-pack/plugins/siem/public/components/flyout/pane/translations.ts b/x-pack/plugins/siem/public/timelines/components/flyout/pane/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/flyout/pane/translations.ts rename to x-pack/plugins/siem/public/timelines/components/flyout/pane/translations.ts diff --git a/x-pack/plugins/siem/public/timelines/components/formatted_duration/helpers.test.ts b/x-pack/plugins/siem/public/timelines/components/formatted_duration/helpers.test.ts new file mode 100644 index 00000000000000..dcf77f06defe3e --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/formatted_duration/helpers.test.ts @@ -0,0 +1,401 @@ +/* + * 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 { getEmptyValue } from '../../../common/components/empty_value'; +import { + getFormattedDurationString, + getHumanizedDuration, + ONE_DAY, + ONE_HOUR, + ONE_MILLISECOND_AS_NANOSECONDS, + ONE_MINUTE, + ONE_MONTH, + ONE_SECOND, + ONE_YEAR, +} from './helpers'; +import * as i18n from './translations'; + +describe('FormattedDurationHelpers', () => { + describe('#getFormattedDurationString', () => { + test('it returns a placeholder when the input is undefined', () => { + expect(getFormattedDurationString(undefined)).toEqual(getEmptyValue()); + }); + + test('it returns a placeholder when the input is null', () => { + expect(getFormattedDurationString(null)).toEqual(getEmptyValue()); + }); + + test('it echos back the input as a string when the input is not a number', () => { + expect(getFormattedDurationString('invalid duration')).toEqual('invalid duration'); + }); + + test('it returns the original input (with no formatting) when the input is negative', () => { + expect(getFormattedDurationString(-1)).toEqual('-1'); + }); + + test('it returns the duration formatted as 0 nanoseconds when the input is 0 nanoseconds', () => { + expect(getFormattedDurationString(0)).toEqual('0ns'); + }); + + test('it returns 1 nanosecond when the input is 1 nanosecond', () => { + expect(getFormattedDurationString(1)).toEqual('1ns'); + }); + + test('it returns 1000 nanoseconds when the input is 1000 nanoseconds', () => { + expect(getFormattedDurationString(1000)).toEqual('1000ns'); + }); + + test('it returns 1000 nanoseconds when the input is a string ("1000") instead of a number', () => { + expect(getFormattedDurationString('1000')).toEqual('1000ns'); + }); + + test('it returns the largest value that would be represented as nanoseconds when the input is 1 millisecond - 1 nanosecond', () => { + expect(getFormattedDurationString(ONE_MILLISECOND_AS_NANOSECONDS - 1)).toEqual('999999ns'); + }); + + test('it returns exactly 1 millisecond (with no fractional component) when the input is exactly one millisecond', () => { + expect(getFormattedDurationString(ONE_MILLISECOND_AS_NANOSECONDS)).toEqual('1ms'); + }); + + test('it returns 1 millisecond with a fractional component when the input is 1 millisecond + 1 nanosecond', () => { + expect(getFormattedDurationString(ONE_MILLISECOND_AS_NANOSECONDS + 1)).toEqual('1.000001ms'); + }); + + test('it returns the largest value (in milliseconds) that would be represented as milliseconds with a fractional component when the input is 1 second - 1 nanosecond', () => { + expect(getFormattedDurationString(ONE_SECOND * ONE_MILLISECOND_AS_NANOSECONDS - 1)).toEqual( + '999.999999ms' + ); + }); + + test('it returns exactly one second (with no millisecond component) when the input is exactly one second', () => { + expect(getFormattedDurationString(ONE_SECOND * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual('1s'); + }); + + test('it returns one second with fractional milliseconds when the input is one second + 1 nanosecond', () => { + expect(getFormattedDurationString(ONE_SECOND * ONE_MILLISECOND_AS_NANOSECONDS + 1)).toEqual( + '1s 0.000001ms' + ); + }); + + test('it returns one second with fractional milliseconds when the input is 1 second + 1 millisecond - 1 nanosecond', () => { + expect( + getFormattedDurationString( + ONE_SECOND * ONE_MILLISECOND_AS_NANOSECONDS + ONE_MILLISECOND_AS_NANOSECONDS - 1 + ) + ).toEqual('1s 0.999999ms'); + }); + + test('it returns 1 second, 1 non-fractional millisecond when the input is 1 second + 1 millisecond', () => { + expect( + getFormattedDurationString( + ONE_SECOND * ONE_MILLISECOND_AS_NANOSECONDS + ONE_MILLISECOND_AS_NANOSECONDS + ) + ).toEqual('1s 1ms'); + }); + + test('it returns 1 seconds with fractional milliseconds when the input is 1 second + 1 millisecond + 1 nanosecond', () => { + expect( + getFormattedDurationString( + ONE_SECOND * ONE_MILLISECOND_AS_NANOSECONDS + ONE_MILLISECOND_AS_NANOSECONDS + 1 + ) + ).toEqual('1s 1.000001ms'); + }); + + test('it returns 1 seconds with fractional milliseconds when the input is 1 second + 2 milliseconds - 1 nanosecond', () => { + expect( + getFormattedDurationString( + ONE_SECOND * ONE_MILLISECOND_AS_NANOSECONDS + 2 * ONE_MILLISECOND_AS_NANOSECONDS - 1 + ) + ).toEqual('1s 1.999999ms'); + }); + + test('it returns 59 seconds with fractional milliseconds when the input is 1 minute - 1 nanosecond', () => { + expect(getFormattedDurationString(ONE_MINUTE * ONE_MILLISECOND_AS_NANOSECONDS - 1)).toEqual( + '59s 999.999999ms' + ); + }); + + test('it returns 1 minute with 0 non-fractional seconds (and no milliseconds) when the input is exactly 1 minute', () => { + expect(getFormattedDurationString(ONE_MINUTE * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual( + '1m 0s' + ); + }); + + test('it returns 1 minute, 0 seconds, and fractional milliseconds when the input is 1 minute + 1 nanosecond', () => { + expect(getFormattedDurationString(ONE_MINUTE * ONE_MILLISECOND_AS_NANOSECONDS + 1)).toEqual( + '1m 0s 0.000001ms' + ); + }); + + test('it returns the duration formatted as 1 minute, 59 seconds and fractional milliseconds when the input is 2 minutes - 1 nanosecond', () => { + expect( + getFormattedDurationString(2 * ONE_MINUTE * ONE_MILLISECOND_AS_NANOSECONDS - 1) + ).toEqual('1m 59s 999.999999ms'); + }); + + test('it returns the duration formatted as 59 minutes, 59 seconds and fractional milliseconds when the input is one hour - 1 nanosecond', () => { + expect(getFormattedDurationString(ONE_HOUR * ONE_MILLISECOND_AS_NANOSECONDS - 1)).toEqual( + '59m 59s 999.999999ms' + ); + }); + + test('it returns the duration formatted as 1 hour, 0 minutes, 0 seconds, (and no milliseconds) when the duration is exactly one hour', () => { + expect(getFormattedDurationString(ONE_HOUR * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual( + '1h 0m 0s' + ); + }); + + test('it returns the duration formatted as 1 hour, 0 minutes and seconds, and fractional milliseconds when the duration is exactly 1 hour + 1 nanosecond', () => { + expect(getFormattedDurationString(ONE_HOUR * ONE_MILLISECOND_AS_NANOSECONDS + 1)).toEqual( + '1h 0m 0s 0.000001ms' + ); + }); + + test('it returns the duration formatted as 23 hours, 59 minutes, 59 seconds, and fractional milliseconds when the duration is one day - 1 nanosecond', () => { + expect(getFormattedDurationString(ONE_DAY * ONE_MILLISECOND_AS_NANOSECONDS - 1)).toEqual( + '23h 59m 59s 999.999999ms' + ); + }); + + test('it returns the duration formatted as 1 day, 0 hours, 0 minutes, and 0 seconds, (and no milliseconds) when the duration is exactly one day', () => { + expect(getFormattedDurationString(ONE_DAY * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual( + '1d 0h 0m 0s' + ); + }); + + test('it returns the duration formatted as one day, with zero hours, minutes, seconds, and fractional milliseconds when the duration is one day + 1 nanosecond', () => { + expect(getFormattedDurationString(ONE_DAY * ONE_MILLISECOND_AS_NANOSECONDS + 1)).toEqual( + '1d 0h 0m 0s 0.000001ms' + ); + }); + + test('it returns the duration formatted as 29 days, 23 hours, 59 minutes, 59 seconds, and with fractional milliseconds when the duration is one month - 1 nanosecond', () => { + expect(getFormattedDurationString(ONE_MONTH * ONE_MILLISECOND_AS_NANOSECONDS - 1)).toEqual( + '29d 23h 59m 59s 999.999999ms' + ); + }); + + test('it returns 30 days, zero hours, minutes, seconds, (and no millieconds) when the duration is exactly one month, as is the current behavior of moment', () => { + expect(getFormattedDurationString(ONE_MONTH * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual( + '30d 0h 0m 0s' // see https://github.com/moment/moment/issues/3653 + ); + }); + + test('it returns the duration as 29 days, 23 hours, 59 minutes, 59 seconds, and fractional milliseconds when the duration is 1 month - 1 nanosecond', () => { + expect(getFormattedDurationString(ONE_MONTH * ONE_MILLISECOND_AS_NANOSECONDS - 1)).toEqual( + '29d 23h 59m 59s 999.999999ms' // see https://github.com/moment/moment/issues/3653 + ); + }); + + test('it returns 1 month, zero days, hours, minutes, seconds (and no milliseconds) month when the duration is exactly 1 month + 1 day, as is the current behavior of moment', () => { + expect( + getFormattedDurationString((ONE_MONTH + ONE_DAY) * ONE_MILLISECOND_AS_NANOSECONDS) + ).toEqual( + '1m 0d 0h 0m 0s' // see https://github.com/moment/moment/issues/3653 + ); + }); + + test('it returns the 1 month with 0 days, hours, minutes, seconds, and fractional milliseconds when the duration is exactly 1 month + 1 day + 1 nanosecond, ', () => { + expect( + getFormattedDurationString((ONE_MONTH + ONE_DAY) * ONE_MILLISECOND_AS_NANOSECONDS + 1) + ).toEqual( + '1m 0d 0h 0m 0s 0.000001ms' // see https://github.com/moment/moment/issues/3653 + ); + }); + + test('it returns 11 months, 30 days (with 0 hours, minutes, and non-fractional seconds) when the duration is exactly one year, as is the current behavior of moment', () => { + expect(getFormattedDurationString(ONE_YEAR * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual( + '11m 30d 0h 0m 0s' // see https://github.com/moment/moment/issues/3209 + ); + }); + + test('it returns one year when the duration is exactly 1 year + 1 day, as is the current behavior of moment', () => { + expect( + getFormattedDurationString((ONE_YEAR + ONE_DAY) * ONE_MILLISECOND_AS_NANOSECONDS) + ).toEqual( + '1y 0m 0d 0h 0m 0s' // see https://github.com/moment/moment/issues/3209 + ); + }); + + test('it returns less than 6 months when input is 1 year + 6 months, as is the current behavior of moment', () => { + expect( + getFormattedDurationString((ONE_YEAR + 6 * ONE_MONTH) * ONE_MILLISECOND_AS_NANOSECONDS) + ).toEqual('1y 5m 27d 0h 0m 0s'); // see https://github.com/moment/moment/issues/3209 + }); + }); + + describe('#getHumanizedDuration', () => { + test('it returns "no duration" when the input is undefined', () => { + expect(getHumanizedDuration(undefined)).toEqual(i18n.NO_DURATION); + }); + + test('it returns "no duration" when the input is null', () => { + expect(getHumanizedDuration(null)).toEqual(i18n.NO_DURATION); + }); + + test('it returns "invalid duration" when the input is not a number', () => { + expect(getHumanizedDuration('an invalid duration')).toEqual(i18n.INVALID_DURATION); + }); + + test('it returns the original "invalid duration" when the input is negative', () => { + expect(getHumanizedDuration(-1)).toEqual(i18n.INVALID_DURATION); + }); + + test('it returns "zero nanoseconds" when the input is 0 nanoseconds', () => { + expect(getHumanizedDuration(0)).toEqual(i18n.ZERO_NANOSECONDS); + }); + + test('it returns "a nanosecond" nanosecond when the input is 1 nanosecond', () => { + expect(getHumanizedDuration(1)).toEqual(i18n.A_NANOSECOND); + }); + + test('it returns "a few nanoseconds" when the input is 1000 nanoseconds', () => { + expect(getHumanizedDuration(1000)).toEqual(i18n.A_FEW_NANOSECONDS); + }); + + test('it returns 1000 nanoseconds when the input is a string ("1000") instead of a number', () => { + expect(getHumanizedDuration('1000')).toEqual(i18n.A_FEW_NANOSECONDS); + }); + + test('it returns "a few nanoseconds" given the largest value that would be represented as nanoseconds', () => { + expect(getHumanizedDuration(ONE_MILLISECOND_AS_NANOSECONDS - 1)).toEqual( + i18n.A_FEW_NANOSECONDS + ); + }); + + test('it returns "a millisecond" when the input is exactly one millisecond', () => { + expect(getHumanizedDuration(ONE_MILLISECOND_AS_NANOSECONDS)).toEqual(i18n.A_MILLISECOND); + }); + + test('it returns "a few milliseconds" when the input is 1 millisecond + 1 nanosecond', () => { + expect(getHumanizedDuration(ONE_MILLISECOND_AS_NANOSECONDS + 1)).toEqual( + i18n.A_FEW_MILLISECONDS + ); + }); + + test('it returns "a few milliseconds" when the input is the maximum value for milliseconds', () => { + expect(getHumanizedDuration(ONE_SECOND * ONE_MILLISECOND_AS_NANOSECONDS - 1)).toEqual( + i18n.A_FEW_MILLISECONDS + ); + }); + + test('it returns "a second" when the input is exactly one second', () => { + expect(getHumanizedDuration(ONE_SECOND * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual( + i18n.A_SECOND + ); + }); + + test('it returns "a few seconds" when the input is one second + 1 nanosecond', () => { + expect(getHumanizedDuration(ONE_SECOND * ONE_MILLISECOND_AS_NANOSECONDS + 1)).toEqual( + 'a few seconds' // <-- note for this and the rest of the tests in this 'describe', this value is coming from moment, which has it's own i18n + ); + }); + + test('it rounds to "a minute" when the input is 45 seconds', () => { + expect(getHumanizedDuration(45 * ONE_SECOND * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual( + 'a minute' // <-- debatable, but thats' how moment describes this + ); + }); + + test('it rounds to "a minute" when the input is 1 minute - 1 nanosecond', () => { + expect(getHumanizedDuration(ONE_MINUTE * ONE_MILLISECOND_AS_NANOSECONDS - 1)).toEqual( + 'a minute' + ); + }); + + test('it returns "a minute" when the input is exactly 1 minute', () => { + expect(getHumanizedDuration(ONE_MINUTE * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual('a minute'); + }); + + test('it rounds to "a minute" when the input is 1 minute + 1 nanosecond', () => { + expect(getHumanizedDuration(ONE_MINUTE * ONE_MILLISECOND_AS_NANOSECONDS + 1)).toEqual( + 'a minute' + ); + }); + + test('it rounds to "two minutes" when the input is 2 minutes - 1 nanosecond', () => { + expect(getHumanizedDuration(2 * ONE_MINUTE * ONE_MILLISECOND_AS_NANOSECONDS - 1)).toEqual( + '2 minutes' + ); + }); + + test('it rounds to "an hour" when the input is one hour - 1 nanosecond', () => { + expect(getHumanizedDuration(ONE_HOUR * ONE_MILLISECOND_AS_NANOSECONDS - 1)).toEqual( + 'an hour' + ); + }); + + test('it returns "an hour" when the input is exactly one hour', () => { + expect(getHumanizedDuration(ONE_HOUR * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual('an hour'); + }); + + test('it rounds to "an hour" when the input 1 hour + 1 nanosecond', () => { + expect(getHumanizedDuration(ONE_HOUR * ONE_MILLISECOND_AS_NANOSECONDS + 1)).toEqual( + 'an hour' + ); + }); + + test('it returns "2 hours" when the input is exactly 2 hours', () => { + expect(getHumanizedDuration(2 * ONE_HOUR * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual( + '2 hours' + ); + }); + + test('it rounds to "a day" when the input is one day - 1 nanosecond', () => { + expect(getHumanizedDuration(ONE_DAY * ONE_MILLISECOND_AS_NANOSECONDS - 1)).toEqual('a day'); + }); + + test('it returns "a day" when the input is exactly one day', () => { + expect(getHumanizedDuration(ONE_DAY * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual('a day'); + }); + + test('it rounds to "a day" when the input is one day + 1 nanosecond', () => { + expect(getHumanizedDuration(ONE_DAY * ONE_MILLISECOND_AS_NANOSECONDS + 1)).toEqual('a day'); + }); + + test('it returns "2 days" when the input is exactly two days', () => { + expect(getHumanizedDuration(2 * ONE_DAY * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual('2 days'); + }); + + test('it rounds to "a month" when the input is one month - 1 nanosecond', () => { + expect(getHumanizedDuration(ONE_MONTH * ONE_MILLISECOND_AS_NANOSECONDS - 1)).toEqual( + 'a month' + ); + }); + + test('it returns "a month" when the input is exactly one month', () => { + expect(getHumanizedDuration(ONE_MONTH * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual('a month'); + }); + + test('it rounds to "a month" when the input is 1 month + 1 day', () => { + expect(getHumanizedDuration((ONE_MONTH + ONE_DAY) * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual( + 'a month' + ); + }); + + test('it returns "2 months" when the input is 2 months', () => { + expect(getHumanizedDuration(2 * ONE_MONTH * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual( + '2 months' + ); + }); + + test('it returns "a year" when the input is exactly one year', () => { + expect(getHumanizedDuration(ONE_YEAR * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual('a year'); + }); + + test('it rounds down to "a year" when the input is 1 year + 6 months, as is the current behavior of moment', () => { + expect( + getHumanizedDuration((ONE_YEAR + 6 * ONE_MONTH) * ONE_MILLISECOND_AS_NANOSECONDS) + ).toEqual('a year'); // <-- as a user, you may not expect this + }); + + test('it returns "2 years" when the duration is exactly 2 years', () => { + expect(getHumanizedDuration(2 * ONE_YEAR * ONE_MILLISECOND_AS_NANOSECONDS)).toEqual( + '2 years' + ); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/timelines/components/formatted_duration/helpers.tsx b/x-pack/plugins/siem/public/timelines/components/formatted_duration/helpers.tsx new file mode 100644 index 00000000000000..113ed70776034a --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/formatted_duration/helpers.tsx @@ -0,0 +1,110 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import moment from 'moment'; + +import { getEmptyValue } from '../../../common/components/empty_value'; + +import * as i18n from './translations'; + +/** one millisecond (as nanoseconds) */ +export const ONE_MILLISECOND_AS_NANOSECONDS = 1000000; + +export const ONE_SECOND = 1000; +export const ONE_MINUTE = 60000; +export const ONE_HOUR = 3600000; +export const ONE_DAY = 86400000; // ms +export const ONE_MONTH = 2592000000; // ms +export const ONE_YEAR = 31536000000; // ms + +const milliseconds = (duration: moment.Duration): string => + Number.isInteger(duration.milliseconds()) + ? `${duration.milliseconds()}ms` + : `${duration.milliseconds().toFixed(6)}ms`; // nanosecond precision +const seconds = (duration: moment.Duration): string => + `${duration.seconds().toFixed()}s${ + duration.milliseconds() > 0 ? ` ${milliseconds(duration)}` : '' + }`; +const minutes = (duration: moment.Duration): string => + `${duration.minutes()}m ${seconds(duration)}`; +const hours = (duration: moment.Duration): string => `${duration.hours()}h ${minutes(duration)}`; +const days = (duration: moment.Duration): string => `${duration.days()}d ${hours(duration)}`; +const months = (duration: moment.Duration): string => + `${duration.years() > 0 || duration.months() > 0 ? `${duration.months()}m ` : ''}${days( + duration + )}`; +const years = (duration: moment.Duration): string => + `${duration.years() > 0 ? `${duration.years()}y ` : ''}${months(duration)}`; + +export const getFormattedDurationString = ( + maybeDurationNanoseconds: string | number | object | undefined | null +): string => { + const totalNanoseconds = Number(maybeDurationNanoseconds); + + if (maybeDurationNanoseconds == null) { + return getEmptyValue(); + } + + if (Number.isNaN(totalNanoseconds) || totalNanoseconds < 0) { + return `${maybeDurationNanoseconds}`; // echo back the duration as a string + } + + if (totalNanoseconds < ONE_MILLISECOND_AS_NANOSECONDS) { + return `${totalNanoseconds}ns`; // display the raw nanoseconds + } + + const duration = moment.duration(totalNanoseconds / ONE_MILLISECOND_AS_NANOSECONDS); + const totalMs = duration.asMilliseconds(); + + if (totalMs < ONE_SECOND) { + return milliseconds(duration); + } else if (totalMs < ONE_MINUTE) { + return seconds(duration); + } else if (totalMs < ONE_HOUR) { + return minutes(duration); + } else if (totalMs < ONE_DAY) { + return hours(duration); + } else if (totalMs < ONE_MONTH) { + return days(duration); + } else if (totalMs < ONE_YEAR) { + return months(duration); + } else { + return years(duration); + } +}; + +export const getHumanizedDuration = ( + maybeDurationNanoseconds: string | number | object | undefined | null +): string => { + if (maybeDurationNanoseconds == null) { + return i18n.NO_DURATION; + } + + const totalNanoseconds = Number(maybeDurationNanoseconds); + + if (Number.isNaN(totalNanoseconds) || totalNanoseconds < 0) { + return i18n.INVALID_DURATION; + } + + if (totalNanoseconds === 0) { + return i18n.ZERO_NANOSECONDS; + } else if (totalNanoseconds === 1) { + return i18n.A_NANOSECOND; + } else if (totalNanoseconds < ONE_MILLISECOND_AS_NANOSECONDS) { + return i18n.A_FEW_NANOSECONDS; + } else if (totalNanoseconds === ONE_MILLISECOND_AS_NANOSECONDS) { + return i18n.A_MILLISECOND; + } + + const totalMs = totalNanoseconds / ONE_MILLISECOND_AS_NANOSECONDS; + if (totalMs < ONE_SECOND) { + return i18n.A_FEW_MILLISECONDS; + } else if (totalMs === ONE_SECOND) { + return i18n.A_SECOND; + } else { + return moment.duration(totalMs).humanize(); + } +}; diff --git a/x-pack/plugins/siem/public/components/formatted_duration/index.tsx b/x-pack/plugins/siem/public/timelines/components/formatted_duration/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/formatted_duration/index.tsx rename to x-pack/plugins/siem/public/timelines/components/formatted_duration/index.tsx diff --git a/x-pack/plugins/siem/public/components/formatted_duration/tooltip/index.tsx b/x-pack/plugins/siem/public/timelines/components/formatted_duration/tooltip/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/formatted_duration/tooltip/index.tsx rename to x-pack/plugins/siem/public/timelines/components/formatted_duration/tooltip/index.tsx diff --git a/x-pack/plugins/siem/public/components/formatted_duration/translations.ts b/x-pack/plugins/siem/public/timelines/components/formatted_duration/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/formatted_duration/translations.ts rename to x-pack/plugins/siem/public/timelines/components/formatted_duration/translations.ts diff --git a/x-pack/plugins/siem/public/timelines/components/formatted_ip/index.tsx b/x-pack/plugins/siem/public/timelines/components/formatted_ip/index.tsx new file mode 100644 index 00000000000000..e3a722214d4723 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/formatted_ip/index.tsx @@ -0,0 +1,184 @@ +/* + * 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 { isArray, isEmpty, isString, uniq } from 'lodash/fp'; +import React from 'react'; + +import { + DragEffects, + DraggableWrapper, +} from '../../../common/components/drag_and_drop/draggable_wrapper'; +import { escapeDataProviderId } from '../../../common/components/drag_and_drop/helpers'; +import { getOrEmptyTagFromValue } from '../../../common/components/empty_value'; +import { IPDetailsLink } from '../../../common/components/links'; +import { parseQueryValue } from '../../../timelines/components/timeline/body/renderers/parse_query_value'; +import { + DataProvider, + IS_OPERATOR, +} from '../../../timelines/components/timeline/data_providers/data_provider'; +import { Provider } from '../../../timelines/components/timeline/data_providers/provider'; + +const getUniqueId = ({ + contextId, + eventId, + fieldName, + address, +}: { + contextId: string; + eventId: string; + fieldName: string; + address: string | object | null | undefined; +}) => `formatted-ip-data-provider-${contextId}-${fieldName}-${address}-${eventId}`; + +const tryStringify = (value: string | object | null | undefined): string => { + try { + return JSON.stringify(value); + } catch (_) { + return `${value}`; + } +}; + +const getDataProvider = ({ + contextId, + eventId, + fieldName, + address, +}: { + contextId: string; + eventId: string; + fieldName: string; + address: string | object | null | undefined; +}): DataProvider => ({ + enabled: true, + id: escapeDataProviderId(getUniqueId({ contextId, eventId, fieldName, address })), + name: `${fieldName}: ${parseQueryValue(address)}`, + queryMatch: { + field: fieldName, + value: parseQueryValue(address), + operator: IS_OPERATOR, + }, + excluded: false, + kqlQuery: '', + and: [], +}); + +const NonDecoratedIpComponent: React.FC<{ + contextId: string; + eventId: string; + fieldName: string; + truncate?: boolean; + value: string | object | null | undefined; +}> = ({ contextId, eventId, fieldName, truncate, value }) => ( + + snapshot.isDragging ? ( + + + + ) : typeof value !== 'object' ? ( + getOrEmptyTagFromValue(value) + ) : ( + getOrEmptyTagFromValue(tryStringify(value)) + ) + } + truncate={truncate} + /> +); + +const NonDecoratedIp = React.memo(NonDecoratedIpComponent); + +const AddressLinksComponent: React.FC<{ + addresses: string[]; + contextId: string; + eventId: string; + fieldName: string; + truncate?: boolean; +}> = ({ addresses, contextId, eventId, fieldName, truncate }) => ( + <> + {uniq(addresses).map(address => ( + + snapshot.isDragging ? ( + + + + ) : ( + + ) + } + truncate={truncate} + /> + ))} + +); + +const AddressLinks = React.memo(AddressLinksComponent); + +const FormattedIpComponent: React.FC<{ + contextId: string; + eventId: string; + fieldName: string; + truncate?: boolean; + value: string | object | null | undefined; +}> = ({ contextId, eventId, fieldName, truncate, value }) => { + if (isString(value) && !isEmpty(value)) { + try { + const addresses = JSON.parse(value); + if (isArray(addresses)) { + return ( + + ); + } + } catch (_) { + // fall back to formatting it as a single link + } + + // return a single draggable link + return ( + + ); + } else { + return ( + + ); + } +}; + +export const FormattedIp = React.memo(FormattedIpComponent); diff --git a/x-pack/plugins/siem/public/timelines/components/ja3_fingerprint/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/ja3_fingerprint/index.test.tsx new file mode 100644 index 00000000000000..4ca1e7cc1bad4a --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/ja3_fingerprint/index.test.tsx @@ -0,0 +1,76 @@ +/* + * 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 { TestProviders } from '../../../common/mock'; +import { useMountAppended } from '../../../common/utils/use_mount_appended'; + +import { Ja3Fingerprint } from '.'; + +describe('Ja3Fingerprint', () => { + const mount = useMountAppended(); + + test('renders the expected label', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="ja3-fingerprint-label"]') + .first() + .text() + ).toEqual('ja3'); + }); + + test('renders the fingerprint as text', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="ja3-fingerprint-link"]') + .first() + .text() + ).toEqual('fff799d91b7c01ae3fe6787cfc895552'); + }); + + test('it renders a hyperlink to an external site to compare the fingerprint against a known set of signatures', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="ja3-fingerprint-link"]') + .first() + .props().href + ).toEqual('https://sslbl.abuse.ch/ja3-fingerprints/fff799d91b7c01ae3fe6787cfc895552'); + }); +}); diff --git a/x-pack/plugins/siem/public/timelines/components/ja3_fingerprint/index.tsx b/x-pack/plugins/siem/public/timelines/components/ja3_fingerprint/index.tsx new file mode 100644 index 00000000000000..2bb4e7471eba88 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/ja3_fingerprint/index.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import styled from 'styled-components'; + +import { DraggableBadge } from '../../../common/components/draggables'; +import { ExternalLinkIcon } from '../../../common/components/external_link_icon'; +import { Ja3FingerprintLink } from '../../../common/components/links'; + +import * as i18n from './translations'; + +export const JA3_HASH_FIELD_NAME = 'tls.fingerprints.ja3.hash'; + +const Ja3FingerprintLabel = styled.span` + margin-right: 5px; +`; + +Ja3FingerprintLabel.displayName = 'Ja3FingerprintLabel'; + +/** + * Renders a ja3 fingerprint, which enables (some) clients and servers communicating + * using TLS traffic to be identified, which is possible because SSL + * negotiations happen in the clear + */ +export const Ja3Fingerprint = React.memo<{ + eventId: string; + contextId: string; + fieldName: string; + value?: string | null; +}>(({ contextId, eventId, fieldName, value }) => ( + + + {i18n.JA3_FINGERPRINT_LABEL} + + + + +)); + +Ja3Fingerprint.displayName = 'Ja3Fingerprint'; diff --git a/x-pack/plugins/siem/public/components/ja3_fingerprint/translations.ts b/x-pack/plugins/siem/public/timelines/components/ja3_fingerprint/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/ja3_fingerprint/translations.ts rename to x-pack/plugins/siem/public/timelines/components/ja3_fingerprint/translations.ts diff --git a/x-pack/plugins/siem/public/components/lazy_accordion/index.tsx b/x-pack/plugins/siem/public/timelines/components/lazy_accordion/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/lazy_accordion/index.tsx rename to x-pack/plugins/siem/public/timelines/components/lazy_accordion/index.tsx diff --git a/x-pack/plugins/siem/public/components/loading/index.tsx b/x-pack/plugins/siem/public/timelines/components/loading/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/loading/index.tsx rename to x-pack/plugins/siem/public/timelines/components/loading/index.tsx diff --git a/x-pack/plugins/siem/public/components/netflow/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/netflow/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/netflow/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/netflow/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/netflow/fingerprints/index.tsx b/x-pack/plugins/siem/public/timelines/components/netflow/fingerprints/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/netflow/fingerprints/index.tsx rename to x-pack/plugins/siem/public/timelines/components/netflow/fingerprints/index.tsx diff --git a/x-pack/plugins/siem/public/timelines/components/netflow/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/netflow/index.test.tsx new file mode 100644 index 00000000000000..0a6d2f8ab3178a --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/netflow/index.test.tsx @@ -0,0 +1,537 @@ +/* + * 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 { get } from 'lodash/fp'; +import React from 'react'; +import { shallow } from 'enzyme'; + +import { asArrayIfExists } from '../../../common/lib/helpers'; +import { getMockNetflowData } from '../../../common/mock'; +import { TestProviders } from '../../../common/mock/test_providers'; +import { + TLS_CLIENT_CERTIFICATE_FINGERPRINT_SHA1_FIELD_NAME, + TLS_SERVER_CERTIFICATE_FINGERPRINT_SHA1_FIELD_NAME, +} from '../certificate_fingerprint'; +import { EVENT_DURATION_FIELD_NAME } from '../duration'; +import { ID_FIELD_NAME } from '../../../common/components/event_details/event_id'; +import { DESTINATION_IP_FIELD_NAME, SOURCE_IP_FIELD_NAME } from '../../../network/components/ip'; +import { JA3_HASH_FIELD_NAME } from '../ja3_fingerprint'; +import { + DESTINATION_PORT_FIELD_NAME, + SOURCE_PORT_FIELD_NAME, +} from '../../../network/components/port'; +import { + DESTINATION_GEO_CITY_NAME_FIELD_NAME, + DESTINATION_GEO_CONTINENT_NAME_FIELD_NAME, + DESTINATION_GEO_COUNTRY_ISO_CODE_FIELD_NAME, + DESTINATION_GEO_COUNTRY_NAME_FIELD_NAME, + DESTINATION_GEO_REGION_NAME_FIELD_NAME, + SOURCE_GEO_CITY_NAME_FIELD_NAME, + SOURCE_GEO_CONTINENT_NAME_FIELD_NAME, + SOURCE_GEO_COUNTRY_ISO_CODE_FIELD_NAME, + SOURCE_GEO_COUNTRY_NAME_FIELD_NAME, + SOURCE_GEO_REGION_NAME_FIELD_NAME, +} from '../../../network/components/source_destination/geo_fields'; +import { + DESTINATION_BYTES_FIELD_NAME, + DESTINATION_PACKETS_FIELD_NAME, + SOURCE_BYTES_FIELD_NAME, + SOURCE_PACKETS_FIELD_NAME, +} from '../../../network/components/source_destination/source_destination_arrows'; +import * as i18n from '../timeline/body/renderers/translations'; + +import { Netflow } from '.'; +import { + EVENT_END_FIELD_NAME, + EVENT_START_FIELD_NAME, +} from './netflow_columns/duration_event_start_end'; +import { PROCESS_NAME_FIELD_NAME, USER_NAME_FIELD_NAME } from './netflow_columns/user_process'; +import { + NETWORK_BYTES_FIELD_NAME, + NETWORK_DIRECTION_FIELD_NAME, + NETWORK_COMMUNITY_ID_FIELD_NAME, + NETWORK_PACKETS_FIELD_NAME, + NETWORK_PROTOCOL_FIELD_NAME, + NETWORK_TRANSPORT_FIELD_NAME, +} from '../../../network/components/source_destination/field_names'; +import { useMountAppended } from '../../../common/utils/use_mount_appended'; + +const getNetflowInstance = () => ( + +); + +describe('Netflow', () => { + const mount = useMountAppended(); + + test('renders correctly against snapshot', () => { + const wrapper = shallow(getNetflowInstance()); + expect(wrapper).toMatchSnapshot(); + }); + + test('it renders a destination label', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="destination-label"]') + .first() + .text() + ).toEqual(i18n.DESTINATION); + }); + + test('it renders destination.bytes', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="destination-bytes"]') + .first() + .text() + ).toEqual('40B'); + }); + + test('it renders destination.geo.continent_name', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="destination.geo.continent_name"]') + .first() + .text() + ).toEqual('North America'); + }); + + test('it renders destination.geo.country_name', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="destination.geo.country_name"]') + .first() + .text() + ).toEqual('United States'); + }); + + test('it renders destination.geo.country_iso_code', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="destination.geo.country_iso_code"]') + .first() + .text() + ).toEqual('US'); + }); + + test('it renders destination.geo.region_name', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="destination.geo.region_name"]') + .first() + .text() + ).toEqual('New York'); + }); + + test('it renders destination.geo.city_name', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="destination.geo.city_name"]') + .first() + .text() + ).toEqual('New York'); + }); + + test('it renders the destination ip and port, separated with a colon', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="destination-ip-and-port"]') + .first() + .text() + ).toEqual('10.1.2.3:80'); + }); + + test('it renders destination.packets', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="destination-packets"]') + .first() + .text() + ).toEqual('1 pkts'); + }); + + test('it hyperlinks links destination.port to an external service that describes the purpose of the port', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="destination-ip-and-port"]') + .find('[data-test-subj="port-or-service-name-link"]') + .first() + .props().href + ).toEqual( + 'https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml?search=80' + ); + }); + + test('it renders event.duration', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="event-duration"]') + .first() + .text() + ).toEqual('1ms'); + }); + + test('it renders event.end', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="event-end"]') + .first() + .text().length + ).toBeGreaterThan(0); // the format of this date will depend on the user's locale and settings + }); + + test('it renders event.start', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="event-start"]') + .first() + .text().length + ).toBeGreaterThan(0); // the format of this date will depend on the user's locale and settings + }); + + test('it renders network.bytes', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="network-bytes"]') + .first() + .text() + ).toEqual('100B'); + }); + + test('it renders network.community_id', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="network-community-id"]') + .first() + .text() + ).toEqual('we.live.in.a'); + }); + + test('it renders network.direction', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="network-direction"]') + .first() + .text() + ).toEqual('outgoing'); + }); + + test('it renders network.packets', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="network-packets"]') + .first() + .text() + ).toEqual('3 pkts'); + }); + + test('it renders network.protocol', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="network-protocol"]') + .first() + .text() + ).toEqual('http'); + }); + + test('it renders process.name', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="process-name"]') + .first() + .text() + ).toEqual('rat'); + }); + + test('it renders a source label', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="source-label"]') + .first() + .text() + ).toEqual(i18n.SOURCE); + }); + + test('it renders source.bytes', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="source-bytes"]') + .first() + .text() + ).toEqual('60B'); + }); + + test('it renders source.geo.continent_name', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="source.geo.continent_name"]') + .first() + .text() + ).toEqual('North America'); + }); + + test('it renders source.geo.country_name', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="source.geo.country_name"]') + .first() + .text() + ).toEqual('United States'); + }); + + test('it renders source.geo.country_iso_code', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="source.geo.country_iso_code"]') + .first() + .text() + ).toEqual('US'); + }); + + test('it renders source.geo.region_name', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="source.geo.region_name"]') + .first() + .text() + ).toEqual('Georgia'); + }); + + test('it renders source.geo.city_name', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="source.geo.city_name"]') + .first() + .text() + ).toEqual('Atlanta'); + }); + + test('it renders the source ip and port, separated with a colon', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="source-ip-and-port"]') + .first() + .text() + ).toEqual('192.168.1.2:9987'); + }); + + test('it renders source.packets', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="source-packets"]') + .first() + .text() + ).toEqual('2 pkts'); + }); + + test('it hyperlinks tls.client_certificate.fingerprint.sha1 site to compare the fingerprint against a known set of signatures', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="client-certificate-fingerprint"]') + .find('[data-test-subj="certificate-fingerprint-link"]') + .first() + .props().href + ).toEqual( + 'https://sslbl.abuse.ch/ssl-certificates/sha1/tls.client_certificate.fingerprint.sha1-value' + ); + }); + + test('renders tls.client_certificate.fingerprint.sha1 text', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="client-certificate-fingerprint"]') + .find('[data-test-subj="certificate-fingerprint-link"]') + .first() + .text() + ).toEqual('tls.client_certificate.fingerprint.sha1-value'); + }); + + test('it hyperlinks tls.fingerprints.ja3.hash site to compare the fingerprint against a known set of signatures', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="ja3-fingerprint-link"]') + .first() + .props().href + ).toEqual('https://sslbl.abuse.ch/ja3-fingerprints/tls.fingerprints.ja3.hash-value'); + }); + + test('renders tls.fingerprints.ja3.hash text', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="ja3-fingerprint-link"]') + .first() + .text() + ).toEqual('tls.fingerprints.ja3.hash-value'); + }); + + test('it hyperlinks tls.server_certificate.fingerprint.sha1 site to compare the fingerprint against a known set of signatures', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="server-certificate-fingerprint"]') + .find('[data-test-subj="certificate-fingerprint-link"]') + .first() + .props().href + ).toEqual( + 'https://sslbl.abuse.ch/ssl-certificates/sha1/tls.server_certificate.fingerprint.sha1-value' + ); + }); + + test('renders tls.server_certificate.fingerprint.sha1 text', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="server-certificate-fingerprint"]') + .find('[data-test-subj="certificate-fingerprint-link"]') + .first() + .text() + ).toEqual('tls.server_certificate.fingerprint.sha1-value'); + }); + + test('it renders network.transport', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="network-transport"]') + .first() + .text() + ).toEqual('tcp'); + }); + + test('it renders user.name', () => { + const wrapper = mount({getNetflowInstance()}); + + expect( + wrapper + .find('[data-test-subj="user-name"]') + .first() + .text() + ).toEqual('first.last'); + }); +}); diff --git a/x-pack/plugins/siem/public/components/netflow/index.tsx b/x-pack/plugins/siem/public/timelines/components/netflow/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/netflow/index.tsx rename to x-pack/plugins/siem/public/timelines/components/netflow/index.tsx diff --git a/x-pack/plugins/siem/public/components/netflow/netflow_columns/duration_event_start_end.tsx b/x-pack/plugins/siem/public/timelines/components/netflow/netflow_columns/duration_event_start_end.tsx similarity index 95% rename from x-pack/plugins/siem/public/components/netflow/netflow_columns/duration_event_start_end.tsx rename to x-pack/plugins/siem/public/timelines/components/netflow/netflow_columns/duration_event_start_end.tsx index 09fa5d9fe1596a..2fbd71e86f8ff9 100644 --- a/x-pack/plugins/siem/public/components/netflow/netflow_columns/duration_event_start_end.tsx +++ b/x-pack/plugins/siem/public/timelines/components/netflow/netflow_columns/duration_event_start_end.tsx @@ -9,9 +9,9 @@ import { uniq } from 'lodash/fp'; import React from 'react'; import styled from 'styled-components'; -import { DefaultDraggable } from '../../draggables'; +import { DefaultDraggable } from '../../../../common/components/draggables'; import { EVENT_DURATION_FIELD_NAME } from '../../duration'; -import { FormattedDate } from '../../formatted_date'; +import { FormattedDate } from '../../../../common/components/formatted_date'; import { FormattedDuration } from '../../formatted_duration'; export const EVENT_START_FIELD_NAME = 'event.start'; diff --git a/x-pack/plugins/siem/public/timelines/components/netflow/netflow_columns/index.tsx b/x-pack/plugins/siem/public/timelines/components/netflow/netflow_columns/index.tsx new file mode 100644 index 00000000000000..78ba7ad92081d6 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/netflow/netflow_columns/index.tsx @@ -0,0 +1,127 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +import { SourceDestination } from '../../../../network/components/source_destination'; + +import { DurationEventStartEnd } from './duration_event_start_end'; +import { NetflowColumnsProps } from './types'; +import { UserProcess } from './user_process'; + +export const EVENT_START = 'event.start'; +export const EVENT_END = 'event.end'; + +const EuiFlexItemMarginRight = styled(EuiFlexItem)` + margin-right: 10px; +`; + +EuiFlexItemMarginRight.displayName = 'EuiFlexItemMarginRight'; + +/** + * Renders columns of draggable badges that describe both Netflow data, or more + * generally, hosts interacting over a network connection. This component is + * consumed by the `Netflow` visualization / row renderer. + * + * This component will allow columns to wrap if constraints on width prevent all + * the columns from fitting on a single horizontal row + */ +export const NetflowColumns = React.memo( + ({ + contextId, + destinationBytes, + destinationGeoContinentName, + destinationGeoCountryName, + destinationGeoCountryIsoCode, + destinationGeoRegionName, + destinationGeoCityName, + destinationIp, + destinationPackets, + destinationPort, + eventDuration, + eventId, + eventEnd, + eventStart, + networkBytes, + networkCommunityId, + networkDirection, + networkPackets, + networkProtocol, + processName, + sourceBytes, + sourceGeoContinentName, + sourceGeoCountryName, + sourceGeoCountryIsoCode, + sourceGeoRegionName, + sourceGeoCityName, + sourceIp, + sourcePackets, + sourcePort, + transport, + userName, + }) => ( + + + + + + + + + + + + + + ) +); + +NetflowColumns.displayName = 'NetflowColumns'; diff --git a/x-pack/plugins/siem/public/components/netflow/netflow_columns/types.ts b/x-pack/plugins/siem/public/timelines/components/netflow/netflow_columns/types.ts similarity index 100% rename from x-pack/plugins/siem/public/components/netflow/netflow_columns/types.ts rename to x-pack/plugins/siem/public/timelines/components/netflow/netflow_columns/types.ts diff --git a/x-pack/plugins/siem/public/components/netflow/netflow_columns/user_process.tsx b/x-pack/plugins/siem/public/timelines/components/netflow/netflow_columns/user_process.tsx similarity index 96% rename from x-pack/plugins/siem/public/components/netflow/netflow_columns/user_process.tsx rename to x-pack/plugins/siem/public/timelines/components/netflow/netflow_columns/user_process.tsx index ab71dc301156f8..214eb3f4932712 100644 --- a/x-pack/plugins/siem/public/components/netflow/netflow_columns/user_process.tsx +++ b/x-pack/plugins/siem/public/timelines/components/netflow/netflow_columns/user_process.tsx @@ -8,7 +8,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { uniq } from 'lodash/fp'; import React from 'react'; -import { DraggableBadge } from '../../draggables'; +import { DraggableBadge } from '../../../../common/components/draggables'; export const PROCESS_NAME_FIELD_NAME = 'process.name'; export const USER_NAME_FIELD_NAME = 'user.name'; diff --git a/x-pack/plugins/siem/public/components/netflow/types.ts b/x-pack/plugins/siem/public/timelines/components/netflow/types.ts similarity index 100% rename from x-pack/plugins/siem/public/components/netflow/types.ts rename to x-pack/plugins/siem/public/timelines/components/netflow/types.ts diff --git a/x-pack/plugins/siem/public/components/notes/add_note/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/notes/add_note/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/notes/add_note/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/notes/add_note/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/notes/add_note/__snapshots__/new_note.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/notes/add_note/__snapshots__/new_note.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/notes/add_note/__snapshots__/new_note.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/notes/add_note/__snapshots__/new_note.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/notes/add_note/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/notes/add_note/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/notes/add_note/index.test.tsx rename to x-pack/plugins/siem/public/timelines/components/notes/add_note/index.test.tsx diff --git a/x-pack/plugins/siem/public/timelines/components/notes/add_note/index.tsx b/x-pack/plugins/siem/public/timelines/components/notes/add_note/index.tsx new file mode 100644 index 00000000000000..d3db1a619600f9 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/notes/add_note/index.tsx @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React, { useCallback } from 'react'; +import styled from 'styled-components'; + +import { MarkdownHint } from '../../../../common/components/markdown/markdown_hint'; +import { + AssociateNote, + GetNewNoteId, + updateAndAssociateNode, + UpdateInternalNewNote, + UpdateNote, +} from '../helpers'; +import * as i18n from '../translations'; + +import { NewNote } from './new_note'; + +const AddNotesContainer = styled(EuiFlexGroup)` + margin-bottom: 5px; + user-select: none; +`; + +AddNotesContainer.displayName = 'AddNotesContainer'; + +const ButtonsContainer = styled(EuiFlexGroup)` + margin-top: 5px; +`; + +ButtonsContainer.displayName = 'ButtonsContainer'; + +export const CancelButton = React.memo<{ onCancelAddNote: () => void }>(({ onCancelAddNote }) => ( + + {i18n.CANCEL} + +)); + +CancelButton.displayName = 'CancelButton'; + +/** Displays an input for entering a new note, with an adjacent "Add" button */ +export const AddNote = React.memo<{ + associateNote: AssociateNote; + getNewNoteId: GetNewNoteId; + newNote: string; + onCancelAddNote?: () => void; + updateNewNote: UpdateInternalNewNote; + updateNote: UpdateNote; +}>(({ associateNote, getNewNoteId, newNote, onCancelAddNote, updateNewNote, updateNote }) => { + const handleClick = useCallback( + () => + updateAndAssociateNode({ + associateNote, + getNewNoteId, + newNote, + updateNewNote, + updateNote, + }), + [associateNote, getNewNoteId, newNote, updateNewNote, updateNote] + ); + + return ( + + + + 0} /> + + + {onCancelAddNote != null ? ( + + + + ) : null} + + + {i18n.ADD_NOTE} + + + + + ); +}); + +AddNote.displayName = 'AddNote'; diff --git a/x-pack/plugins/siem/public/components/notes/add_note/new_note.test.tsx b/x-pack/plugins/siem/public/timelines/components/notes/add_note/new_note.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/notes/add_note/new_note.test.tsx rename to x-pack/plugins/siem/public/timelines/components/notes/add_note/new_note.test.tsx diff --git a/x-pack/plugins/siem/public/components/notes/add_note/new_note.tsx b/x-pack/plugins/siem/public/timelines/components/notes/add_note/new_note.tsx similarity index 96% rename from x-pack/plugins/siem/public/components/notes/add_note/new_note.tsx rename to x-pack/plugins/siem/public/timelines/components/notes/add_note/new_note.tsx index 15e58f3efd21ec..99662d446e10f9 100644 --- a/x-pack/plugins/siem/public/components/notes/add_note/new_note.tsx +++ b/x-pack/plugins/siem/public/timelines/components/notes/add_note/new_note.tsx @@ -8,7 +8,7 @@ import { EuiPanel, EuiTabbedContent, EuiTextArea } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; -import { Markdown } from '../../markdown'; +import { Markdown } from '../../../../common/components/markdown'; import { UpdateInternalNewNote } from '../helpers'; import * as i18n from '../translations'; diff --git a/x-pack/plugins/siem/public/components/notes/columns.tsx b/x-pack/plugins/siem/public/timelines/components/notes/columns.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/notes/columns.tsx rename to x-pack/plugins/siem/public/timelines/components/notes/columns.tsx diff --git a/x-pack/plugins/siem/public/timelines/components/notes/helpers.tsx b/x-pack/plugins/siem/public/timelines/components/notes/helpers.tsx new file mode 100644 index 00000000000000..938bc0d222002f --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/notes/helpers.tsx @@ -0,0 +1,113 @@ +/* + * 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 { EuiIcon, EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; +import moment from 'moment'; +import React from 'react'; +import styled from 'styled-components'; + +import { Note } from '../../../common/lib/note'; + +import * as i18n from './translations'; +import { CountBadge } from '../../../common/components/page'; + +/** Performs IO to update (or add a new) note */ +export type UpdateNote = (note: Note) => void; +/** Performs IO to associate a note with something (e.g. a timeline, an event, etc). (The "something" is opaque to the caller) */ +export type AssociateNote = (noteId: string) => void; +/** Performs IO to get a new note ID */ +export type GetNewNoteId = () => string; +/** Updates the local state containing a new note being edited by the user */ +export type UpdateInternalNewNote = (newNote: string) => void; +/** Closes the notes popover */ +export type OnClosePopover = () => void; +/** Performs IO to associate a note with an event */ +export type AddNoteToEvent = ({ eventId, noteId }: { eventId: string; noteId: string }) => void; + +/** + * Defines the behavior of the search input that appears above the table of data + */ +export const search = { + box: { + incremental: true, + placeholder: i18n.SEARCH_PLACEHOLDER, + schema: { + fields: { + user: 'string', + note: 'string', + }, + }, + }, +}; + +const TitleText = styled.h3` + margin: 0 5px; + cursor: default; + user-select: none; +`; + +TitleText.displayName = 'TitleText'; + +/** Displays a count of the existing notes */ +export const NotesCount = React.memo<{ + noteIds: string[]; +}>(({ noteIds }) => ( + + + + + + + + {i18n.NOTES} + + + + + {noteIds.length} + + +)); + +NotesCount.displayName = 'NotesCount'; + +/** Creates a new instance of a `note` */ +export const createNote = ({ + newNote, + getNewNoteId, +}: { + newNote: string; + getNewNoteId: GetNewNoteId; +}): Note => ({ + created: moment.utc().toDate(), + id: getNewNoteId(), + lastEdit: null, + note: newNote.trim(), + saveObjectId: null, + user: 'elastic', // TODO: get the logged-in Kibana user + version: null, +}); + +interface UpdateAndAssociateNodeParams { + associateNote: AssociateNote; + getNewNoteId: GetNewNoteId; + newNote: string; + updateNewNote: UpdateInternalNewNote; + updateNote: UpdateNote; +} + +export const updateAndAssociateNode = ({ + associateNote, + getNewNoteId, + newNote, + updateNewNote, + updateNote, +}: UpdateAndAssociateNodeParams) => { + const note = createNote({ newNote, getNewNoteId }); + updateNote(note); // perform IO to store the newly-created note + associateNote(note.id); // associate the note with the (opaque) thing + updateNewNote(''); // clear the input +}; diff --git a/x-pack/plugins/siem/public/timelines/components/notes/index.tsx b/x-pack/plugins/siem/public/timelines/components/notes/index.tsx new file mode 100644 index 00000000000000..42f28f03406798 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/notes/index.tsx @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiInMemoryTable, + EuiInMemoryTableProps, + EuiModalBody, + EuiModalHeader, + EuiPanel, + EuiSpacer, +} from '@elastic/eui'; +import React, { useState } from 'react'; +import styled from 'styled-components'; + +import { Note } from '../../../common/lib/note'; + +import { AddNote } from './add_note'; +import { columns } from './columns'; +import { AssociateNote, GetNewNoteId, NotesCount, search, UpdateNote } from './helpers'; +import { NOTES_PANEL_WIDTH, NOTES_PANEL_HEIGHT } from '../timeline/properties/notes_size'; + +interface Props { + associateNote: AssociateNote; + getNotesByIds: (noteIds: string[]) => Note[]; + getNewNoteId: GetNewNoteId; + noteIds: string[]; + updateNote: UpdateNote; +} + +const NotesPanel = styled(EuiPanel)` + height: ${NOTES_PANEL_HEIGHT}px; + width: ${NOTES_PANEL_WIDTH}px; + + & thead { + display: none; + } +`; + +NotesPanel.displayName = 'NotesPanel'; + +const InMemoryTable: typeof EuiInMemoryTable & { displayName: string } = styled( + EuiInMemoryTable as React.ComponentType> +)` + overflow-x: hidden; + overflow-y: auto; + height: 220px; +` as any; // eslint-disable-line @typescript-eslint/no-explicit-any + +InMemoryTable.displayName = 'InMemoryTable'; + +/** A view for entering and reviewing notes */ +export const Notes = React.memo( + ({ associateNote, getNotesByIds, getNewNoteId, noteIds, updateNote }) => { + const [newNote, setNewNote] = useState(''); + + return ( + + + + + + + + + + + + ); + } +); + +Notes.displayName = 'Notes'; diff --git a/x-pack/plugins/siem/public/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/notes/note_card/__snapshots__/note_card_body.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/notes/note_card/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/notes/note_card/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/notes/note_card/index.test.tsx rename to x-pack/plugins/siem/public/timelines/components/notes/note_card/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/notes/note_card/index.tsx b/x-pack/plugins/siem/public/timelines/components/notes/note_card/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/notes/note_card/index.tsx rename to x-pack/plugins/siem/public/timelines/components/notes/note_card/index.tsx diff --git a/x-pack/plugins/siem/public/components/notes/note_card/note_card_body.test.tsx b/x-pack/plugins/siem/public/timelines/components/notes/note_card/note_card_body.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/notes/note_card/note_card_body.test.tsx rename to x-pack/plugins/siem/public/timelines/components/notes/note_card/note_card_body.test.tsx diff --git a/x-pack/plugins/siem/public/components/notes/note_card/note_card_body.tsx b/x-pack/plugins/siem/public/timelines/components/notes/note_card/note_card_body.tsx similarity index 82% rename from x-pack/plugins/siem/public/components/notes/note_card/note_card_body.tsx rename to x-pack/plugins/siem/public/timelines/components/notes/note_card/note_card_body.tsx index 4463f8d4ff6024..f846ead810ff2c 100644 --- a/x-pack/plugins/siem/public/components/notes/note_card/note_card_body.tsx +++ b/x-pack/plugins/siem/public/timelines/components/notes/note_card/note_card_body.tsx @@ -8,9 +8,9 @@ import { EuiPanel, EuiToolTip } from '@elastic/eui'; import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; -import { WithCopyToClipboard } from '../../../lib/clipboard/with_copy_to_clipboard'; -import { Markdown } from '../../markdown'; -import { WithHoverActions } from '../../with_hover_actions'; +import { WithCopyToClipboard } from '../../../../common/lib/clipboard/with_copy_to_clipboard'; +import { Markdown } from '../../../../common/components/markdown'; +import { WithHoverActions } from '../../../../common/components/with_hover_actions'; import * as i18n from '../translations'; const BodyContainer = styled(EuiPanel)` diff --git a/x-pack/plugins/siem/public/components/notes/note_card/note_card_header.test.tsx b/x-pack/plugins/siem/public/timelines/components/notes/note_card/note_card_header.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/notes/note_card/note_card_header.test.tsx rename to x-pack/plugins/siem/public/timelines/components/notes/note_card/note_card_header.test.tsx diff --git a/x-pack/plugins/siem/public/components/notes/note_card/note_card_header.tsx b/x-pack/plugins/siem/public/timelines/components/notes/note_card/note_card_header.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/notes/note_card/note_card_header.tsx rename to x-pack/plugins/siem/public/timelines/components/notes/note_card/note_card_header.tsx diff --git a/x-pack/plugins/siem/public/components/notes/note_card/note_created.test.tsx b/x-pack/plugins/siem/public/timelines/components/notes/note_card/note_created.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/notes/note_card/note_created.test.tsx rename to x-pack/plugins/siem/public/timelines/components/notes/note_card/note_created.test.tsx diff --git a/x-pack/plugins/siem/public/components/notes/note_card/note_created.tsx b/x-pack/plugins/siem/public/timelines/components/notes/note_card/note_created.tsx similarity index 89% rename from x-pack/plugins/siem/public/components/notes/note_card/note_created.tsx rename to x-pack/plugins/siem/public/timelines/components/notes/note_card/note_created.tsx index cdd0406c714509..dc97373660bd1c 100644 --- a/x-pack/plugins/siem/public/components/notes/note_card/note_created.tsx +++ b/x-pack/plugins/siem/public/timelines/components/notes/note_card/note_created.tsx @@ -8,7 +8,7 @@ import { FormattedRelative } from '@kbn/i18n/react'; import React from 'react'; import styled from 'styled-components'; -import { LocalizedDateTooltip } from '../../localized_date_tooltip'; +import { LocalizedDateTooltip } from '../../../../common/components/localized_date_tooltip'; const NoteCreatedContainer = styled.span` user-select: none; diff --git a/x-pack/plugins/siem/public/timelines/components/notes/note_cards/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/notes/note_cards/index.test.tsx new file mode 100644 index 00000000000000..fa63eb625f2839 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/notes/note_cards/index.test.tsx @@ -0,0 +1,137 @@ +/* + * 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 { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { ThemeProvider } from 'styled-components'; +import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; + +import { Note } from '../../../../common/lib/note'; + +import { NoteCards } from '.'; + +describe('NoteCards', () => { + const noteIds = ['abc', 'def']; + const theme = () => ({ eui: euiDarkVars, darkMode: true }); + + const getNotesByIds = (_: string[]): Note[] => [ + { + created: new Date(), + id: 'abc', + lastEdit: null, + note: 'a fake note', + saveObjectId: null, + user: 'elastic', + version: null, + }, + { + created: new Date(), + id: 'def', + lastEdit: null, + note: 'another fake note', + saveObjectId: null, + user: 'elastic', + version: null, + }, + ]; + + test('it renders the notes column when noteIds are specified', () => { + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="notes"]').exists()).toEqual(true); + }); + + test('it does NOT render the notes column when noteIds are NOT specified', () => { + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="notes"]').exists()).toEqual(false); + }); + + test('renders note cards', () => { + const wrapper = mountWithIntl( + + + + ); + + expect( + wrapper + .find('[data-test-subj="note-card"]') + .find('[data-test-subj="note-card-body"]') + .find('[data-test-subj="markdown-root"]') + .first() + .text() + ).toEqual(getNotesByIds(noteIds)[0].note); + }); + + test('it shows controls for adding notes when showAddNote is true', () => { + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="add-note"]').exists()).toEqual(true); + }); + + test('it does NOT show controls for adding notes when showAddNote is false', () => { + const wrapper = mountWithIntl( + + + + ); + + expect(wrapper.find('[data-test-subj="add-note"]').exists()).toEqual(false); + }); +}); diff --git a/x-pack/plugins/siem/public/timelines/components/notes/note_cards/index.tsx b/x-pack/plugins/siem/public/timelines/components/notes/note_cards/index.tsx new file mode 100644 index 00000000000000..346d77b14cd90e --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/notes/note_cards/index.tsx @@ -0,0 +1,106 @@ +/* + * 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 { EuiFlexGroup, EuiPanel } from '@elastic/eui'; +import React, { useState, useCallback } from 'react'; +import styled from 'styled-components'; + +import { Note } from '../../../../common/lib/note'; +import { AddNote } from '../add_note'; +import { AssociateNote, GetNewNoteId, UpdateNote } from '../helpers'; +import { NoteCard } from '../note_card'; + +const AddNoteContainer = styled.div``; +AddNoteContainer.displayName = 'AddNoteContainer'; + +const NoteContainer = styled.div` + margin-top: 5px; +`; +NoteContainer.displayName = 'NoteContainer'; + +interface NoteCardsCompProps { + children: React.ReactNode; +} + +const NoteCardsComp = React.memo(({ children }) => ( + + {children} + +)); +NoteCardsComp.displayName = 'NoteCardsComp'; + +const NotesContainer = styled(EuiFlexGroup)` + padding: 0 5px; + margin-bottom: 5px; +`; +NotesContainer.displayName = 'NotesContainer'; + +interface Props { + associateNote: AssociateNote; + getNotesByIds: (noteIds: string[]) => Note[]; + getNewNoteId: GetNewNoteId; + noteIds: string[]; + showAddNote: boolean; + toggleShowAddNote: () => void; + updateNote: UpdateNote; +} + +/** A view for entering and reviewing notes */ +export const NoteCards = React.memo( + ({ + associateNote, + getNotesByIds, + getNewNoteId, + noteIds, + showAddNote, + toggleShowAddNote, + updateNote, + }) => { + const [newNote, setNewNote] = useState(''); + + const associateNoteAndToggleShow = useCallback( + (noteId: string) => { + associateNote(noteId); + toggleShowAddNote(); + }, + [associateNote, toggleShowAddNote] + ); + + return ( + + {noteIds.length ? ( + + {getNotesByIds(noteIds).map(note => ( + + + + ))} + + ) : null} + + {showAddNote ? ( + + + + ) : null} + + ); + } +); + +NoteCards.displayName = 'NoteCards'; diff --git a/x-pack/plugins/siem/public/components/notes/translations.ts b/x-pack/plugins/siem/public/timelines/components/notes/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/notes/translations.ts rename to x-pack/plugins/siem/public/timelines/components/notes/translations.ts diff --git a/x-pack/plugins/siem/public/components/open_timeline/constants.ts b/x-pack/plugins/siem/public/timelines/components/open_timeline/constants.ts similarity index 100% rename from x-pack/plugins/siem/public/components/open_timeline/constants.ts rename to x-pack/plugins/siem/public/timelines/components/open_timeline/constants.ts diff --git a/x-pack/plugins/siem/public/components/open_timeline/delete_timeline_modal/delete_timeline_modal.test.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/delete_timeline_modal/delete_timeline_modal.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/open_timeline/delete_timeline_modal/delete_timeline_modal.test.tsx rename to x-pack/plugins/siem/public/timelines/components/open_timeline/delete_timeline_modal/delete_timeline_modal.test.tsx diff --git a/x-pack/plugins/siem/public/components/open_timeline/delete_timeline_modal/delete_timeline_modal.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/delete_timeline_modal/delete_timeline_modal.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/open_timeline/delete_timeline_modal/delete_timeline_modal.tsx rename to x-pack/plugins/siem/public/timelines/components/open_timeline/delete_timeline_modal/delete_timeline_modal.tsx diff --git a/x-pack/plugins/siem/public/components/open_timeline/delete_timeline_modal/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/delete_timeline_modal/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/open_timeline/delete_timeline_modal/index.test.tsx rename to x-pack/plugins/siem/public/timelines/components/open_timeline/delete_timeline_modal/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/open_timeline/delete_timeline_modal/index.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/delete_timeline_modal/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/open_timeline/delete_timeline_modal/index.tsx rename to x-pack/plugins/siem/public/timelines/components/open_timeline/delete_timeline_modal/index.tsx diff --git a/x-pack/plugins/siem/public/components/open_timeline/edit_timeline_actions.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/edit_timeline_actions.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/open_timeline/edit_timeline_actions.tsx rename to x-pack/plugins/siem/public/timelines/components/open_timeline/edit_timeline_actions.tsx diff --git a/x-pack/plugins/siem/public/components/open_timeline/edit_timeline_batch_actions.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/edit_timeline_batch_actions.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/open_timeline/edit_timeline_batch_actions.tsx rename to x-pack/plugins/siem/public/timelines/components/open_timeline/edit_timeline_batch_actions.tsx diff --git a/x-pack/plugins/siem/public/components/open_timeline/export_timeline/export_timeline.test.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/export_timeline/export_timeline.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/open_timeline/export_timeline/export_timeline.test.tsx rename to x-pack/plugins/siem/public/timelines/components/open_timeline/export_timeline/export_timeline.test.tsx diff --git a/x-pack/plugins/siem/public/components/open_timeline/export_timeline/export_timeline.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/export_timeline/export_timeline.tsx similarity index 90% rename from x-pack/plugins/siem/public/components/open_timeline/export_timeline/export_timeline.tsx rename to x-pack/plugins/siem/public/timelines/components/open_timeline/export_timeline/export_timeline.tsx index ebfd5c18bd5dc2..43ef3bccbea56e 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/export_timeline/export_timeline.tsx +++ b/x-pack/plugins/siem/public/timelines/components/open_timeline/export_timeline/export_timeline.tsx @@ -6,9 +6,12 @@ import React, { useCallback } from 'react'; import uuid from 'uuid'; -import { GenericDownloader, ExportSelectedData } from '../../generic_downloader'; +import { + GenericDownloader, + ExportSelectedData, +} from '../../../../common/components/generic_downloader'; import * as i18n from '../translations'; -import { useStateToaster } from '../../toasters'; +import { useStateToaster } from '../../../../common/components/toasters'; const ExportTimeline: React.FC<{ exportedIds: string[] | undefined; diff --git a/x-pack/plugins/siem/public/components/open_timeline/export_timeline/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/export_timeline/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/open_timeline/export_timeline/index.test.tsx rename to x-pack/plugins/siem/public/timelines/components/open_timeline/export_timeline/index.test.tsx diff --git a/x-pack/plugins/siem/public/timelines/components/open_timeline/export_timeline/index.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/export_timeline/index.tsx new file mode 100644 index 00000000000000..7bac3229c81732 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/open_timeline/export_timeline/index.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState, useCallback } from 'react'; +import { DeleteTimelines } from '../types'; + +import { TimelineDownloader } from './export_timeline'; +import { DeleteTimelineModalOverlay } from '../delete_timeline_modal'; +import { exportSelectedTimeline } from '../../../containers/api'; + +export interface ExportTimeline { + disableExportTimelineDownloader: () => void; + enableExportTimelineDownloader: () => void; + isEnableDownloader: boolean; +} + +export const useExportTimeline = (): ExportTimeline => { + const [isEnableDownloader, setIsEnableDownloader] = useState(false); + + const enableExportTimelineDownloader = useCallback(() => { + setIsEnableDownloader(true); + }, []); + + const disableExportTimelineDownloader = useCallback(() => { + setIsEnableDownloader(false); + }, []); + + return { + disableExportTimelineDownloader, + enableExportTimelineDownloader, + isEnableDownloader, + }; +}; + +const EditTimelineActionsComponent: React.FC<{ + deleteTimelines: DeleteTimelines | undefined; + ids: string[]; + isEnableDownloader: boolean; + isDeleteTimelineModalOpen: boolean; + onComplete: () => void; + title: string; +}> = ({ + deleteTimelines, + ids, + isEnableDownloader, + isDeleteTimelineModalOpen, + onComplete, + title, +}) => ( + <> + + {deleteTimelines != null && ( + + )} + +); + +export const EditTimelineActions = React.memo(EditTimelineActionsComponent); +export const EditOneTimelineAction = React.memo(EditTimelineActionsComponent); diff --git a/x-pack/plugins/siem/public/components/open_timeline/export_timeline/mocks.ts b/x-pack/plugins/siem/public/timelines/components/open_timeline/export_timeline/mocks.ts similarity index 100% rename from x-pack/plugins/siem/public/components/open_timeline/export_timeline/mocks.ts rename to x-pack/plugins/siem/public/timelines/components/open_timeline/export_timeline/mocks.ts diff --git a/x-pack/plugins/siem/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/siem/public/timelines/components/open_timeline/helpers.test.ts new file mode 100644 index 00000000000000..e6db9df61b9020 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/open_timeline/helpers.test.ts @@ -0,0 +1,893 @@ +/* + * 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 { cloneDeep, omit } from 'lodash/fp'; +import { Dispatch } from 'redux'; + +import { + mockTimelineResults, + mockTimelineResult, + mockTimelineModel, +} from '../../../common/mock/timeline_results'; +import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; +import { setTimelineRangeDatePicker as dispatchSetTimelineRangeDatePicker } from '../../../common/store/inputs/actions'; +import { + setKqlFilterQueryDraft as dispatchSetKqlFilterQueryDraft, + applyKqlFilterQuery as dispatchApplyKqlFilterQuery, + addTimeline as dispatchAddTimeline, + addNote as dispatchAddGlobalTimelineNote, +} from '../../../timelines/store/timeline/actions'; +import { + addNotes as dispatchAddNotes, + updateNote as dispatchUpdateNote, +} from '../../../common/store/app/actions'; +import { + defaultTimelineToTimelineModel, + getNotesCount, + getPinnedEventCount, + isUntitled, + omitTypenameInTimeline, + dispatchUpdateTimeline, +} from './helpers'; +import { OpenTimelineResult, DispatchUpdateTimeline } from './types'; +import { KueryFilterQueryKind } from '../../../common/store/model'; +import { Note } from '../../../common/lib/note'; +import moment from 'moment'; +import sinon from 'sinon'; +import { TimelineType } from '../../../../common/types/timeline'; + +jest.mock('../../../common/store/inputs/actions'); +jest.mock('../../../timelines/store/timeline/actions'); +jest.mock('../../../common/store/app/actions'); +jest.mock('uuid', () => { + return { + v1: jest.fn(() => 'uuid.v1()'), + v4: jest.fn(() => 'uuid.v4()'), + }; +}); + +describe('helpers', () => { + let mockResults: OpenTimelineResult[]; + + beforeEach(() => { + mockResults = cloneDeep(mockTimelineResults); + }); + + describe('#getPinnedEventCount', () => { + test('returns 6 when the timeline has 6 pinned events', () => { + const with6Events = mockResults[0]; + + expect(getPinnedEventCount(with6Events)).toEqual(6); + }); + + test('returns zero when the timeline has an empty collection of pinned events', () => { + const withPinnedEvents = { ...mockResults[0], pinnedEventIds: {} }; + + expect(getPinnedEventCount(withPinnedEvents)).toEqual(0); + }); + + test('returns zero when pinnedEventIds is undefined', () => { + const withPinnedEvents = omit('pinnedEventIds', { ...mockResults[0] }); + + expect(getPinnedEventCount(withPinnedEvents)).toEqual(0); + }); + + test('returns zero when pinnedEventIds is null', () => { + const withPinnedEvents = omit('pinnedEventIds', { ...mockResults[0] }); + + expect(getPinnedEventCount(withPinnedEvents)).toEqual(0); + }); + }); + + describe('#getNotesCount', () => { + test('returns a total of 4 notes when the timeline has 4 notes (event1 [2] + event2 [1] + global [1])', () => { + const with4Notes = mockResults[0]; + + expect(getNotesCount(with4Notes)).toEqual(4); + }); + + test('returns 1 note (global [1]) when eventIdToNoteIds is undefined', () => { + const with1Note = omit('eventIdToNoteIds', { ...mockResults[0] }); + + expect(getNotesCount(with1Note)).toEqual(1); + }); + + test('returns 1 note (global [1]) when eventIdToNoteIds is null', () => { + const eventIdToNoteIdsIsNull = { + ...mockResults[0], + eventIdToNoteIds: null, + }; + expect(getNotesCount(eventIdToNoteIdsIsNull)).toEqual(1); + }); + + test('returns 1 note (global [1]) when eventIdToNoteIds is empty', () => { + const eventIdToNoteIdsIsEmpty = { + ...mockResults[0], + eventIdToNoteIds: {}, + }; + expect(getNotesCount(eventIdToNoteIdsIsEmpty)).toEqual(1); + }); + + test('returns 3 notes (event1 [2] + event2 [1]) when noteIds is undefined', () => { + const noteIdsIsUndefined = omit('noteIds', { ...mockResults[0] }); + + expect(getNotesCount(noteIdsIsUndefined)).toEqual(3); + }); + + test('returns 3 notes (event1 [2] + event2 [1]) when noteIds is null', () => { + const noteIdsIsNull = { + ...mockResults[0], + noteIds: null, + }; + + expect(getNotesCount(noteIdsIsNull)).toEqual(3); + }); + + test('returns 3 notes (event1 [2] + event2 [1]) when noteIds is empty', () => { + const noteIdsIsEmpty = { + ...mockResults[0], + noteIds: [], + }; + + expect(getNotesCount(noteIdsIsEmpty)).toEqual(3); + }); + + test('returns 0 when eventIdToNoteIds and noteIds are undefined', () => { + const eventIdToNoteIdsAndNoteIdsUndefined = omit(['eventIdToNoteIds', 'noteIds'], { + ...mockResults[0], + }); + + expect(getNotesCount(eventIdToNoteIdsAndNoteIdsUndefined)).toEqual(0); + }); + + test('returns 0 when eventIdToNoteIds and noteIds are null', () => { + const eventIdToNoteIdsAndNoteIdsNull = { + ...mockResults[0], + eventIdToNoteIds: null, + noteIds: null, + }; + + expect(getNotesCount(eventIdToNoteIdsAndNoteIdsNull)).toEqual(0); + }); + + test('returns 0 when eventIdToNoteIds and noteIds are empty', () => { + const eventIdToNoteIdsAndNoteIdsEmpty = { + ...mockResults[0], + eventIdToNoteIds: {}, + noteIds: [], + }; + + expect(getNotesCount(eventIdToNoteIdsAndNoteIdsEmpty)).toEqual(0); + }); + }); + + describe('#isUntitled', () => { + test('returns true when title is undefined', () => { + const titleIsUndefined = omit('title', { + ...mockResults[0], + }); + + expect(isUntitled(titleIsUndefined)).toEqual(true); + }); + + test('returns true when title is null', () => { + const titleIsNull = { + ...mockResults[0], + title: null, + }; + + expect(isUntitled(titleIsNull)).toEqual(true); + }); + + test('returns true when title is just whitespace', () => { + const titleIsWitespace = { + ...mockResults[0], + title: ' ', + }; + + expect(isUntitled(titleIsWitespace)).toEqual(true); + }); + + test('returns false when title is surrounded by whitespace', () => { + const titleIsWitespace = { + ...mockResults[0], + title: ' the king of the north ', + }; + + expect(isUntitled(titleIsWitespace)).toEqual(false); + }); + + test('returns false when title is NOT surrounded by whitespace', () => { + const titleIsWitespace = { + ...mockResults[0], + title: 'in the beginning...', + }; + + expect(isUntitled(titleIsWitespace)).toEqual(false); + }); + }); + + describe('#defaultTimelineToTimelineModel', () => { + test('if title is null, we should get the default title', () => { + const timeline = { + savedObjectId: 'savedObject-1', + title: null, + version: '1', + }; + + const newTimeline = defaultTimelineToTimelineModel(timeline, false); + expect(newTimeline).toEqual({ + columns: [ + { + columnHeaderType: 'not-filtered', + id: '@timestamp', + width: 190, + }, + { + columnHeaderType: 'not-filtered', + id: 'message', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'event.category', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'event.action', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'host.name', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'source.ip', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'destination.ip', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'user.name', + width: 180, + }, + ], + dataProviders: [], + dateRange: { + end: 0, + start: 0, + }, + description: '', + deletedEventIds: [], + eventIdToNoteIds: {}, + eventType: 'all', + filters: [], + highlightedDropAndProviderId: '', + historyIds: [], + id: 'savedObject-1', + isFavorite: false, + isLive: false, + isSelectAllChecked: false, + isLoading: false, + isSaving: false, + itemsPerPage: 25, + itemsPerPageOptions: [10, 25, 50, 100], + kqlMode: 'filter', + kqlQuery: { + filterQuery: null, + filterQueryDraft: null, + }, + loadingEventIds: [], + noteIds: [], + pinnedEventIds: {}, + pinnedEventsSaveObject: {}, + savedObjectId: 'savedObject-1', + selectedEventIds: {}, + show: false, + showCheckboxes: false, + showRowRenderers: true, + sort: { + columnId: '@timestamp', + sortDirection: 'desc', + }, + title: '', + timelineType: TimelineType.default, + templateTimelineId: null, + templateTimelineVersion: null, + version: '1', + width: 1100, + }); + }); + test('if columns are null, we should get the default columns', () => { + const timeline = { + savedObjectId: 'savedObject-1', + columns: null, + version: '1', + }; + + const newTimeline = defaultTimelineToTimelineModel(timeline, false); + expect(newTimeline).toEqual({ + columns: [ + { + columnHeaderType: 'not-filtered', + id: '@timestamp', + width: 190, + }, + { + columnHeaderType: 'not-filtered', + id: 'message', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'event.category', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'event.action', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'host.name', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'source.ip', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'destination.ip', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'user.name', + width: 180, + }, + ], + dataProviders: [], + dateRange: { + end: 0, + start: 0, + }, + description: '', + deletedEventIds: [], + eventIdToNoteIds: {}, + eventType: 'all', + filters: [], + highlightedDropAndProviderId: '', + historyIds: [], + id: 'savedObject-1', + isFavorite: false, + isLive: false, + isSelectAllChecked: false, + isLoading: false, + isSaving: false, + itemsPerPage: 25, + itemsPerPageOptions: [10, 25, 50, 100], + kqlMode: 'filter', + kqlQuery: { + filterQuery: null, + filterQueryDraft: null, + }, + loadingEventIds: [], + noteIds: [], + pinnedEventIds: {}, + pinnedEventsSaveObject: {}, + savedObjectId: 'savedObject-1', + selectedEventIds: {}, + show: false, + showCheckboxes: false, + showRowRenderers: true, + sort: { + columnId: '@timestamp', + sortDirection: 'desc', + }, + title: '', + timelineType: TimelineType.default, + templateTimelineId: null, + templateTimelineVersion: null, + version: '1', + width: 1100, + }); + }); + test('should merge columns when event.action is deleted without two extra column names of user.name', () => { + const timeline = { + savedObjectId: 'savedObject-1', + columns: timelineDefaults.columns.filter(column => column.id !== 'event.action'), + version: '1', + }; + + const newTimeline = defaultTimelineToTimelineModel(timeline, false); + expect(newTimeline).toEqual({ + savedObjectId: 'savedObject-1', + columns: [ + { + columnHeaderType: 'not-filtered', + id: '@timestamp', + width: 190, + }, + { + columnHeaderType: 'not-filtered', + id: 'message', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'event.category', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'host.name', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'source.ip', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'destination.ip', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'user.name', + width: 180, + }, + ], + version: '1', + dataProviders: [], + description: '', + deletedEventIds: [], + eventIdToNoteIds: {}, + eventType: 'all', + filters: [], + highlightedDropAndProviderId: '', + historyIds: [], + isFavorite: false, + isLive: false, + isSelectAllChecked: false, + isLoading: false, + isSaving: false, + itemsPerPage: 25, + itemsPerPageOptions: [10, 25, 50, 100], + kqlMode: 'filter', + kqlQuery: { + filterQuery: null, + filterQueryDraft: null, + }, + loadingEventIds: [], + title: '', + timelineType: TimelineType.default, + templateTimelineId: null, + templateTimelineVersion: null, + noteIds: [], + pinnedEventIds: {}, + pinnedEventsSaveObject: {}, + dateRange: { + start: 0, + end: 0, + }, + selectedEventIds: {}, + show: false, + showCheckboxes: false, + showRowRenderers: true, + sort: { + columnId: '@timestamp', + sortDirection: 'desc', + }, + width: 1100, + id: 'savedObject-1', + }); + }); + + test('should merge filters object back with json object', () => { + const timeline = { + savedObjectId: 'savedObject-1', + columns: timelineDefaults.columns.filter(column => column.id !== 'event.action'), + filters: [ + { + meta: { + alias: null, + controlledBy: null, + disabled: false, + index: null, + key: 'event.category', + negate: false, + params: '{"query":"file"}', + type: 'phrase', + value: null, + }, + query: '{"match_phrase":{"event.category":"file"}}', + exists: null, + }, + { + meta: { + alias: null, + controlledBy: null, + disabled: false, + index: null, + key: '@timestamp', + negate: false, + params: null, + type: 'exists', + value: 'exists', + }, + query: null, + exists: '{"field":"@timestamp"}', + }, + ], + version: '1', + }; + + const newTimeline = defaultTimelineToTimelineModel(timeline, false); + expect(newTimeline).toEqual({ + savedObjectId: 'savedObject-1', + columns: [ + { + columnHeaderType: 'not-filtered', + id: '@timestamp', + width: 190, + }, + { + columnHeaderType: 'not-filtered', + id: 'message', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'event.category', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'host.name', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'source.ip', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'destination.ip', + width: 180, + }, + { + columnHeaderType: 'not-filtered', + id: 'user.name', + width: 180, + }, + ], + version: '1', + dataProviders: [], + description: '', + deletedEventIds: [], + eventIdToNoteIds: {}, + eventType: 'all', + filters: [ + { + $state: { + store: 'appState', + }, + meta: { + alias: null, + controlledBy: null, + disabled: false, + index: null, + key: 'event.category', + negate: false, + params: { + query: 'file', + }, + type: 'phrase', + value: null, + }, + query: { + match_phrase: { + 'event.category': 'file', + }, + }, + }, + { + $state: { + store: 'appState', + }, + exists: { + field: '@timestamp', + }, + meta: { + alias: null, + controlledBy: null, + disabled: false, + index: null, + key: '@timestamp', + negate: false, + params: null, + type: 'exists', + value: 'exists', + }, + }, + ], + highlightedDropAndProviderId: '', + historyIds: [], + isFavorite: false, + isLive: false, + isSelectAllChecked: false, + isLoading: false, + isSaving: false, + itemsPerPage: 25, + itemsPerPageOptions: [10, 25, 50, 100], + kqlMode: 'filter', + kqlQuery: { + filterQuery: null, + filterQueryDraft: null, + }, + loadingEventIds: [], + title: '', + timelineType: TimelineType.default, + templateTimelineId: null, + templateTimelineVersion: null, + noteIds: [], + pinnedEventIds: {}, + pinnedEventsSaveObject: {}, + dateRange: { + start: 0, + end: 0, + }, + selectedEventIds: {}, + show: false, + showCheckboxes: false, + showRowRenderers: true, + sort: { + columnId: '@timestamp', + sortDirection: 'desc', + }, + width: 1100, + id: 'savedObject-1', + }); + }); + }); + + describe('omitTypenameInTimeline', () => { + test('it does not modify the passed in timeline if no __typename exists', () => { + const result = omitTypenameInTimeline(mockTimelineResult); + + expect(result).toEqual(mockTimelineResult); + }); + + test('it returns timeline with __typename removed when it exists', () => { + const mockTimeline = { + ...mockTimelineResult, + __typename: 'something, something', + }; + const result = omitTypenameInTimeline(mockTimeline); + const expectedTimeline = { + ...mockTimeline, + __typename: undefined, + }; + + expect(result).toEqual(expectedTimeline); + }); + }); + + describe('dispatchUpdateTimeline', () => { + const dispatch = jest.fn() as Dispatch; + const anchor = '2020-03-27T20:34:51.337Z'; + const unix = moment(anchor).valueOf(); + let clock: sinon.SinonFakeTimers; + let timelineDispatch: DispatchUpdateTimeline; + + beforeEach(() => { + jest.clearAllMocks(); + + clock = sinon.useFakeTimers(unix); + timelineDispatch = dispatchUpdateTimeline(dispatch); + }); + + afterEach(function() { + clock.restore(); + }); + + test('it invokes date range picker dispatch', () => { + timelineDispatch({ + duplicate: true, + id: 'timeline-1', + from: 1585233356356, + to: 1585233716356, + notes: [], + timeline: mockTimelineModel, + })(); + + expect(dispatchSetTimelineRangeDatePicker).toHaveBeenCalledWith({ + from: 1585233356356, + to: 1585233716356, + }); + }); + + test('it invokes add timeline dispatch', () => { + timelineDispatch({ + duplicate: true, + id: 'timeline-1', + from: 1585233356356, + to: 1585233716356, + notes: [], + timeline: mockTimelineModel, + })(); + + expect(dispatchAddTimeline).toHaveBeenCalledWith({ + id: 'timeline-1', + timeline: mockTimelineModel, + }); + }); + + test('it does not invoke kql filter query dispatches if timeline.kqlQuery.filterQuery is null', () => { + timelineDispatch({ + duplicate: true, + id: 'timeline-1', + from: 1585233356356, + to: 1585233716356, + notes: [], + timeline: mockTimelineModel, + })(); + + expect(dispatchSetKqlFilterQueryDraft).not.toHaveBeenCalled(); + expect(dispatchApplyKqlFilterQuery).not.toHaveBeenCalled(); + }); + + test('it does not invoke notes dispatch if duplicate is true', () => { + timelineDispatch({ + duplicate: true, + id: 'timeline-1', + from: 1585233356356, + to: 1585233716356, + notes: [], + timeline: mockTimelineModel, + })(); + + expect(dispatchAddNotes).not.toHaveBeenCalled(); + }); + + test('it does not invoke kql filter query dispatches if timeline.kqlQuery.kuery is null', () => { + const mockTimeline = { + ...mockTimelineModel, + kqlQuery: { + filterQuery: { + kuery: null, + serializedQuery: 'some-serialized-query', + }, + filterQueryDraft: null, + }, + }; + timelineDispatch({ + duplicate: true, + id: 'timeline-1', + from: 1585233356356, + to: 1585233716356, + notes: [], + timeline: mockTimeline, + })(); + + expect(dispatchSetKqlFilterQueryDraft).not.toHaveBeenCalled(); + expect(dispatchApplyKqlFilterQuery).not.toHaveBeenCalled(); + }); + + test('it invokes kql filter query dispatches if timeline.kqlQuery.filterQuery.kuery is not null', () => { + const mockTimeline = { + ...mockTimelineModel, + kqlQuery: { + filterQuery: { + kuery: { expression: 'expression', kind: 'kuery' as KueryFilterQueryKind }, + serializedQuery: 'some-serialized-query', + }, + filterQueryDraft: null, + }, + }; + timelineDispatch({ + duplicate: true, + id: 'timeline-1', + from: 1585233356356, + to: 1585233716356, + notes: [], + timeline: mockTimeline, + })(); + + expect(dispatchSetKqlFilterQueryDraft).toHaveBeenCalledWith({ + id: 'timeline-1', + filterQueryDraft: { + kind: 'kuery', + expression: 'expression', + }, + }); + expect(dispatchApplyKqlFilterQuery).toHaveBeenCalledWith({ + id: 'timeline-1', + filterQuery: { + kuery: { + kind: 'kuery', + expression: 'expression', + }, + serializedQuery: 'some-serialized-query', + }, + }); + }); + + test('it invokes dispatchAddNotes if duplicate is false', () => { + timelineDispatch({ + duplicate: false, + id: 'timeline-1', + from: 1585233356356, + to: 1585233716356, + notes: [ + { + created: 1585233356356, + updated: 1585233356356, + noteId: 'note-id', + note: 'I am a note', + }, + ], + timeline: mockTimelineModel, + })(); + + expect(dispatchAddGlobalTimelineNote).not.toHaveBeenCalled(); + expect(dispatchUpdateNote).not.toHaveBeenCalled(); + expect(dispatchAddNotes).toHaveBeenCalledWith({ + notes: [ + { + created: new Date('2020-03-26T14:35:56.356Z'), + id: 'note-id', + lastEdit: new Date('2020-03-26T14:35:56.356Z'), + note: 'I am a note', + user: 'unknown', + saveObjectId: 'note-id', + version: undefined, + }, + ], + }); + }); + + test('it invokes dispatch to create a timeline note if duplicate is true and ruleNote exists', () => { + timelineDispatch({ + duplicate: true, + id: 'timeline-1', + from: 1585233356356, + to: 1585233716356, + notes: [], + timeline: mockTimelineModel, + ruleNote: '# this would be some markdown', + })(); + const expectedNote: Note = { + created: new Date(anchor), + id: 'uuid.v4()', + lastEdit: null, + note: '# this would be some markdown', + saveObjectId: null, + user: 'elastic', + version: null, + }; + + expect(dispatchAddNotes).not.toHaveBeenCalled(); + expect(dispatchUpdateNote).toHaveBeenCalledWith({ note: expectedNote }); + expect(dispatchAddGlobalTimelineNote).toHaveBeenLastCalledWith({ + id: 'timeline-1', + noteId: 'uuid.v4()', + }); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/siem/public/timelines/components/open_timeline/helpers.ts new file mode 100644 index 00000000000000..df433f147490e4 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/open_timeline/helpers.ts @@ -0,0 +1,318 @@ +/* + * 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 ApolloClient from 'apollo-client'; +import { getOr, set, isEmpty } from 'lodash/fp'; +import { Action } from 'typescript-fsa'; +import uuid from 'uuid'; +import { Dispatch } from 'redux'; +import { oneTimelineQuery } from '../../containers/one/index.gql_query'; +import { TimelineResult, GetOneTimeline, NoteResult } from '../../../graphql/types'; +import { + addNotes as dispatchAddNotes, + updateNote as dispatchUpdateNote, +} from '../../../common/store/app/actions'; +import { setTimelineRangeDatePicker as dispatchSetTimelineRangeDatePicker } from '../../../common/store/inputs/actions'; +import { + setKqlFilterQueryDraft as dispatchSetKqlFilterQueryDraft, + applyKqlFilterQuery as dispatchApplyKqlFilterQuery, + addTimeline as dispatchAddTimeline, + addNote as dispatchAddGlobalTimelineNote, +} from '../../../timelines/store/timeline/actions'; + +import { ColumnHeaderOptions, TimelineModel } from '../../../timelines/store/timeline/model'; +import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; +import { + defaultColumnHeaderType, + defaultHeaders, +} from '../timeline/body/column_headers/default_headers'; +import { + DEFAULT_DATE_COLUMN_MIN_WIDTH, + DEFAULT_COLUMN_MIN_WIDTH, +} from '../timeline/body/constants'; + +import { OpenTimelineResult, UpdateTimeline, DispatchUpdateTimeline } from './types'; +import { getTimeRangeSettings } from '../../../common/utils/default_date_settings'; +import { createNote } from '../notes/helpers'; + +export const OPEN_TIMELINE_CLASS_NAME = 'open-timeline'; + +/** Returns a count of the pinned events in a timeline */ +export const getPinnedEventCount = ({ pinnedEventIds }: OpenTimelineResult): number => + pinnedEventIds != null ? Object.keys(pinnedEventIds).length : 0; + +/** Returns the sum of all notes added to pinned events and notes applicable to the timeline */ +export const getNotesCount = ({ eventIdToNoteIds, noteIds }: OpenTimelineResult): number => { + const eventNoteCount = + eventIdToNoteIds != null + ? Object.keys(eventIdToNoteIds).reduce( + (count, eventId) => count + eventIdToNoteIds[eventId].length, + 0 + ) + : 0; + + const globalNoteCount = noteIds != null ? noteIds.length : 0; + + return eventNoteCount + globalNoteCount; +}; + +/** Returns true if the timeline is untitlied */ +export const isUntitled = ({ title }: OpenTimelineResult): boolean => + title == null || title.trim().length === 0; + +const omitTypename = (key: string, value: keyof TimelineModel) => + key === '__typename' ? undefined : value; + +export const omitTypenameInTimeline = (timeline: TimelineResult): TimelineResult => + JSON.parse(JSON.stringify(timeline), omitTypename); + +const parseString = (params: string) => { + try { + return JSON.parse(params); + } catch { + return params; + } +}; + +export const defaultTimelineToTimelineModel = ( + timeline: TimelineResult, + duplicate: boolean +): TimelineModel => { + return Object.entries({ + ...timeline, + columns: + timeline.columns != null + ? timeline.columns.map(col => { + const timelineCols: ColumnHeaderOptions = { + ...col, + columnHeaderType: defaultColumnHeaderType, + id: col.id != null ? col.id : 'unknown', + placeholder: col.placeholder != null ? col.placeholder : undefined, + category: col.category != null ? col.category : undefined, + description: col.description != null ? col.description : undefined, + example: col.example != null ? col.example : undefined, + type: col.type != null ? col.type : undefined, + aggregatable: col.aggregatable != null ? col.aggregatable : undefined, + width: + col.id === '@timestamp' ? DEFAULT_DATE_COLUMN_MIN_WIDTH : DEFAULT_COLUMN_MIN_WIDTH, + }; + return timelineCols; + }) + : defaultHeaders, + eventIdToNoteIds: duplicate + ? {} + : timeline.eventIdToNoteIds != null + ? timeline.eventIdToNoteIds.reduce((acc, note) => { + if (note.eventId != null) { + const eventNotes = getOr([], note.eventId, acc); + return { ...acc, [note.eventId]: [...eventNotes, note.noteId] }; + } + return acc; + }, {}) + : {}, + filters: + timeline.filters != null + ? timeline.filters.map(filter => ({ + $state: { + store: 'appState', + }, + meta: { + ...filter.meta, + ...(filter.meta && filter.meta.field != null + ? { params: parseString(filter.meta.field) } + : {}), + ...(filter.meta && filter.meta.params != null + ? { params: parseString(filter.meta.params) } + : {}), + ...(filter.meta && filter.meta.value != null + ? { value: parseString(filter.meta.value) } + : {}), + }, + ...(filter.exists != null ? { exists: parseString(filter.exists) } : {}), + ...(filter.match_all != null ? { exists: parseString(filter.match_all) } : {}), + ...(filter.missing != null ? { exists: parseString(filter.missing) } : {}), + ...(filter.query != null ? { query: parseString(filter.query) } : {}), + ...(filter.range != null ? { range: parseString(filter.range) } : {}), + ...(filter.script != null ? { exists: parseString(filter.script) } : {}), + })) + : [], + isFavorite: duplicate + ? false + : timeline.favorite != null + ? timeline.favorite.length > 0 + : false, + noteIds: duplicate ? [] : timeline.noteIds != null ? timeline.noteIds : [], + pinnedEventIds: duplicate + ? {} + : timeline.pinnedEventIds != null + ? timeline.pinnedEventIds.reduce( + (acc, pinnedEventId) => ({ ...acc, [pinnedEventId]: true }), + {} + ) + : {}, + pinnedEventsSaveObject: duplicate + ? {} + : timeline.pinnedEventsSaveObject != null + ? timeline.pinnedEventsSaveObject.reduce( + (acc, pinnedEvent) => ({ + ...acc, + ...(pinnedEvent.eventId != null ? { [pinnedEvent.eventId]: pinnedEvent } : {}), + }), + {} + ) + : {}, + id: duplicate ? '' : timeline.savedObjectId, + savedObjectId: duplicate ? null : timeline.savedObjectId, + version: duplicate ? null : timeline.version, + title: duplicate ? '' : timeline.title || '', + templateTimelineId: duplicate ? null : timeline.templateTimelineId, + templateTimelineVersion: duplicate ? null : timeline.templateTimelineVersion, + }).reduce((acc: TimelineModel, [key, value]) => (value != null ? set(key, value, acc) : acc), { + ...timelineDefaults, + id: '', + }); +}; + +export const formatTimelineResultToModel = ( + timelineToOpen: TimelineResult, + duplicate: boolean = false +): { notes: NoteResult[] | null | undefined; timeline: TimelineModel } => { + const { notes, ...timelineModel } = timelineToOpen; + return { + notes, + timeline: defaultTimelineToTimelineModel(timelineModel, duplicate), + }; +}; + +export interface QueryTimelineById { + apolloClient: ApolloClient | ApolloClient<{}> | undefined; + duplicate: boolean; + timelineId: string; + onOpenTimeline?: (timeline: TimelineModel) => void; + openTimeline?: boolean; + updateIsLoading: ({ + id, + isLoading, + }: { + id: string; + isLoading: boolean; + }) => Action<{ id: string; isLoading: boolean }>; + updateTimeline: DispatchUpdateTimeline; +} + +export const queryTimelineById = ({ + apolloClient, + duplicate = false, + timelineId, + onOpenTimeline, + openTimeline = true, + updateIsLoading, + updateTimeline, +}: QueryTimelineById) => { + updateIsLoading({ id: 'timeline-1', isLoading: true }); + if (apolloClient) { + apolloClient + .query({ + query: oneTimelineQuery, + fetchPolicy: 'no-cache', + variables: { id: timelineId }, + }) + // eslint-disable-next-line + .then(result => { + const timelineToOpen: TimelineResult = omitTypenameInTimeline( + getOr({}, 'data.getOneTimeline', result) + ); + + const { timeline, notes } = formatTimelineResultToModel(timelineToOpen, duplicate); + if (onOpenTimeline != null) { + onOpenTimeline(timeline); + } else if (updateTimeline) { + const { from, to } = getTimeRangeSettings(); + updateTimeline({ + duplicate, + from: getOr(from, 'dateRange.start', timeline), + id: 'timeline-1', + notes, + timeline: { + ...timeline, + show: openTimeline, + }, + to: getOr(to, 'dateRange.end', timeline), + })(); + } + }) + .finally(() => { + updateIsLoading({ id: 'timeline-1', isLoading: false }); + }); + } +}; + +export const dispatchUpdateTimeline = (dispatch: Dispatch): DispatchUpdateTimeline => ({ + duplicate, + id, + from, + notes, + timeline, + to, + ruleNote, +}: UpdateTimeline): (() => void) => () => { + dispatch(dispatchSetTimelineRangeDatePicker({ from, to })); + dispatch(dispatchAddTimeline({ id, timeline })); + if ( + timeline.kqlQuery != null && + timeline.kqlQuery.filterQuery != null && + timeline.kqlQuery.filterQuery.kuery != null && + timeline.kqlQuery.filterQuery.kuery.expression !== '' + ) { + dispatch( + dispatchSetKqlFilterQueryDraft({ + id, + filterQueryDraft: { + kind: 'kuery', + expression: timeline.kqlQuery.filterQuery.kuery.expression || '', + }, + }) + ); + dispatch( + dispatchApplyKqlFilterQuery({ + id, + filterQuery: { + kuery: { + kind: 'kuery', + expression: timeline.kqlQuery.filterQuery.kuery.expression || '', + }, + serializedQuery: timeline.kqlQuery.filterQuery.serializedQuery || '', + }, + }) + ); + } + + if (duplicate && ruleNote != null && !isEmpty(ruleNote)) { + const getNewNoteId = (): string => uuid.v4(); + const newNote = createNote({ newNote: ruleNote, getNewNoteId }); + dispatch(dispatchUpdateNote({ note: newNote })); + dispatch(dispatchAddGlobalTimelineNote({ noteId: newNote.id, id })); + } + + if (!duplicate) { + dispatch( + dispatchAddNotes({ + notes: + notes != null + ? notes.map((note: NoteResult) => ({ + created: note.created != null ? new Date(note.created) : new Date(), + id: note.noteId, + lastEdit: note.updated != null ? new Date(note.updated) : new Date(), + note: note.note || '', + user: note.updatedBy || 'unknown', + saveObjectId: note.noteId, + version: note.version, + })) + : [], + }) + ); + } +}; diff --git a/x-pack/plugins/siem/public/timelines/components/open_timeline/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/index.test.tsx new file mode 100644 index 00000000000000..52197b92bdfb12 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/open_timeline/index.test.tsx @@ -0,0 +1,658 @@ +/* + * 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 euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { mount } from 'enzyme'; +import { MockedProvider } from 'react-apollo/test-utils'; +import React from 'react'; +import { ThemeProvider } from 'styled-components'; + +import { wait } from '../../../common/lib/helpers'; +import { TestProviderWithoutDragAndDrop, apolloClient } from '../../../common/mock/test_providers'; +import { mockOpenTimelineQueryResults } from '../../../common/mock/timeline_results'; +import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../pages/timelines_page'; + +import { NotePreviews } from './note_previews'; +import { OPEN_TIMELINE_CLASS_NAME } from './helpers'; +import { TimelineTabsStyle } from './types'; + +import { StatefulOpenTimeline } from '.'; +import { useGetAllTimeline, getAllTimeline } from '../../containers/all'; +jest.mock('../../../common/lib/kibana'); +jest.mock('../../containers/all', () => { + const originalModule = jest.requireActual('../../containers/all'); + return { + ...originalModule, + useGetAllTimeline: jest.fn(), + getAllTimeline: originalModule.getAllTimeline, + }; +}); +jest.mock('./use_timeline_types', () => { + return { + useTimelineTypes: jest.fn().mockReturnValue({ + timelineType: 'default', + timelineTabs:
, + timelineFilters:
, + }), + }; +}); + +describe('StatefulOpenTimeline', () => { + const theme = () => ({ eui: euiDarkVars, darkMode: true }); + const title = 'All Timelines / Open Timelines'; + beforeEach(() => { + ((useGetAllTimeline as unknown) as jest.Mock).mockReturnValue({ + fetchAllTimeline: jest.fn(), + timelines: getAllTimeline( + '', + mockOpenTimelineQueryResults[0].result.data?.getAllTimeline?.timeline ?? [] + ), + loading: false, + totalCount: mockOpenTimelineQueryResults[0].result.data.getAllTimeline.totalCount, + refetch: jest.fn(), + }); + }); + + test('it has the expected initial state', () => { + const wrapper = mount( + + + + + + + + ); + + const componentProps = wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .props(); + + expect(componentProps).toEqual({ + ...componentProps, + itemIdToExpandedNotesRowMap: {}, + onlyFavorites: false, + pageIndex: 0, + pageSize: 10, + query: '', + selectedItems: [], + sortDirection: 'desc', + sortField: 'updated', + }); + }); + + describe('#onQueryChange', () => { + test('it updates the query state with the expected trimmed value when the user enters a query', () => { + const wrapper = mount( + + + + + + + + ); + wrapper + .find('[data-test-subj="search-bar"] input') + .simulate('keyup', { keyCode: 13, target: { value: ' abcd ' } }); + expect( + wrapper + .find('[data-test-subj="search-row"]') + .first() + .prop('query') + ).toEqual('abcd'); + }); + + test('it appends the word "with" to the Showing in Timelines message when the user enters a query', async () => { + const wrapper = mount( + + + + + + + + ); + + await wait(); + + wrapper + .find('[data-test-subj="search-bar"] input') + .simulate('keyup', { keyCode: 13, target: { value: ' abcd ' } }); + + expect( + wrapper + .find('[data-test-subj="query-message"]') + .first() + .text() + ).toContain('Showing: 11 timelines with'); + }); + + test('echos (renders) the query when the user enters a query', async () => { + const wrapper = mount( + + + + + + + + ); + + await wait(); + + wrapper + .find('[data-test-subj="search-bar"] input') + .simulate('keyup', { keyCode: 13, target: { value: ' abcd ' } }); + + expect( + wrapper + .find('[data-test-subj="selectable-query-text"]') + .first() + .text() + ).toEqual('with "abcd"'); + }); + }); + + describe('#focusInput', () => { + test('focuses the input when the component mounts', async () => { + const wrapper = mount( + + + + + + + + ); + + await wait(); + + expect( + wrapper + .find(`.${OPEN_TIMELINE_CLASS_NAME} input`) + .first() + .getDOMNode().id === document.activeElement!.id + ).toBe(true); + }); + }); + + describe('#onAddTimelinesToFavorites', () => { + // This functionality is hiding for now and waiting to see the light in the near future + test.skip('it invokes addTimelinesToFavorites with the selected timelines when the button is clicked', async () => { + const addTimelinesToFavorites = jest.fn(); + + const wrapper = mount( + + + + + + + + ); + + await wait(); + + wrapper + .find('.euiCheckbox__input') + .first() + .simulate('change', { target: { checked: true } }); + + wrapper + .find('[data-test-subj="favorite-selected"]') + .first() + .simulate('click'); + + expect(addTimelinesToFavorites).toHaveBeenCalledWith([ + 'saved-timeline-11', + 'saved-timeline-10', + 'saved-timeline-9', + 'saved-timeline-8', + 'saved-timeline-6', + 'saved-timeline-5', + 'saved-timeline-4', + 'saved-timeline-3', + 'saved-timeline-2', + ]); + }); + }); + + describe('#onDeleteSelected', () => { + // TODO - Have been skip because we need to re-implement the test as the component changed + test.skip('it invokes deleteTimelines with the selected timelines when the button is clicked', async () => { + const deleteTimelines = jest.fn(); + + const wrapper = mount( + + + + + + + + ); + + await wait(); + + wrapper + .find('.euiCheckbox__input') + .first() + .simulate('change', { target: { checked: true } }); + + wrapper + .find('[data-test-subj="delete-selected"]') + .first() + .simulate('click'); + + expect(deleteTimelines).toHaveBeenCalledWith([ + 'saved-timeline-11', + 'saved-timeline-10', + 'saved-timeline-9', + 'saved-timeline-8', + 'saved-timeline-6', + 'saved-timeline-5', + 'saved-timeline-4', + 'saved-timeline-3', + 'saved-timeline-2', + ]); + }); + }); + + describe('#onSelectionChange', () => { + test('it updates the selection state when timelines are selected', async () => { + const wrapper = mount( + + + + + + + + ); + + await wait(); + + wrapper + .find('.euiCheckbox__input') + .first() + .simulate('change', { target: { checked: true } }); + + const selectedItems: [] = wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .prop('selectedItems'); + + expect(selectedItems.length).toEqual(13); // 13 because we did mock 13 timelines in the query + }); + }); + + describe('#onTableChange', () => { + test('it updates the sort state when the user clicks on a column to sort it', () => { + const wrapper = mount( + + + + + + + + ); + + expect( + wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .prop('sortDirection') + ).toEqual('desc'); + + wrapper + .find('thead tr th button') + .at(0) + .simulate('click'); + + expect( + wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .prop('sortDirection') + ).toEqual('asc'); + }); + }); + + describe('#onToggleOnlyFavorites', () => { + test('it updates the onlyFavorites state when the user clicks the Only Favorites button', () => { + const wrapper = mount( + + + + + + + + ); + + expect( + wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .prop('onlyFavorites') + ).toEqual(false); + + wrapper + .find('[data-test-subj="only-favorites-toggle"]') + .first() + .simulate('click'); + + expect( + wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .prop('onlyFavorites') + ).toEqual(true); + }); + }); + + describe('#onToggleShowNotes', () => { + test('it updates the itemIdToExpandedNotesRowMap state when the user clicks the expand notes button', async () => { + const wrapper = mount( + + + + + + + + ); + + await wait(); + wrapper.update(); + + expect( + wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .prop('itemIdToExpandedNotesRowMap') + ).toEqual({}); + + wrapper + .find('[data-test-subj="expand-notes"]') + .first() + .simulate('click'); + + expect( + wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .prop('itemIdToExpandedNotesRowMap') + ).toEqual({ + '10849df0-7b44-11e9-a608-ab3d811609': ( + ({ ...note, savedObjectId: note.noteId }) + ) + : [] + } + /> + ), + }); + }); + + test('it renders the expanded notes when the expand button is clicked', async () => { + const wrapper = mount( + + + + + + + + ); + + await wait(); + + wrapper.update(); + + wrapper + .find('[data-test-subj="expand-notes"]') + .first() + .simulate('click'); + expect(wrapper.find('[data-test-subj="note-previews-container"]').exists()).toEqual(true); + expect(wrapper.find('[data-test-subj="updated-by"]').exists()).toEqual(true); + + expect( + wrapper + .find('[data-test-subj="note-previews-container"]') + .find('[data-test-subj="updated-by"]') + .first() + .text() + ).toEqual('elastic'); + }); + + test('it renders the title', async () => { + const wrapper = mount( + + + + + + + + ); + + await wait(); + + expect(wrapper.find(`[data-test-subj="timeline-${TimelineTabsStyle.tab}"]`).exists()).toEqual( + true + ); + }); + }); + + describe('#resetSelectionState', () => { + test('when the user deletes selected timelines, resetSelectionState is invoked to clear the selection state', async () => { + const wrapper = mount( + + + + + + + + ); + const getSelectedItem = (): [] => + wrapper + .find('[data-test-subj="open-timeline"]') + .last() + .prop('selectedItems'); + await wait(); + expect(getSelectedItem().length).toEqual(0); + wrapper + .find('.euiCheckbox__input') + .first() + .simulate('change', { target: { checked: true } }); + expect(getSelectedItem().length).toEqual(13); + }); + }); + + test('it renders the expected count of matching timelines when no query has been entered', async () => { + const wrapper = mount( + + + + + + + + ); + + await wait(); + + wrapper.update(); + + expect( + wrapper + .find('[data-test-subj="query-message"]') + .first() + .text() + ).toContain('Showing: 11 timelines '); + }); + + // TODO - Have been skip because we need to re-implement the test as the component changed + test.skip('it invokes onOpenTimeline with the expected parameters when the hyperlink is clicked', async () => { + const onOpenTimeline = jest.fn(); + + const wrapper = mount( + + + + + + + + ); + + await wait(); + + wrapper + .find( + `[data-test-subj="title-${ + mockOpenTimelineQueryResults[0].result.data!.getAllTimeline.timeline[0].savedObjectId + }"]` + ) + .first() + .simulate('click'); + + expect(onOpenTimeline).toHaveBeenCalledWith({ + duplicate: false, + timelineId: mockOpenTimelineQueryResults[0].result.data!.getAllTimeline.timeline[0] + .savedObjectId, + }); + }); + + // TODO - Have been skip because we need to re-implement the test as the component changed + test.skip('it invokes onOpenTimeline with the expected params when the button is clicked', async () => { + const onOpenTimeline = jest.fn(); + + const wrapper = mount( + + + + + + + + ); + + await wait(); + + wrapper + .find('[data-test-subj="open-duplicate"]') + .first() + .simulate('click'); + + expect(onOpenTimeline).toBeCalledWith({ duplicate: true, timelineId: 'saved-timeline-11' }); + }); +}); diff --git a/x-pack/plugins/siem/public/timelines/components/open_timeline/index.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/index.tsx new file mode 100644 index 00000000000000..735ccdd19a5614 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/open_timeline/index.tsx @@ -0,0 +1,343 @@ +/* + * 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 ApolloClient from 'apollo-client'; +import React, { useEffect, useState, useCallback } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; + +import { Dispatch } from 'redux'; +import { defaultHeaders } from '../timeline/body/column_headers/default_headers'; +import { deleteTimelineMutation } from '../../containers/delete/persist.gql_query'; +import { useGetAllTimeline } from '../../containers/all'; +import { DeleteTimelineMutation, SortFieldTimeline, Direction } from '../../../graphql/types'; +import { State } from '../../../common/store'; +import { ColumnHeaderOptions, TimelineModel } from '../../../timelines/store/timeline/model'; +import { timelineSelectors } from '../../../timelines/store/timeline'; +import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; +import { + createTimeline as dispatchCreateNewTimeline, + updateIsLoading as dispatchUpdateIsLoading, +} from '../../../timelines/store/timeline/actions'; +import { OpenTimeline } from './open_timeline'; +import { OPEN_TIMELINE_CLASS_NAME, queryTimelineById, dispatchUpdateTimeline } from './helpers'; +import { OpenTimelineModalBody } from './open_timeline_modal/open_timeline_modal_body'; +import { + ActionTimelineToShow, + DeleteTimelines, + EuiSearchBarQuery, + OnDeleteSelected, + OnOpenTimeline, + OnQueryChange, + OnSelectionChange, + OnTableChange, + OnTableChangeParams, + OpenTimelineProps, + OnToggleOnlyFavorites, + OpenTimelineResult, + OnToggleShowNotes, + OnDeleteOneTimeline, +} from './types'; +import { DEFAULT_SORT_FIELD, DEFAULT_SORT_DIRECTION } from './constants'; +import { useTimelineTypes } from './use_timeline_types'; + +interface OwnProps { + apolloClient: ApolloClient; + /** Displays open timeline in modal */ + isModal: boolean; + closeModalTimeline?: () => void; + hideActions?: ActionTimelineToShow[]; + onOpenTimeline?: (timeline: TimelineModel) => void; +} + +export type OpenTimelineOwnProps = OwnProps & + Pick< + OpenTimelineProps, + 'defaultPageSize' | 'title' | 'importDataModalToggle' | 'setImportDataModalToggle' + > & + PropsFromRedux; + +/** Returns a collection of selected timeline ids */ +export const getSelectedTimelineIds = (selectedItems: OpenTimelineResult[]): string[] => + selectedItems.reduce( + (validSelections, timelineResult) => + timelineResult.savedObjectId != null + ? [...validSelections, timelineResult.savedObjectId] + : validSelections, + [] + ); + +/** Manages the state (e.g table selection) of the (pure) `OpenTimeline` component */ +export const StatefulOpenTimelineComponent = React.memo( + ({ + apolloClient, + closeModalTimeline, + createNewTimeline, + defaultPageSize, + hideActions = [], + isModal = false, + importDataModalToggle, + onOpenTimeline, + setImportDataModalToggle, + timeline, + title, + updateTimeline, + updateIsLoading, + }) => { + /** Required by EuiTable for expandable rows: a map of `TimelineResult.savedObjectId` to rendered notes */ + const [itemIdToExpandedNotesRowMap, setItemIdToExpandedNotesRowMap] = useState< + Record + >({}); + /** Only query for favorite timelines when true */ + const [onlyFavorites, setOnlyFavorites] = useState(false); + /** The requested page of results */ + const [pageIndex, setPageIndex] = useState(0); + /** The requested size of each page of search results */ + const [pageSize, setPageSize] = useState(defaultPageSize); + /** The current search criteria */ + const [search, setSearch] = useState(''); + /** The currently-selected timelines in the table */ + const [selectedItems, setSelectedItems] = useState([]); + /** The requested sort direction of the query results */ + const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>(DEFAULT_SORT_DIRECTION); + /** The requested field to sort on */ + const [sortField, setSortField] = useState(DEFAULT_SORT_FIELD); + + const { timelineType, timelineTabs, timelineFilters } = useTimelineTypes(); + const { fetchAllTimeline, timelines, loading, totalCount } = useGetAllTimeline(); + + const refetch = useCallback(() => { + fetchAllTimeline({ + pageInfo: { + pageIndex: pageIndex + 1, + pageSize, + }, + search, + sort: { sortField: sortField as SortFieldTimeline, sortOrder: sortDirection as Direction }, + onlyUserFavorite: onlyFavorites, + timelineType, + }); + }, [pageIndex, pageSize, search, sortField, sortDirection, timelineType, onlyFavorites]); + + /** Invoked when the user presses enters to submit the text in the search input */ + const onQueryChange: OnQueryChange = useCallback((query: EuiSearchBarQuery) => { + setSearch(query.queryText.trim()); + }, []); + + /** Focuses the input that filters the field browser */ + const focusInput = () => { + const elements = document.querySelector(`.${OPEN_TIMELINE_CLASS_NAME} input`); + + if (elements != null) { + elements.focus(); + } + }; + + /* This feature will be implemented in the near future, so we are keeping it to know what to do */ + + /** Invoked when the user clicks the action to add the selected timelines to favorites */ + // const onAddTimelinesToFavorites: OnAddTimelinesToFavorites = () => { + // const { addTimelinesToFavorites } = this.props; + // const { selectedItems } = this.state; + // if (addTimelinesToFavorites != null) { + // addTimelinesToFavorites(getSelectedTimelineIds(selectedItems)); + // TODO: it's not possible to clear the selection state of the newly-favorited + // items, because we can't pass the selection state as props to the table. + // See: https://github.com/elastic/eui/issues/1077 + // TODO: the query must re-execute to show the results of the mutation + // } + // }; + + const deleteTimelines: DeleteTimelines = useCallback( + async (timelineIds: string[]) => { + if (timelineIds.includes(timeline.savedObjectId || '')) { + createNewTimeline({ id: 'timeline-1', columns: defaultHeaders, show: false }); + } + + await apolloClient.mutate< + DeleteTimelineMutation.Mutation, + DeleteTimelineMutation.Variables + >({ + mutation: deleteTimelineMutation, + fetchPolicy: 'no-cache', + variables: { id: timelineIds }, + }); + refetch(); + }, + [apolloClient, createNewTimeline, refetch, timeline] + ); + + const onDeleteOneTimeline: OnDeleteOneTimeline = useCallback( + async (timelineIds: string[]) => { + await deleteTimelines(timelineIds); + }, + [deleteTimelines] + ); + + /** Invoked when the user clicks the action to delete the selected timelines */ + const onDeleteSelected: OnDeleteSelected = useCallback(async () => { + await deleteTimelines(getSelectedTimelineIds(selectedItems)); + + // NOTE: we clear the selection state below, but if the server fails to + // delete a timeline, it will remain selected in the table: + resetSelectionState(); + + // TODO: the query must re-execute to show the results of the deletion + }, [selectedItems, deleteTimelines]); + + /** Invoked when the user selects (or de-selects) timelines */ + const onSelectionChange: OnSelectionChange = useCallback( + (newSelectedItems: OpenTimelineResult[]) => { + setSelectedItems(newSelectedItems); // <-- this is NOT passed down as props to the table: https://github.com/elastic/eui/issues/1077 + }, + [] + ); + + /** Invoked by the EUI table implementation when the user interacts with the table (i.e. to update sorting) */ + const onTableChange: OnTableChange = useCallback(({ page, sort }: OnTableChangeParams) => { + const { index, size } = page; + const { field, direction } = sort; + setPageIndex(index); + setPageSize(size); + setSortDirection(direction); + setSortField(field); + }, []); + + /** Invoked when the user toggles the option to only view favorite timelines */ + const onToggleOnlyFavorites: OnToggleOnlyFavorites = useCallback(() => { + setOnlyFavorites(!onlyFavorites); + }, [onlyFavorites]); + + /** Invoked when the user toggles the expansion or collapse of inline notes in a table row */ + const onToggleShowNotes: OnToggleShowNotes = useCallback( + (newItemIdToExpandedNotesRowMap: Record) => { + setItemIdToExpandedNotesRowMap(newItemIdToExpandedNotesRowMap); + }, + [] + ); + + /** Resets the selection state such that all timelines are unselected */ + const resetSelectionState = useCallback(() => { + setSelectedItems([]); + }, []); + + const openTimeline: OnOpenTimeline = useCallback( + ({ duplicate, timelineId }: { duplicate: boolean; timelineId: string }) => { + if (isModal && closeModalTimeline != null) { + closeModalTimeline(); + } + + queryTimelineById({ + apolloClient, + duplicate, + onOpenTimeline, + timelineId, + updateIsLoading, + updateTimeline, + }); + }, + [apolloClient, updateIsLoading, updateTimeline] + ); + + useEffect(() => { + focusInput(); + }, []); + + useEffect(() => { + refetch(); + }, [refetch]); + + return !isModal ? ( + + ) : ( + + ); + } +); + +const makeMapStateToProps = () => { + const getTimeline = timelineSelectors.getTimelineByIdSelector(); + const mapStateToProps = (state: State) => { + const timeline = getTimeline(state, 'timeline-1') ?? timelineDefaults; + return { + timeline, + }; + }; + return mapStateToProps; +}; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + createNewTimeline: ({ + id, + columns, + show, + }: { + id: string; + columns: ColumnHeaderOptions[]; + show?: boolean; + }) => dispatch(dispatchCreateNewTimeline({ id, columns, show })), + updateIsLoading: ({ id, isLoading }: { id: string; isLoading: boolean }) => + dispatch(dispatchUpdateIsLoading({ id, isLoading })), + updateTimeline: dispatchUpdateTimeline(dispatch), +}); + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const StatefulOpenTimeline = connector(StatefulOpenTimelineComponent); diff --git a/x-pack/plugins/siem/public/timelines/components/open_timeline/note_previews/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/note_previews/index.test.tsx new file mode 100644 index 00000000000000..318e50bb67d2d2 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/open_timeline/note_previews/index.test.tsx @@ -0,0 +1,188 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { cloneDeep } from 'lodash/fp'; +import moment from 'moment'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import React from 'react'; +import { ThemeProvider } from 'styled-components'; + +import { mockTimelineResults } from '../../../../common/mock/timeline_results'; +import { OpenTimelineResult, TimelineResultNote } from '../types'; +import { NotePreviews } from '.'; + +describe('NotePreviews', () => { + const theme = () => ({ eui: euiDarkVars, darkMode: true }); + let mockResults: OpenTimelineResult[]; + let note1updated: number; + let note2updated: number; + let note3updated: number; + + beforeEach(() => { + mockResults = cloneDeep(mockTimelineResults); + note1updated = moment('2019-03-24T04:12:33.000Z').valueOf(); + note2updated = moment(note1updated) + .add(1, 'minute') + .valueOf(); + note3updated = moment(note2updated) + .add(1, 'minute') + .valueOf(); + }); + + test('it renders a note preview for each note when isModal is false', () => { + const hasNotes: OpenTimelineResult[] = [{ ...mockResults[0] }]; + + const wrapper = mountWithIntl( + + + + ); + + hasNotes[0].notes!.forEach(({ savedObjectId }) => { + expect(wrapper.find(`[data-test-subj="note-preview-${savedObjectId}"]`).exists()).toBe(true); + }); + }); + + test('it renders a note preview for each note when isModal is true', () => { + const hasNotes: OpenTimelineResult[] = [{ ...mockResults[0] }]; + + const wrapper = mountWithIntl( + + + + ); + + hasNotes[0].notes!.forEach(({ savedObjectId }) => { + expect(wrapper.find(`[data-test-subj="note-preview-${savedObjectId}"]`).exists()).toBe(true); + }); + }); + + test('it does NOT render the preview container if notes is undefined', () => { + const wrapper = mountWithIntl(); + + expect(wrapper.find('[data-test-subj="note-previews-container"]').exists()).toBe(false); + }); + + test('it does NOT render the preview container if notes is null', () => { + const wrapper = mountWithIntl(); + + expect(wrapper.find('[data-test-subj="note-previews-container"]').exists()).toBe(false); + }); + + test('it does NOT render the preview container if notes is empty', () => { + const wrapper = mountWithIntl(); + + expect(wrapper.find('[data-test-subj="note-previews-container"]').exists()).toBe(false); + }); + + test('it filters-out non-unique savedObjectIds', () => { + const nonUniqueNotes: TimelineResultNote[] = [ + { + note: '1', + savedObjectId: 'noteId1', + updated: note1updated, + updatedBy: 'alice', + }, + { + note: '2 (savedObjectId is the same as the previous entry)', + savedObjectId: 'noteId1', + updated: note2updated, + updatedBy: 'alice', + }, + { + note: '3', + savedObjectId: 'noteId2', + updated: note3updated, + updatedBy: 'bob', + }, + ]; + + const wrapper = mountWithIntl( + + + + ); + + expect( + wrapper + .find(`[data-test-subj="updated-by"]`) + .at(2) + .text() + ).toEqual('bob'); + }); + + test('it filters-out null savedObjectIds', () => { + const nonUniqueNotes: TimelineResultNote[] = [ + { + note: '1', + savedObjectId: 'noteId1', + updated: note1updated, + updatedBy: 'alice', + }, + { + note: '2 (savedObjectId is null)', + savedObjectId: null, + updated: note2updated, + updatedBy: 'alice', + }, + { + note: '3', + savedObjectId: 'noteId2', + updated: note3updated, + updatedBy: 'bob', + }, + ]; + + const wrapper = mountWithIntl( + + + + ); + + expect( + wrapper + .find(`[data-test-subj="updated-by"]`) + .at(2) + .text() + ).toEqual('bob'); + }); + + test('it filters-out undefined savedObjectIds', () => { + const nonUniqueNotes: TimelineResultNote[] = [ + { + note: '1', + savedObjectId: 'noteId1', + updated: note1updated, + updatedBy: 'alice', + }, + { + note: 'b (savedObjectId is undefined)', + updated: note2updated, + updatedBy: 'alice', + }, + { + note: 'c', + savedObjectId: 'noteId2', + updated: note3updated, + updatedBy: 'bob', + }, + ]; + + const wrapper = mountWithIntl( + + + + ); + + expect( + wrapper + .find(`[data-test-subj="updated-by"]`) + .at(2) + .text() + ).toEqual('bob'); + }); +}); diff --git a/x-pack/plugins/siem/public/components/open_timeline/note_previews/index.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/note_previews/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/open_timeline/note_previews/index.tsx rename to x-pack/plugins/siem/public/timelines/components/open_timeline/note_previews/index.tsx diff --git a/x-pack/plugins/siem/public/components/open_timeline/note_previews/note_preview.test.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/note_previews/note_preview.test.tsx similarity index 98% rename from x-pack/plugins/siem/public/components/open_timeline/note_previews/note_preview.test.tsx rename to x-pack/plugins/siem/public/timelines/components/open_timeline/note_previews/note_preview.test.tsx index 7cefaf08d76cb2..c0046e43eef306 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/note_previews/note_preview.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/open_timeline/note_previews/note_preview.test.tsx @@ -9,7 +9,7 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { ThemeProvider } from 'styled-components'; -import { getEmptyValue } from '../../empty_value'; +import { getEmptyValue } from '../../../../common/components/empty_value'; import { NotePreview } from './note_preview'; import * as i18n from '../translations'; diff --git a/x-pack/plugins/siem/public/components/open_timeline/note_previews/note_preview.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/note_previews/note_preview.tsx similarity index 90% rename from x-pack/plugins/siem/public/components/open_timeline/note_previews/note_preview.tsx rename to x-pack/plugins/siem/public/timelines/components/open_timeline/note_previews/note_preview.tsx index bb4a032734b5b9..d079a4bedcbaf3 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/note_previews/note_preview.tsx +++ b/x-pack/plugins/siem/public/timelines/components/open_timeline/note_previews/note_preview.tsx @@ -9,9 +9,9 @@ import { FormattedRelative } from '@kbn/i18n/react'; import React from 'react'; import styled from 'styled-components'; -import { getEmptyValue, defaultToEmptyTag } from '../../empty_value'; -import { FormattedDate } from '../../formatted_date'; -import { Markdown } from '../../markdown'; +import { getEmptyValue, defaultToEmptyTag } from '../../../../common/components/empty_value'; +import { FormattedDate } from '../../../../common/components/formatted_date'; +import { Markdown } from '../../../../common/components/markdown'; import * as i18n from '../translations'; import { TimelineResultNote } from '../types'; diff --git a/x-pack/plugins/siem/public/components/open_timeline/open_timeline.test.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/open_timeline.test.tsx similarity index 98% rename from x-pack/plugins/siem/public/components/open_timeline/open_timeline.test.tsx rename to x-pack/plugins/siem/public/timelines/components/open_timeline/open_timeline.test.tsx index 449e1b169cea64..787da4ed6cf416 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/open_timeline.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/open_timeline/open_timeline.test.tsx @@ -10,14 +10,14 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { ThemeProvider } from 'styled-components'; -import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../pages/timelines/timelines_page'; +import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../pages/timelines_page'; import { OpenTimelineResult, OpenTimelineProps } from './types'; import { TimelinesTableProps } from './timelines_table'; -import { mockTimelineResults } from '../../mock/timeline_results'; +import { mockTimelineResults } from '../../../common/mock/timeline_results'; import { OpenTimeline } from './open_timeline'; import { DEFAULT_SORT_DIRECTION, DEFAULT_SORT_FIELD } from './constants'; -jest.mock('../../lib/kibana'); +jest.mock('../../../common/lib/kibana'); describe('OpenTimeline', () => { const theme = () => ({ eui: euiDarkVars, darkMode: true }); diff --git a/x-pack/plugins/siem/public/components/open_timeline/open_timeline.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/open_timeline.tsx similarity index 97% rename from x-pack/plugins/siem/public/components/open_timeline/open_timeline.tsx rename to x-pack/plugins/siem/public/timelines/components/open_timeline/open_timeline.tsx index e172a006abe4b0..cdbba307a11544 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/open_timeline.tsx +++ b/x-pack/plugins/siem/public/timelines/components/open_timeline/open_timeline.tsx @@ -11,9 +11,9 @@ import { OPEN_TIMELINE_CLASS_NAME } from './helpers'; import { OpenTimelineProps, OpenTimelineResult } from './types'; import { SearchRow } from './search_row'; import { TimelinesTable } from './timelines_table'; -import { ImportDataModal } from '../import_data_modal'; +import { ImportDataModal } from '../../../common/components/import_data_modal'; import * as i18n from './translations'; -import { importTimelines } from '../../containers/timeline/api'; +import { importTimelines } from '../../containers/api'; import { UtilityBarGroup, @@ -21,7 +21,7 @@ import { UtilityBar, UtilityBarSection, UtilityBarAction, -} from '../utility_bar'; +} from '../../../common/components/utility_bar'; import { useEditTimelinBatchActions } from './edit_timeline_batch_actions'; import { useEditTimelineActions } from './edit_timeline_actions'; import { EditOneTimelineAction } from './export_timeline'; diff --git a/x-pack/plugins/siem/public/timelines/components/open_timeline/open_timeline_modal/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/open_timeline_modal/index.test.tsx new file mode 100644 index 00000000000000..8382af6056ca78 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/open_timeline/open_timeline_modal/index.test.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { mount } from 'enzyme'; +import React from 'react'; +import { MockedProvider } from 'react-apollo/test-utils'; +import { ThemeProvider } from 'styled-components'; + +import { wait } from '../../../../common/lib/helpers'; +import { TestProviderWithoutDragAndDrop } from '../../../../common/mock/test_providers'; +import { mockOpenTimelineQueryResults } from '../../../../common/mock/timeline_results'; +import { useGetAllTimeline, getAllTimeline } from '../../../containers/all'; + +import { OpenTimelineModal } from '.'; + +jest.mock('../../../../common/lib/kibana'); +jest.mock('../../../../common/utils/apollo_context', () => ({ + useApolloClient: () => ({}), +})); +jest.mock('../../../containers/all', () => { + const originalModule = jest.requireActual('../../../containers/all'); + return { + useGetAllTimeline: jest.fn(), + getAllTimeline: originalModule.getAllTimeline, + }; +}); +jest.mock('../use_timeline_types', () => { + return { + useTimelineTypes: jest.fn().mockReturnValue({ + timelineType: 'default', + timelineTabs:
, + timelineFilters:
, + }), + }; +}); + +describe('OpenTimelineModal', () => { + const theme = () => ({ eui: euiDarkVars, darkMode: true }); + beforeEach(() => { + ((useGetAllTimeline as unknown) as jest.Mock).mockReturnValue({ + fetchAllTimeline: jest.fn(), + timelines: getAllTimeline( + '', + mockOpenTimelineQueryResults[0].result.data?.getAllTimeline?.timeline ?? [] + ), + loading: false, + totalCount: mockOpenTimelineQueryResults[0].result.data.getAllTimeline.totalCount, + refetch: jest.fn(), + }); + }); + + test('it renders the expected modal', async () => { + const wrapper = mount( + + + + + + + + ); + + await wait(); + + wrapper.update(); + + expect(wrapper.find('div[data-test-subj="open-timeline-modal"].euiModal').length).toEqual(1); + }); +}); diff --git a/x-pack/plugins/siem/public/timelines/components/open_timeline/open_timeline_modal/index.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/open_timeline_modal/index.tsx new file mode 100644 index 00000000000000..901ae955cbfe9b --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/open_timeline/open_timeline_modal/index.tsx @@ -0,0 +1,55 @@ +/* + * 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 { EuiModal, EuiOverlayMask } from '@elastic/eui'; +import React from 'react'; + +import { TimelineModel } from '../../../../timelines/store/timeline/model'; +import { useApolloClient } from '../../../../common/utils/apollo_context'; + +import * as i18n from '../translations'; +import { ActionTimelineToShow } from '../types'; +import { StatefulOpenTimeline } from '..'; + +export interface OpenTimelineModalProps { + onClose: () => void; + hideActions?: ActionTimelineToShow[]; + modalTitle?: string; + onOpen?: (timeline: TimelineModel) => void; +} + +const DEFAULT_SEARCH_RESULTS_PER_PAGE = 10; +const OPEN_TIMELINE_MODAL_WIDTH = 1000; // px + +export const OpenTimelineModal = React.memo( + ({ hideActions = [], modalTitle, onClose, onOpen }) => { + const apolloClient = useApolloClient(); + + if (!apolloClient) return null; + + return ( + + + + + + ); + } +); + +OpenTimelineModal.displayName = 'OpenTimelineModal'; diff --git a/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx similarity index 97% rename from x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx rename to x-pack/plugins/siem/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx index a610884d287a62..1b320c9ebd7551 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.test.tsx @@ -10,14 +10,14 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { ThemeProvider } from 'styled-components'; -import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../../pages/timelines/timelines_page'; +import { DEFAULT_SEARCH_RESULTS_PER_PAGE } from '../../../pages/timelines_page'; import { OpenTimelineResult, OpenTimelineProps } from '../types'; import { TimelinesTableProps } from '../timelines_table'; -import { mockTimelineResults } from '../../../mock/timeline_results'; +import { mockTimelineResults } from '../../../../common/mock/timeline_results'; import { OpenTimelineModalBody } from './open_timeline_modal_body'; import { DEFAULT_SORT_DIRECTION, DEFAULT_SORT_FIELD } from '../constants'; -jest.mock('../../../lib/kibana'); +jest.mock('../../../../common/lib/kibana'); describe('OpenTimelineModal', () => { const theme = () => ({ eui: euiDarkVars, darkMode: true }); diff --git a/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx rename to x-pack/plugins/siem/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_body.tsx diff --git a/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_button.test.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_button.test.tsx similarity index 89% rename from x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_button.test.tsx rename to x-pack/plugins/siem/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_button.test.tsx index 66947a313f5e55..0244bdda0d826e 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_button.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_button.test.tsx @@ -10,9 +10,9 @@ import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; import { ThemeProvider } from 'styled-components'; -import { wait } from '../../../lib/helpers'; -import { TestProviderWithoutDragAndDrop } from '../../../mock/test_providers'; -import { mockOpenTimelineQueryResults } from '../../../mock/timeline_results'; +import { wait } from '../../../../common/lib/helpers'; +import { TestProviderWithoutDragAndDrop } from '../../../../common/mock/test_providers'; +import { mockOpenTimelineQueryResults } from '../../../../common/mock/timeline_results'; import * as i18n from '../translations'; import { OpenTimelineModalButton } from './open_timeline_modal_button'; diff --git a/x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_button.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_button.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/open_timeline/open_timeline_modal/open_timeline_modal_button.tsx rename to x-pack/plugins/siem/public/timelines/components/open_timeline/open_timeline_modal/open_timeline_modal_button.tsx diff --git a/x-pack/plugins/siem/public/components/open_timeline/search_row/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/search_row/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/open_timeline/search_row/index.test.tsx rename to x-pack/plugins/siem/public/timelines/components/open_timeline/search_row/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/open_timeline/search_row/index.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/search_row/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/open_timeline/search_row/index.tsx rename to x-pack/plugins/siem/public/timelines/components/open_timeline/search_row/index.tsx diff --git a/x-pack/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.test.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/timelines_table/actions_columns.test.tsx similarity index 98% rename from x-pack/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.test.tsx rename to x-pack/plugins/siem/public/timelines/components/open_timeline/timelines_table/actions_columns.test.tsx index b0f8963dd501ea..0560bcf2b08ca3 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/open_timeline/timelines_table/actions_columns.test.tsx @@ -11,12 +11,12 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { ThemeProvider } from 'styled-components'; -import { mockTimelineResults } from '../../../mock/timeline_results'; +import { mockTimelineResults } from '../../../../common/mock/timeline_results'; import { OpenTimelineResult } from '../types'; import { TimelinesTableProps } from '.'; import { getMockTimelinesTableProps } from './mocks'; -jest.mock('../../../lib/kibana'); +jest.mock('../../../../common/lib/kibana'); const { TimelinesTable } = jest.requireActual('.'); diff --git a/x-pack/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/timelines_table/actions_columns.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/open_timeline/timelines_table/actions_columns.tsx rename to x-pack/plugins/siem/public/timelines/components/open_timeline/timelines_table/actions_columns.tsx diff --git a/x-pack/plugins/siem/public/components/open_timeline/timelines_table/common_columns.test.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/timelines_table/common_columns.test.tsx similarity index 98% rename from x-pack/plugins/siem/public/components/open_timeline/timelines_table/common_columns.test.tsx rename to x-pack/plugins/siem/public/timelines/components/open_timeline/timelines_table/common_columns.test.tsx index a312c72ecc25be..4fb6a4d84f7db1 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/timelines_table/common_columns.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/open_timeline/timelines_table/common_columns.test.tsx @@ -11,16 +11,16 @@ import React from 'react'; import { ThemeProvider } from 'styled-components'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { getEmptyValue } from '../../empty_value'; +import { getEmptyValue } from '../../../../common/components/empty_value'; import { OpenTimelineResult } from '../types'; -import { mockTimelineResults } from '../../../mock/timeline_results'; +import { mockTimelineResults } from '../../../../common/mock/timeline_results'; import { NotePreviews } from '../note_previews'; import { TimelinesTable, TimelinesTableProps } from '.'; import * as i18n from '../translations'; import { getMockTimelinesTableProps } from './mocks'; -jest.mock('../../../lib/kibana'); +jest.mock('../../../../common/lib/kibana'); describe('#getCommonColumns', () => { const theme = () => ({ eui: euiDarkVars, darkMode: true }); diff --git a/x-pack/plugins/siem/public/components/open_timeline/timelines_table/common_columns.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/timelines_table/common_columns.tsx similarity index 95% rename from x-pack/plugins/siem/public/components/open_timeline/timelines_table/common_columns.tsx rename to x-pack/plugins/siem/public/timelines/components/open_timeline/timelines_table/common_columns.tsx index 0d3a73a389050d..e0c7ab68f6bf51 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/timelines_table/common_columns.tsx +++ b/x-pack/plugins/siem/public/timelines/components/open_timeline/timelines_table/common_columns.tsx @@ -15,8 +15,8 @@ import { isUntitled } from '../helpers'; import { NotePreviews } from '../note_previews'; import * as i18n from '../translations'; import { OnOpenTimeline, OnToggleShowNotes, OpenTimelineResult } from '../types'; -import { getEmptyTagValue } from '../../empty_value'; -import { FormattedRelativePreferenceDate } from '../../formatted_date'; +import { getEmptyTagValue } from '../../../../common/components/empty_value'; +import { FormattedRelativePreferenceDate } from '../../../../common/components/formatted_date'; /** * Returns the column definitions (passed as the `columns` prop to diff --git a/x-pack/plugins/siem/public/components/open_timeline/timelines_table/common_styles.ts b/x-pack/plugins/siem/public/timelines/components/open_timeline/timelines_table/common_styles.ts similarity index 100% rename from x-pack/plugins/siem/public/components/open_timeline/timelines_table/common_styles.ts rename to x-pack/plugins/siem/public/timelines/components/open_timeline/timelines_table/common_styles.ts diff --git a/x-pack/plugins/siem/public/components/open_timeline/timelines_table/extended_columns.test.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/timelines_table/extended_columns.test.tsx similarity index 92% rename from x-pack/plugins/siem/public/components/open_timeline/timelines_table/extended_columns.test.tsx rename to x-pack/plugins/siem/public/timelines/components/open_timeline/timelines_table/extended_columns.test.tsx index 14409a6bbb5ae9..be7127668f7f1b 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/timelines_table/extended_columns.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/open_timeline/timelines_table/extended_columns.test.tsx @@ -10,8 +10,8 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { ThemeProvider } from 'styled-components'; -import { getEmptyValue } from '../../empty_value'; -import { mockTimelineResults } from '../../../mock/timeline_results'; +import { getEmptyValue } from '../../../../common/components/empty_value'; +import { mockTimelineResults } from '../../../../common/mock/timeline_results'; import { OpenTimelineResult } from '../types'; import { TimelinesTable, TimelinesTableProps } from '.'; @@ -19,7 +19,7 @@ import { TimelinesTable, TimelinesTableProps } from '.'; import * as i18n from '../translations'; import { getMockTimelinesTableProps } from './mocks'; -jest.mock('../../../lib/kibana'); +jest.mock('../../../../common/lib/kibana'); describe('#getExtendedColumns', () => { const theme = () => ({ eui: euiDarkVars, darkMode: true }); diff --git a/x-pack/plugins/siem/public/components/open_timeline/timelines_table/extended_columns.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/timelines_table/extended_columns.tsx similarity index 91% rename from x-pack/plugins/siem/public/components/open_timeline/timelines_table/extended_columns.tsx rename to x-pack/plugins/siem/public/timelines/components/open_timeline/timelines_table/extended_columns.tsx index b6d874fa0c4d1a..e50336f5169e88 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/timelines_table/extended_columns.tsx +++ b/x-pack/plugins/siem/public/timelines/components/open_timeline/timelines_table/extended_columns.tsx @@ -8,7 +8,7 @@ import React from 'react'; -import { defaultToEmptyTag } from '../../empty_value'; +import { defaultToEmptyTag } from '../../../../common/components/empty_value'; import * as i18n from '../translations'; import { OpenTimelineResult } from '../types'; diff --git a/x-pack/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.test.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/timelines_table/icon_header_columns.test.tsx similarity index 97% rename from x-pack/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.test.tsx rename to x-pack/plugins/siem/public/timelines/components/open_timeline/timelines_table/icon_header_columns.test.tsx index 658dd96faa9864..f1df605c072dde 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/open_timeline/timelines_table/icon_header_columns.test.tsx @@ -10,11 +10,11 @@ import { mountWithIntl } from 'test_utils/enzyme_helpers'; import React from 'react'; import { ThemeProvider } from 'styled-components'; -import { mockTimelineResults } from '../../../mock/timeline_results'; +import { mockTimelineResults } from '../../../../common/mock/timeline_results'; import { TimelinesTable, TimelinesTableProps } from '.'; import { OpenTimelineResult } from '../types'; import { getMockTimelinesTableProps } from './mocks'; -jest.mock('../../../lib/kibana'); +jest.mock('../../../../common/lib/kibana'); describe('#getActionsColumns', () => { const theme = () => ({ eui: euiDarkVars, darkMode: true }); diff --git a/x-pack/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/timelines_table/icon_header_columns.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/open_timeline/timelines_table/icon_header_columns.tsx rename to x-pack/plugins/siem/public/timelines/components/open_timeline/timelines_table/icon_header_columns.tsx diff --git a/x-pack/plugins/siem/public/timelines/components/open_timeline/timelines_table/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/timelines_table/index.test.tsx new file mode 100644 index 00000000000000..1ebde8488e46cf --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/open_timeline/timelines_table/index.test.tsx @@ -0,0 +1,326 @@ +/* + * 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 euiDarkVars from '@elastic/eui/dist/eui_theme_dark.json'; +import { cloneDeep } from 'lodash/fp'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import React from 'react'; +import { ThemeProvider } from 'styled-components'; + +import { mockTimelineResults } from '../../../../common/mock/timeline_results'; +import { OpenTimelineResult } from '../types'; +import { TimelinesTable, TimelinesTableProps } from '.'; +import { getMockTimelinesTableProps } from './mocks'; + +import * as i18n from '../translations'; + +jest.mock('../../../../common/lib/kibana'); + +describe('TimelinesTable', () => { + const theme = () => ({ eui: euiDarkVars, darkMode: true }); + let mockResults: OpenTimelineResult[]; + + beforeEach(() => { + mockResults = cloneDeep(mockTimelineResults); + }); + + test('it renders the select all timelines header checkbox when actionTimelineToShow has the action selectable', () => { + const wrapper = mountWithIntl( + + + + ); + + expect( + wrapper + .find('thead tr th input') + .first() + .exists() + ).toBe(true); + }); + + test('it does NOT render the select all timelines header checkbox when actionTimelineToShow has not the action selectable', () => { + const testProps: TimelinesTableProps = { + ...getMockTimelinesTableProps(mockResults), + actionTimelineToShow: ['delete', 'duplicate'], + }; + const wrapper = mountWithIntl( + + + + ); + + expect( + wrapper + .find('thead tr th input') + .first() + .exists() + ).toBe(false); + }); + + test('it renders the Modified By column when showExtendedColumns is true ', () => { + const testProps: TimelinesTableProps = { + ...getMockTimelinesTableProps(mockResults), + showExtendedColumns: true, + }; + const wrapper = mountWithIntl( + + + + ); + + expect( + wrapper + .find('thead tr th') + .at(4) + .text() + ).toContain(i18n.MODIFIED_BY); + }); + + test('it renders the notes column in the position of the Modified By column when showExtendedColumns is false', () => { + const testProps: TimelinesTableProps = { + ...getMockTimelinesTableProps(mockResults), + showExtendedColumns: false, + }; + const wrapper = mountWithIntl( + + + + ); + + expect( + wrapper + .find('thead tr th') + .at(5) + .find('[data-test-subj="notes-count-header-icon"]') + .first() + .exists() + ).toBe(true); + }); + + test('it renders the delete timeline (trash icon) when actionTimelineToShow has the delete action', () => { + const wrapper = mountWithIntl( + + + + ); + + expect( + wrapper + .find('[data-test-subj="delete-timeline"]') + .first() + .exists() + ).toBe(true); + }); + + test('it does NOT render the delete timeline (trash icon) when actionTimelineToShow has NOT the delete action', () => { + const testProps: TimelinesTableProps = { + ...getMockTimelinesTableProps(mockResults), + actionTimelineToShow: ['duplicate', 'selectable'], + }; + const wrapper = mountWithIntl( + + + + ); + + expect( + wrapper + .find('[data-test-subj="delete-timeline"]') + .first() + .exists() + ).toBe(false); + }); + + test('it renders the rows per page selector when showExtendedColumns is true', () => { + const wrapper = mountWithIntl( + + + + ); + + expect( + wrapper + .find('EuiTablePagination EuiPopover') + .first() + .exists() + ).toBe(true); + }); + + test('it does NOT render the rows per page selector when showExtendedColumns is false', () => { + const testProps: TimelinesTableProps = { + ...getMockTimelinesTableProps(mockResults), + showExtendedColumns: false, + }; + const wrapper = mountWithIntl( + + + + ); + + expect( + wrapper + .find('EuiTablePagination EuiPopover') + .first() + .exists() + ).toBe(false); + }); + + test('it renders the default page size specified by the defaultPageSize prop', () => { + const defaultPageSize = 123; + const testProps = { + ...getMockTimelinesTableProps(mockResults), + defaultPageSize, + pageSize: defaultPageSize, + }; + const wrapper = mountWithIntl( + + + + ); + + expect( + wrapper + .find('EuiTablePagination EuiPopover') + .first() + .text() + ).toEqual('Rows per page: 123'); + }); + + test('it sorts the Last Modified column in descending order when showExtendedColumns is true ', () => { + const wrapper = mountWithIntl( + + + + ); + + expect( + wrapper + .find('[aria-sort="descending"]') + .first() + .text() + ).toContain(i18n.LAST_MODIFIED); + }); + + test('it sorts the Last Modified column in descending order when showExtendedColumns is false ', () => { + const testProps: TimelinesTableProps = { + ...getMockTimelinesTableProps(mockResults), + showExtendedColumns: false, + }; + const wrapper = mountWithIntl( + + + + ); + + expect( + wrapper + .find('[aria-sort="descending"]') + .first() + .text() + ).toContain(i18n.LAST_MODIFIED); + }); + + test('it displays the expected message when no search results are found', () => { + const testProps: TimelinesTableProps = { + ...getMockTimelinesTableProps(mockResults), + searchResults: [], + }; + const wrapper = mountWithIntl( + + + + ); + + expect( + wrapper + .find('tbody tr td div') + .first() + .text() + ).toEqual(i18n.ZERO_TIMELINES_MATCH); + }); + + test('it invokes onTableChange with the expected parameters when a table header is clicked to sort it', () => { + const onTableChange = jest.fn(); + const testProps: TimelinesTableProps = { + ...getMockTimelinesTableProps(mockResults), + onTableChange, + }; + const wrapper = mountWithIntl( + + + + ); + + wrapper + .find('thead tr th button') + .at(0) + .simulate('click'); + + wrapper.update(); + + expect(onTableChange).toHaveBeenCalledWith({ + page: { index: 0, size: 10 }, + sort: { direction: 'asc', field: 'updated' }, + }); + }); + + test('it invokes onSelectionChange when a row is selected', () => { + const onSelectionChange = jest.fn(); + const testProps: TimelinesTableProps = { + ...getMockTimelinesTableProps(mockResults), + onSelectionChange, + }; + const wrapper = mountWithIntl( + + + + ); + + wrapper + .find('thead tr th input') + .at(0) + .simulate('change', { target: { checked: true } }); + + wrapper.update(); + + expect(onSelectionChange).toHaveBeenCalled(); + }); + + test('it enables the table loading animation when isLoading is true', () => { + const testProps: TimelinesTableProps = { + ...getMockTimelinesTableProps(mockResults), + loading: true, + }; + const wrapper = mountWithIntl( + + + + ); + + const props = wrapper + .find('[data-test-subj="timelines-table"]') + .first() + .props() as TimelinesTableProps; + + expect(props.loading).toBe(true); + }); + + test('it disables the table loading animation when isLoading is false', () => { + const wrapper = mountWithIntl( + + + + ); + + const props = wrapper + .find('[data-test-subj="timelines-table"]') + .first() + .props() as TimelinesTableProps; + + expect(props.loading).toBe(false); + }); +}); diff --git a/x-pack/plugins/siem/public/components/open_timeline/timelines_table/index.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/timelines_table/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/open_timeline/timelines_table/index.tsx rename to x-pack/plugins/siem/public/timelines/components/open_timeline/timelines_table/index.tsx diff --git a/x-pack/plugins/siem/public/timelines/components/open_timeline/timelines_table/mocks.ts b/x-pack/plugins/siem/public/timelines/components/open_timeline/timelines_table/mocks.ts new file mode 100644 index 00000000000000..78ca898cc407e8 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/open_timeline/timelines_table/mocks.ts @@ -0,0 +1,32 @@ +/* + * 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_SEARCH_RESULTS_PER_PAGE } from '../../../pages/timelines_page'; +import { DEFAULT_SORT_DIRECTION, DEFAULT_SORT_FIELD } from '../constants'; +import { OpenTimelineResult } from '../types'; +import { TimelinesTableProps } from '.'; + +export const getMockTimelinesTableProps = ( + mockOpenTimelineResults: OpenTimelineResult[] +): TimelinesTableProps => ({ + actionTimelineToShow: ['delete', 'duplicate', 'selectable'], + deleteTimelines: jest.fn(), + defaultPageSize: DEFAULT_SEARCH_RESULTS_PER_PAGE, + enableExportTimelineDownloader: jest.fn(), + itemIdToExpandedNotesRowMap: {}, + loading: false, + onOpenDeleteTimelineModal: jest.fn(), + onOpenTimeline: jest.fn(), + onSelectionChange: jest.fn(), + onTableChange: jest.fn(), + onToggleShowNotes: jest.fn(), + pageIndex: 0, + pageSize: DEFAULT_SEARCH_RESULTS_PER_PAGE, + searchResults: mockOpenTimelineResults, + showExtendedColumns: true, + sortDirection: DEFAULT_SORT_DIRECTION, + sortField: DEFAULT_SORT_FIELD, + totalSearchResultsCount: mockOpenTimelineResults.length, +}); diff --git a/x-pack/plugins/siem/public/components/open_timeline/title_row/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/title_row/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/open_timeline/title_row/index.test.tsx rename to x-pack/plugins/siem/public/timelines/components/open_timeline/title_row/index.test.tsx diff --git a/x-pack/plugins/siem/public/timelines/components/open_timeline/title_row/index.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/title_row/index.tsx new file mode 100644 index 00000000000000..e5f921e397b03a --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/open_timeline/title_row/index.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import React from 'react'; + +import * as i18n from '../translations'; +import { OpenTimelineProps } from '../types'; +import { HeaderSection } from '../../../../common/components/header_section'; + +type Props = Pick & { + /** The number of timelines currently selected */ + selectedTimelinesCount: number; + children?: JSX.Element; +}; + +/** + * Renders the row containing the tile (e.g. Open Timelines / All timelines) + * and action buttons (i.e. Favorite Selected and Delete Selected) + */ +export const TitleRow = React.memo( + ({ children, onAddTimelinesToFavorites, selectedTimelinesCount, title }) => ( + + + {onAddTimelinesToFavorites && ( + + + {i18n.FAVORITE_SELECTED} + + + )} + + {children && {children}} + + + ) +); + +TitleRow.displayName = 'TitleRow'; diff --git a/x-pack/plugins/siem/public/components/open_timeline/translations.ts b/x-pack/plugins/siem/public/timelines/components/open_timeline/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/open_timeline/translations.ts rename to x-pack/plugins/siem/public/timelines/components/open_timeline/translations.ts diff --git a/x-pack/plugins/siem/public/timelines/components/open_timeline/types.ts b/x-pack/plugins/siem/public/timelines/components/open_timeline/types.ts new file mode 100644 index 00000000000000..f874b5f58d9856 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/open_timeline/types.ts @@ -0,0 +1,203 @@ +/* + * 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 { SetStateAction, Dispatch } from 'react'; +import { AllTimelinesVariables } from '../../containers/all'; +import { TimelineModel } from '../../../timelines/store/timeline/model'; +import { NoteResult } from '../../../graphql/types'; +import { TimelineType, TimelineTypeLiteral } from '../../../../common/types/timeline'; + +/** The users who added a timeline to favorites */ +export interface FavoriteTimelineResult { + userId?: number | null; + userName?: string | null; + favoriteDate?: number | null; +} + +export interface TimelineResultNote { + savedObjectId?: string | null; + note?: string | null; + noteId?: string | null; + updated?: number | null; + updatedBy?: string | null; +} + +export interface TimelineActionsOverflowColumns { + width: string; + actions: Array<{ + name: string; + icon?: string; + onClick?: (timeline: OpenTimelineResult) => void; + description: string; + render?: (timeline: OpenTimelineResult) => JSX.Element; + } | null>; +} + +/** The results of the query run by the OpenTimeline component */ +export interface OpenTimelineResult { + created?: number | null; + description?: string | null; + eventIdToNoteIds?: Readonly> | null; + favorite?: FavoriteTimelineResult[] | null; + noteIds?: string[] | null; + notes?: TimelineResultNote[] | null; + pinnedEventIds?: Readonly> | null; + savedObjectId?: string | null; + title?: string | null; + templateTimelineId?: string | null; + type?: TimelineType.template | TimelineType.default; + updated?: number | null; + updatedBy?: string | null; +} + +/** + * EuiSearchBar returns this object when the user changes the query. At the + * time of this writing, there is no typescript definition for this type, so + * only the properties used by the Open Timeline component are exposed. + */ +export interface EuiSearchBarQuery { + queryText: string; +} + +/** Performs IO to delete the specified timelines */ +export type DeleteTimelines = (timelineIds: string[], variables?: AllTimelinesVariables) => void; + +/** Invoked when the user clicks the action make the selected timelines favorites */ +export type OnAddTimelinesToFavorites = () => void; + +/** Invoked when the user clicks the action to delete the selected timelines */ +export type OnDeleteSelected = () => void; +export type OnDeleteOneTimeline = (timelineIds: string[]) => void; + +/** Invoked when the user clicks on the name of a timeline to open it */ +export type OnOpenTimeline = ({ + duplicate, + timelineId, +}: { + duplicate: boolean; + timelineId: string; +}) => void; + +export type OnOpenDeleteTimelineModal = (selectedItem: OpenTimelineResult) => void; +export type SetActionTimeline = Dispatch>; +export type EnableExportTimelineDownloader = (selectedItem: OpenTimelineResult) => void; +/** Invoked when the user presses enters to submit the text in the search input */ +export type OnQueryChange = (query: EuiSearchBarQuery) => void; + +/** Invoked when the user selects (or de-selects) timelines in the table */ +export type OnSelectionChange = (selectedItems: OpenTimelineResult[]) => void; + +/** Invoked when the user toggles the option to only view favorite timelines */ +export type OnToggleOnlyFavorites = () => void; + +/** Invoked when the user toggles the expansion or collapse of inline notes in a table row */ +export type OnToggleShowNotes = (itemIdToExpandedNotesRowMap: Record) => void; + +/** Parameters to the OnTableChange callback */ +export interface OnTableChangeParams { + page: { + index: number; + size: number; + }; + sort: { + field: string; + direction: 'asc' | 'desc'; + }; +} + +/** Invoked by the EUI table implementation when the user interacts with the table */ +export type OnTableChange = (tableChange: OnTableChangeParams) => void; + +export type ActionTimelineToShow = 'duplicate' | 'delete' | 'export' | 'selectable'; + +export interface OpenTimelineProps { + /** Invoked when the user clicks the delete (trash) icon on an individual timeline */ + deleteTimelines?: DeleteTimelines; + /** The default requested size of each page of search results */ + defaultPageSize: number; + /** Displays an indicator that data is loading when true */ + isLoading: boolean; + /** Required by EuiTable for expandable rows: a map of `TimelineResult.savedObjectId` to rendered notes */ + itemIdToExpandedNotesRowMap: Record; + /** Display import timelines modal*/ + importDataModalToggle?: boolean; + /** If this callback is specified, a "Favorite Selected" button will be displayed, and this callback will be invoked when the button is clicked */ + onAddTimelinesToFavorites?: OnAddTimelinesToFavorites; + /** If this callback is specified, a "Delete Selected" button will be displayed, and this callback will be invoked when the button is clicked */ + onDeleteSelected?: OnDeleteSelected; + /** Only show favorite timelines when true */ + onlyFavorites: boolean; + /** Invoked when the user presses enter after typing in the search bar */ + onQueryChange: OnQueryChange; + /** Invoked when the user selects (or de-selects) timelines in the table */ + onSelectionChange: OnSelectionChange; + /** Invoked when the user clicks on the name of a timeline to open it */ + onOpenTimeline: OnOpenTimeline; + /** Invoked by the EUI table implementation when the user interacts with the table */ + onTableChange: OnTableChange; + /** Invoked when the user toggles the option to only show favorite timelines */ + onToggleOnlyFavorites: OnToggleOnlyFavorites; + /** Invoked when the user toggles the expansion or collapse of inline notes in a table row */ + onToggleShowNotes: OnToggleShowNotes; + /** the requested page of results */ + pageIndex: number; + /** the requested size of each page of search results */ + pageSize: number; + /** The currently applied search criteria */ + query: string; + /** Refetch table */ + refetch?: (existingTimeline?: OpenTimelineResult[], existingCount?: number) => void; + /** The results of executing a search */ + searchResults: OpenTimelineResult[]; + /** the currently-selected timelines in the table */ + selectedItems: OpenTimelineResult[]; + /** Toggle export timelines modal*/ + setImportDataModalToggle?: React.Dispatch>; + /** the requested sort direction of the query results */ + sortDirection: 'asc' | 'desc'; + /** the requested field to sort on */ + sortField: string; + /** timeline / template timeline */ + tabs: JSX.Element; + /** The title of the Open Timeline component */ + title: string; + /** The total (server-side) count of the search results */ + totalSearchResultsCount: number; + /** Hide action on timeline if needed it */ + hideActions?: ActionTimelineToShow[]; +} + +export interface UpdateTimeline { + duplicate: boolean; + id: string; + from: number; + notes: NoteResult[] | null | undefined; + timeline: TimelineModel; + to: number; + ruleNote?: string; +} + +export type DispatchUpdateTimeline = ({ + duplicate, + id, + from, + notes, + timeline, + to, + ruleNote, +}: UpdateTimeline) => () => void; + +export enum TimelineTabsStyle { + tab = 'tab', + filter = 'filter', +} + +export interface TimelineTab { + id: TimelineTypeLiteral; + name: string; + disabled: boolean; + href: string; +} diff --git a/x-pack/plugins/siem/public/components/open_timeline/use_timeline_types.tsx b/x-pack/plugins/siem/public/timelines/components/open_timeline/use_timeline_types.tsx similarity index 92% rename from x-pack/plugins/siem/public/components/open_timeline/use_timeline_types.tsx rename to x-pack/plugins/siem/public/timelines/components/open_timeline/use_timeline_types.tsx index 1e23bc5bdda3cb..f99d8c566c4a54 100644 --- a/x-pack/plugins/siem/public/components/open_timeline/use_timeline_types.tsx +++ b/x-pack/plugins/siem/public/timelines/components/open_timeline/use_timeline_types.tsx @@ -7,12 +7,11 @@ import React, { useState, useCallback, useMemo } from 'react'; import { useParams } from 'react-router-dom'; import { EuiTabs, EuiTab, EuiSpacer, EuiFilterButton } from '@elastic/eui'; -import { TimelineTypeLiteralWithNull, TimelineType } from '../../../common/types/timeline'; - -import { getTimelineTabsUrl } from '../link_to'; -import { useGetUrlSearch } from '../navigation/use_get_url_search'; -import { navTabs } from '../../pages/home/home_navigations'; +import { TimelineTypeLiteralWithNull, TimelineType } from '../../../../common/types/timeline'; +import { getTimelineTabsUrl } from '../../../common/components/link_to'; +import { navTabs } from '../../../app/home/home_navigations'; +import { useGetUrlSearch } from '../../../common/components/navigation/use_get_url_search'; import * as i18n from './translations'; import { TimelineTabsStyle, TimelineTab } from './types'; diff --git a/x-pack/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/__snapshots__/timeline.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/__snapshots__/timeline.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/and_or_badge/__examples__/index.stories.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/and_or_badge/__examples__/index.stories.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/and_or_badge/__examples__/index.stories.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/and_or_badge/__examples__/index.stories.tsx diff --git a/x-pack/plugins/siem/public/components/and_or_badge/index.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/and_or_badge/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/and_or_badge/index.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/and_or_badge/index.tsx diff --git a/x-pack/plugins/siem/public/components/and_or_badge/translations.ts b/x-pack/plugins/siem/public/timelines/components/timeline/and_or_badge/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/and_or_badge/translations.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/and_or_badge/translations.ts diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/auto_save_warning/index.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/auto_save_warning/index.tsx new file mode 100644 index 00000000000000..210af7a571569a --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/auto_save_warning/index.tsx @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + EuiButton, + EuiFlexGroup, + EuiFlexItem, + EuiGlobalToastListToast as Toast, +} from '@elastic/eui'; +import { getOr } from 'lodash/fp'; +import React from 'react'; +import { connect, ConnectedProps } from 'react-redux'; + +import { State } from '../../../../common/store'; +import { setTimelineRangeDatePicker as dispatchSetTimelineRangeDatePicker } from '../../../../common/store/inputs/actions'; +import { timelineActions, timelineSelectors } from '../../../store/timeline'; +import { AutoSavedWarningMsg } from '../../../store/timeline/types'; +import { useStateToaster } from '../../../../common/components/toasters'; +import * as i18n from './translations'; + +const AutoSaveWarningMsgComponent = React.memo( + ({ + newTimelineModel, + setTimelineRangeDatePicker, + timelineId, + updateAutoSaveMsg, + updateTimeline, + }) => { + const dispatchToaster = useStateToaster()[1]; + if (timelineId != null && newTimelineModel != null) { + const toast: Toast = { + id: 'AutoSaveWarningMsg', + title: i18n.TITLE, + color: 'warning', + iconType: 'alert', + toastLifeTimeMs: 10000, + text: ( + <> +

{i18n.DESCRIPTION}

+ + + { + updateTimeline({ id: timelineId, timeline: newTimelineModel }); + updateAutoSaveMsg({ timelineId: null, newTimelineModel: null }); + setTimelineRangeDatePicker({ + from: getOr(0, 'dateRange.start', newTimelineModel), + to: getOr(0, 'dateRange.end', newTimelineModel), + }); + }} + > + {i18n.REFRESH_TIMELINE} + + + + + ), + }; + dispatchToaster({ + type: 'addToaster', + toast, + }); + } + + return null; + } +); + +AutoSaveWarningMsgComponent.displayName = 'AutoSaveWarningMsgComponent'; + +const mapStateToProps = (state: State) => { + const autoSaveMessage: AutoSavedWarningMsg = timelineSelectors.autoSaveMsgSelector(state); + + return { + timelineId: autoSaveMessage.timelineId, + newTimelineModel: autoSaveMessage.newTimelineModel, + }; +}; + +const mapDispatchToProps = { + setTimelineRangeDatePicker: dispatchSetTimelineRangeDatePicker, + updateAutoSaveMsg: timelineActions.updateAutoSaveMsg, + updateTimeline: timelineActions.updateTimeline, +}; + +const connector = connect(mapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const AutoSaveWarningMsg = connector(AutoSaveWarningMsgComponent); diff --git a/x-pack/plugins/siem/public/components/timeline/auto_save_warning/translations.ts b/x-pack/plugins/siem/public/timelines/components/timeline/auto_save_warning/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/auto_save_warning/translations.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/auto_save_warning/translations.ts diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/actions/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/actions/index.test.tsx new file mode 100644 index 00000000000000..ee177f4aba05ed --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/actions/index.test.tsx @@ -0,0 +1,265 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; + +import { TestProviders } from '../../../../../common/mock'; +import { DEFAULT_ACTIONS_COLUMN_WIDTH } from '../constants'; + +import { Actions } from '.'; + +describe('Actions', () => { + test('it renders a checkbox for selecting the event when `showCheckboxes` is `true`', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="select-event"]').exists()).toEqual(true); + }); + + test('it does NOT render a checkbox for selecting the event when `showCheckboxes` is `false`', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="select-event"]').exists()).toBe(false); + }); + + test('it renders a button for expanding the event', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="expand-event"]').exists()).toEqual(true); + }); + + test('it invokes onEventToggled when the button to expand an event is clicked', () => { + const onEventToggled = jest.fn(); + + const wrapper = mount( + + + + ); + + wrapper + .find('[data-test-subj="expand-event"]') + .first() + .simulate('click'); + + expect(onEventToggled).toBeCalled(); + }); + + test('it does NOT render a notes button when isEventsViewer is true', () => { + const toggleShowNotes = jest.fn(); + + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="timeline-notes-button-small"]').exists()).toBe(false); + }); + + test('it invokes toggleShowNotes when the button for adding notes is clicked', () => { + const toggleShowNotes = jest.fn(); + + const wrapper = mount( + + + + ); + + wrapper + .find('[data-test-subj="timeline-notes-button-small"]') + .first() + .simulate('click'); + + expect(toggleShowNotes).toBeCalled(); + }); + + test('it does NOT render a pin button when isEventViewer is true', () => { + const onPinClicked = jest.fn(); + + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="pin"]').exists()).toBe(false); + }); + + test('it invokes onPinClicked when the button for pinning events is clicked', () => { + const onPinClicked = jest.fn(); + + const wrapper = mount( + + + + ); + + wrapper + .find('[data-test-subj="pin"]') + .first() + .simulate('click'); + + expect(onPinClicked).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/actions/index.tsx new file mode 100644 index 00000000000000..d36a064b6cc7d6 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/actions/index.tsx @@ -0,0 +1,177 @@ +/* + * 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 { EuiButtonIcon, EuiCheckbox, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui'; +import React from 'react'; + +import { Note } from '../../../../../common/lib/note'; +import { AssociateNote, UpdateNote } from '../../../notes/helpers'; +import { Pin } from '../../pin'; +import { NotesButton } from '../../properties/helpers'; +import { EventsLoading, EventsTd, EventsTdContent, EventsTdGroupActions } from '../../styles'; +import { eventHasNotes, getPinTooltip } from '../helpers'; +import * as i18n from '../translations'; +import { OnRowSelected } from '../../events'; +import { Ecs } from '../../../../../graphql/types'; + +export interface TimelineActionProps { + eventId: string; + ecsData: Ecs; +} + +export interface TimelineAction { + getAction: ({ eventId, ecsData }: TimelineActionProps) => JSX.Element; + width: number; + id: string; +} + +interface Props { + actionsColumnWidth: number; + additionalActions?: JSX.Element[]; + associateNote: AssociateNote; + checked: boolean; + onRowSelected: OnRowSelected; + expanded: boolean; + eventId: string; + eventIsPinned: boolean; + getNotesByIds: (noteIds: string[]) => Note[]; + isEventViewer?: boolean; + loading: boolean; + loadingEventIds: Readonly; + noteIds: string[]; + onEventToggled: () => void; + onPinClicked: () => void; + showNotes: boolean; + showCheckboxes: boolean; + toggleShowNotes: () => void; + updateNote: UpdateNote; +} + +const emptyNotes: string[] = []; + +export const Actions = React.memo( + ({ + actionsColumnWidth, + additionalActions, + associateNote, + checked, + expanded, + eventId, + eventIsPinned, + getNotesByIds, + isEventViewer = false, + loading = false, + loadingEventIds, + noteIds, + onEventToggled, + onPinClicked, + onRowSelected, + showCheckboxes, + showNotes, + toggleShowNotes, + updateNote, + }) => ( + + {showCheckboxes && ( + + + {loadingEventIds.includes(eventId) ? ( + + ) : ( + ) => { + onRowSelected({ + eventIds: [eventId], + isSelected: event.currentTarget.checked, + }); + }} + /> + )} + + + )} + + <>{additionalActions} + + + + {loading && } + + {!loading && ( + + )} + + + + {!isEventViewer && ( + <> + + + + + + + + + + + + + + + )} + + ), + (nextProps, prevProps) => { + return ( + prevProps.actionsColumnWidth === nextProps.actionsColumnWidth && + prevProps.checked === nextProps.checked && + prevProps.expanded === nextProps.expanded && + prevProps.eventId === nextProps.eventId && + prevProps.eventIsPinned === nextProps.eventIsPinned && + prevProps.loading === nextProps.loading && + prevProps.loadingEventIds === nextProps.loadingEventIds && + prevProps.noteIds === nextProps.noteIds && + prevProps.onRowSelected === nextProps.onRowSelected && + prevProps.showCheckboxes === nextProps.showCheckboxes && + prevProps.showNotes === nextProps.showNotes + ); + } +); +Actions.displayName = 'Actions'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/actions/index.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/actions/index.tsx new file mode 100644 index 00000000000000..8ec7c52179b99a --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/actions/index.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiButtonIcon } from '@elastic/eui'; +import React from 'react'; + +import { ColumnHeaderOptions } from '../../../../../../timelines/store/timeline/model'; +import { OnColumnRemoved } from '../../../events'; +import { EventsHeadingExtra, EventsLoading } from '../../../styles'; +import { useTimelineContext } from '../../../timeline_context'; +import { Sort } from '../../sort'; + +import * as i18n from '../translations'; + +interface Props { + header: ColumnHeaderOptions; + onColumnRemoved: OnColumnRemoved; + sort: Sort; +} + +/** Given a `header`, returns the `SortDirection` applicable to it */ + +export const CloseButton = React.memo<{ + columnId: string; + onColumnRemoved: OnColumnRemoved; +}>(({ columnId, onColumnRemoved }) => ( + ) => { + // To avoid a re-sorting when you delete a column + event.preventDefault(); + event.stopPropagation(); + onColumnRemoved(columnId); + }} + /> +)); + +CloseButton.displayName = 'CloseButton'; + +export const Actions = React.memo(({ header, onColumnRemoved, sort }) => { + const { isLoading } = useTimelineContext(); + return ( + <> + {sort.columnId === header.id && isLoading ? ( + + + + ) : ( + + + + )} + + ); +}); + +Actions.displayName = 'Actions'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/column_headers/column_header.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/column_header.tsx similarity index 95% rename from x-pack/plugins/siem/public/components/timeline/body/column_headers/column_header.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/column_header.tsx index e070ed8fa1d2ab..10f2d264d65d2d 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/column_headers/column_header.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/column_header.tsx @@ -9,8 +9,8 @@ import { Draggable } from 'react-beautiful-dnd'; import { Resizable, ResizeCallback } from 're-resizable'; import deepEqual from 'fast-deep-equal'; -import { ColumnHeaderOptions } from '../../../../store/timeline/model'; -import { getDraggableFieldId } from '../../../drag_and_drop/helpers'; +import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; +import { getDraggableFieldId } from '../../../../../common/components/drag_and_drop/helpers'; import { OnColumnRemoved, OnColumnSorted, OnFilterChange, OnColumnResized } from '../../events'; import { EventsTh, EventsThContent, EventsHeadingHandle } from '../../styles'; import { Sort } from '../sort'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/column_headers/common/dragging_container.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/common/dragging_container.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/column_headers/common/dragging_container.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/common/dragging_container.tsx diff --git a/x-pack/plugins/siem/public/components/timeline/body/column_headers/common/styles.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/common/styles.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/column_headers/common/styles.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/common/styles.tsx diff --git a/x-pack/plugins/siem/public/components/timeline/body/column_headers/default_headers.ts b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/default_headers.ts similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/column_headers/default_headers.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/default_headers.ts diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/events_select/helpers.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/events_select/helpers.tsx new file mode 100644 index 00000000000000..9b2cb2e97b98a5 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/events_select/helpers.tsx @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiText } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +import { Pin } from '../../../pin'; + +import * as i18n from './translations'; + +const InputDisplay = styled.div` + width: 5px; +`; + +InputDisplay.displayName = 'InputDisplay'; + +const PinIconContainer = styled.div` + margin-right: 5px; +`; + +PinIconContainer.displayName = 'PinIconContainer'; + +const PinActionItem = styled.div` + display: flex; + flex-direction: row; +`; + +PinActionItem.displayName = 'PinActionItem'; + +export type EventsSelectAction = + | 'select-all' + | 'select-none' + | 'select-pinned' + | 'select-unpinned' + | 'pin-selected' + | 'unpin-selected'; + +export interface EventsSelectOption { + value: EventsSelectAction; + inputDisplay: JSX.Element | string; + disabled?: boolean; + dropdownDisplay: JSX.Element | string; +} + +export const DropdownDisplay = React.memo<{ text: string }>(({ text }) => ( + + {text} + +)); + +DropdownDisplay.displayName = 'DropdownDisplay'; + +export const getEventsSelectOptions = (): EventsSelectOption[] => [ + { + inputDisplay: , + disabled: true, + dropdownDisplay: , + value: 'select-all', + }, + { + inputDisplay: , + disabled: true, + dropdownDisplay: , + value: 'select-none', + }, + { + inputDisplay: , + disabled: true, + dropdownDisplay: , + value: 'select-pinned', + }, + { + inputDisplay: , + disabled: true, + dropdownDisplay: , + value: 'select-unpinned', + }, + { + inputDisplay: , + disabled: true, + dropdownDisplay: ( + + + + + + + ), + value: 'pin-selected', + }, + { + inputDisplay: , + disabled: true, + dropdownDisplay: ( + + + + + + + ), + value: 'unpin-selected', + }, +]; diff --git a/x-pack/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/events_select/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/column_headers/events_select/index.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/events_select/index.tsx diff --git a/x-pack/plugins/siem/public/components/timeline/body/column_headers/events_select/translations.ts b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/events_select/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/column_headers/events_select/translations.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/events_select/translations.ts diff --git a/x-pack/plugins/siem/public/components/timeline/body/column_headers/filter/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/filter/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/column_headers/filter/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/filter/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/filter/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/filter/index.test.tsx new file mode 100644 index 00000000000000..9d1920b03c9bec --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/filter/index.test.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mount, shallow } from 'enzyme'; +import React from 'react'; + +import { ColumnHeaderType } from '../../../../../../timelines/store/timeline/model'; +import { defaultHeaders } from '../default_headers'; + +import { Filter } from '.'; + +const textFilter: ColumnHeaderType = 'text-filter'; +const notFiltered: ColumnHeaderType = 'not-filtered'; + +describe('Filter', () => { + test('renders correctly against snapshot', () => { + const textFilterColumnHeader = { + ...defaultHeaders[0], + columnHeaderType: textFilter, + }; + + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + describe('rendering', () => { + test('it renders a text filter when the columnHeaderType is "text-filter"', () => { + const textFilterColumnHeader = { + ...defaultHeaders[0], + columnHeaderType: textFilter, + }; + + const wrapper = mount(); + + expect( + wrapper + .find('[data-test-subj="textFilter"]') + .first() + .props() + ).toHaveProperty('placeholder'); + }); + + test('it does NOT render a filter when the columnHeaderType is "not-filtered"', () => { + const notFilteredHeader = { + ...defaultHeaders[0], + columnHeaderType: notFiltered, + }; + + const wrapper = mount(); + + expect(wrapper.find('[data-test-subj="textFilter"]').exists()).toEqual(false); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/filter/index.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/filter/index.tsx new file mode 100644 index 00000000000000..9daccf27399fb6 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/filter/index.tsx @@ -0,0 +1,37 @@ +/* + * 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 { noop } from 'lodash/fp'; +import React from 'react'; + +import { OnFilterChange } from '../../../events'; +import { ColumnHeaderOptions } from '../../../../../../timelines/store/timeline/model'; +import { TextFilter } from '../text_filter'; + +interface Props { + header: ColumnHeaderOptions; + onFilterChange?: OnFilterChange; +} + +/** Renders a header's filter, based on the `columnHeaderType` */ +export const Filter = React.memo(({ header, onFilterChange = noop }) => { + switch (header.columnHeaderType) { + case 'text-filter': + return ( + + ); + case 'not-filtered': // fall through + default: + return null; + } +}); + +Filter.displayName = 'Filter'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/header/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/timeline/body/column_headers/header/header_content.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/header/header_content.tsx similarity index 93% rename from x-pack/plugins/siem/public/components/timeline/body/column_headers/header/header_content.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/header/header_content.tsx index 0a69cef6185706..83e3728c149016 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/column_headers/header/header_content.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/header/header_content.tsx @@ -8,8 +8,8 @@ import { EuiToolTip } from '@elastic/eui'; import { noop } from 'lodash/fp'; import React from 'react'; -import { ColumnHeaderOptions } from '../../../../../store/timeline/model'; -import { TruncatableText } from '../../../../truncatable_text'; +import { ColumnHeaderOptions } from '../../../../../../timelines/store/timeline/model'; +import { TruncatableText } from '../../../../../../common/components/truncatable_text'; import { EventsHeading, EventsHeadingTitleButton, EventsHeadingTitleSpan } from '../../../styles'; import { useTimelineContext } from '../../../timeline_context'; import { Sort } from '../../sort'; diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/header/helpers.ts b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/header/helpers.ts new file mode 100644 index 00000000000000..6d70795c422d93 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/header/helpers.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Direction } from '../../../../../../graphql/types'; +import { assertUnreachable } from '../../../../../../common/lib/helpers'; +import { ColumnHeaderOptions } from '../../../../../../timelines/store/timeline/model'; +import { Sort, SortDirection } from '../../sort'; + +interface GetNewSortDirectionOnClickParams { + clickedHeader: ColumnHeaderOptions; + currentSort: Sort; +} + +/** Given a `header`, returns the `SortDirection` applicable to it */ +export const getNewSortDirectionOnClick = ({ + clickedHeader, + currentSort, +}: GetNewSortDirectionOnClickParams): Direction => + clickedHeader.id === currentSort.columnId ? getNextSortDirection(currentSort) : Direction.desc; + +/** Given a current sort direction, it returns the next sort direction */ +export const getNextSortDirection = (currentSort: Sort): Direction => { + switch (currentSort.sortDirection) { + case Direction.desc: + return Direction.asc; + case Direction.asc: + return Direction.desc; + case 'none': + return Direction.desc; + default: + return assertUnreachable(currentSort.sortDirection, 'Unhandled sort direction'); + } +}; + +interface GetSortDirectionParams { + header: ColumnHeaderOptions; + sort: Sort; +} + +export const getSortDirection = ({ header, sort }: GetSortDirectionParams): SortDirection => + header.id === sort.columnId ? sort.sortDirection : 'none'; diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/header/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/header/index.test.tsx new file mode 100644 index 00000000000000..dfbb5508f27c74 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/header/index.test.tsx @@ -0,0 +1,350 @@ +/* + * 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 { mount, shallow } from 'enzyme'; +import React from 'react'; + +import { Direction } from '../../../../../../graphql/types'; +import { TestProviders } from '../../../../../../common/mock'; +import { ColumnHeaderType } from '../../../../../../timelines/store/timeline/model'; +import { Sort } from '../../sort'; +import { CloseButton } from '../actions'; +import { defaultHeaders } from '../default_headers'; + +import { HeaderComponent } from '.'; +import { getNewSortDirectionOnClick, getNextSortDirection, getSortDirection } from './helpers'; + +const filteredColumnHeader: ColumnHeaderType = 'text-filter'; + +describe('Header', () => { + const columnHeader = defaultHeaders[0]; + const sort: Sort = { + columnId: columnHeader.id, + sortDirection: Direction.desc, + }; + const timelineId = 'fakeId'; + + test('renders correctly against snapshot', () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); + + describe('rendering', () => { + test('it renders the header text', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find(`[data-test-subj="header-text-${columnHeader.id}"]`) + .first() + .text() + ).toEqual(columnHeader.id); + }); + + test('it renders the header text alias when label is provided', () => { + const label = 'Timestamp'; + const headerWithLabel = { ...columnHeader, label }; + const wrapper = mount( + + + + ); + + expect( + wrapper + .find(`[data-test-subj="header-text-${columnHeader.id}"]`) + .first() + .text() + ).toEqual(label); + }); + + test('it renders a sort indicator', () => { + const headerSortable = { ...columnHeader, aggregatable: true }; + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="header-sort-indicator"]') + .first() + .exists() + ).toEqual(true); + }); + + test('it renders a filter', () => { + const columnWithFilter = { + ...columnHeader, + columnHeaderType: filteredColumnHeader, + }; + + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="textFilter"]') + .first() + .props() + ).toHaveProperty('placeholder'); + }); + }); + + describe('onColumnSorted', () => { + test('it invokes the onColumnSorted callback when the header sort button is clicked', () => { + const mockOnColumnSorted = jest.fn(); + const headerSortable = { ...columnHeader, aggregatable: true }; + const wrapper = mount( + + + + ); + + wrapper + .find('[data-test-subj="header-sort-button"]') + .first() + .simulate('click'); + + expect(mockOnColumnSorted).toBeCalledWith({ + columnId: columnHeader.id, + sortDirection: 'asc', // (because the previous state was Direction.desc) + }); + }); + + test('it does NOT render the header sort button when aggregatable is false', () => { + const mockOnColumnSorted = jest.fn(); + const headerSortable = { ...columnHeader, aggregatable: false }; + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="header-sort-button"]').length).toEqual(0); + }); + + test('it does NOT render the header sort button when aggregatable is missing', () => { + const mockOnColumnSorted = jest.fn(); + const headerSortable = { ...columnHeader }; + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="header-sort-button"]').length).toEqual(0); + }); + + test('it does NOT invoke the onColumnSorted callback when the header is clicked and aggregatable is undefined', () => { + const mockOnColumnSorted = jest.fn(); + const headerSortable = { ...columnHeader, aggregatable: undefined }; + const wrapper = mount( + + + + ); + + wrapper + .find('[data-test-subj="header"]') + .first() + .simulate('click'); + + expect(mockOnColumnSorted).not.toHaveBeenCalled(); + }); + }); + + describe('CloseButton', () => { + test('it invokes the onColumnRemoved callback with the column ID when the close button is clicked', () => { + const mockOnColumnRemoved = jest.fn(); + + const wrapper = mount( + + ); + + wrapper + .find('[data-test-subj="remove-column"]') + .first() + .simulate('click'); + + expect(mockOnColumnRemoved).toBeCalledWith(columnHeader.id); + }); + }); + + describe('getSortDirection', () => { + test('it returns the sort direction when the header id matches the sort column id', () => { + expect(getSortDirection({ header: columnHeader, sort })).toEqual(sort.sortDirection); + }); + + test('it returns "none" when sort direction when the header id does NOT match the sort column id', () => { + const nonMatching: Sort = { + columnId: 'differentSocks', + sortDirection: Direction.desc, + }; + + expect(getSortDirection({ header: columnHeader, sort: nonMatching })).toEqual('none'); + }); + }); + + describe('getNextSortDirection', () => { + test('it returns "asc" when the current direction is "desc"', () => { + const sortDescending: Sort = { columnId: columnHeader.id, sortDirection: Direction.desc }; + + expect(getNextSortDirection(sortDescending)).toEqual('asc'); + }); + + test('it returns "desc" when the current direction is "asc"', () => { + const sortAscending: Sort = { + columnId: columnHeader.id, + sortDirection: Direction.asc, + }; + + expect(getNextSortDirection(sortAscending)).toEqual(Direction.desc); + }); + + test('it returns "desc" by default', () => { + const sortNone: Sort = { + columnId: columnHeader.id, + sortDirection: 'none', + }; + + expect(getNextSortDirection(sortNone)).toEqual(Direction.desc); + }); + }); + + describe('getNewSortDirectionOnClick', () => { + test('it returns the expected new sort direction when the header id matches the sort column id', () => { + const sortMatches: Sort = { + columnId: columnHeader.id, + sortDirection: Direction.desc, + }; + + expect( + getNewSortDirectionOnClick({ + clickedHeader: columnHeader, + currentSort: sortMatches, + }) + ).toEqual(Direction.asc); + }); + + test('it returns the expected new sort direction when the header id does NOT match the sort column id', () => { + const sortDoesNotMatch: Sort = { + columnId: 'someOtherColumn', + sortDirection: 'none', + }; + + expect( + getNewSortDirectionOnClick({ + clickedHeader: columnHeader, + currentSort: sortDoesNotMatch, + }) + ).toEqual(Direction.desc); + }); + }); + + describe('text truncation styling', () => { + test('truncates the header text with an ellipsis', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find(`[data-test-subj="header-text-${columnHeader.id}"]`)).toHaveStyleRule( + 'text-overflow', + 'ellipsis' + ); + }); + }); + + describe('header tooltip', () => { + test('it has a tooltip to display the properties of the field', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="header-tooltip"]').exists()).toEqual(true); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/header/index.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/header/index.tsx new file mode 100644 index 00000000000000..854d45449c92cd --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/header/index.tsx @@ -0,0 +1,55 @@ +/* + * 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 { noop } from 'lodash/fp'; +import React, { useCallback } from 'react'; + +import { ColumnHeaderOptions } from '../../../../../../timelines/store/timeline/model'; +import { OnColumnRemoved, OnColumnSorted, OnFilterChange } from '../../../events'; +import { Sort } from '../../sort'; +import { Actions } from '../actions'; +import { Filter } from '../filter'; +import { getNewSortDirectionOnClick } from './helpers'; +import { HeaderContent } from './header_content'; + +interface Props { + header: ColumnHeaderOptions; + onColumnRemoved: OnColumnRemoved; + onColumnSorted: OnColumnSorted; + onFilterChange?: OnFilterChange; + sort: Sort; + timelineId: string; +} + +export const HeaderComponent: React.FC = ({ + header, + onColumnRemoved, + onColumnSorted, + onFilterChange = noop, + sort, +}) => { + const onClick = useCallback(() => { + onColumnSorted!({ + columnId: header.id, + sortDirection: getNewSortDirectionOnClick({ + clickedHeader: header, + currentSort: sort, + }), + }); + }, [onColumnSorted, header, sort]); + + return ( + <> + + + + + + + ); +}; + +export const Header = React.memo(HeaderComponent); diff --git a/x-pack/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/header_tooltip_content/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/column_headers/header_tooltip_content/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/header_tooltip_content/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx new file mode 100644 index 00000000000000..534dd7bc9b73c2 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.test.tsx @@ -0,0 +1,93 @@ +/* + * 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 { mount, shallow } from 'enzyme'; +import { cloneDeep } from 'lodash/fp'; +import React from 'react'; + +import { ColumnHeaderOptions } from '../../../../../../timelines/store/timeline/model'; +import { defaultHeaders } from '../../../../../../common/mock'; + +import { HeaderToolTipContent } from '.'; + +describe('HeaderToolTipContent', () => { + let header: ColumnHeaderOptions; + beforeEach(() => { + header = cloneDeep(defaultHeaders[0]); + }); + + test('it renders the category', () => { + const wrapper = mount(); + + expect( + wrapper + .find('[data-test-subj="category-value"]') + .first() + .text() + ).toEqual(header.category); + }); + + test('it renders the name of the field', () => { + const wrapper = mount(); + + expect( + wrapper + .find('[data-test-subj="field-value"]') + .first() + .text() + ).toEqual(header.id); + }); + + test('it renders the expected icon for the header type', () => { + const wrapper = mount(); + + expect( + wrapper + .find('[data-test-subj="type-icon"]') + .first() + .props().type + ).toEqual('clock'); + }); + + test('it renders the type of the field', () => { + const wrapper = mount(); + + expect( + wrapper + .find('[data-test-subj="type-value"]') + .first() + .text() + ).toEqual(header.type); + }); + + test('it renders the description of the field', () => { + const wrapper = mount(); + + expect( + wrapper + .find('[data-test-subj="description-value"]') + .first() + .text() + ).toEqual(header.description); + }); + + test('it does NOT render the description column when the field does NOT contain a description', () => { + const noDescription = { + ...header, + description: '', + }; + + const wrapper = mount(); + + expect(wrapper.find('[data-test-subj="description"]').exists()).toEqual(false); + }); + + test('it renders the expected table content', () => { + const wrapper = shallow(); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.tsx new file mode 100644 index 00000000000000..efad85775a9e46 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/header_tooltip_content/index.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiIcon } from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; +import React from 'react'; +import styled from 'styled-components'; + +import { ColumnHeaderOptions } from '../../../../../../timelines/store/timeline/model'; +import { getIconFromType } from '../../../../../../common/components/event_details/helpers'; +import * as i18n from '../translations'; + +const IconType = styled(EuiIcon)` + margin-right: 3px; + position: relative; + top: -2px; +`; +IconType.displayName = 'IconType'; + +const P = styled.p` + margin-bottom: 5px; +`; +P.displayName = 'P'; + +const ToolTipTableMetadata = styled.span` + margin-right: 5px; +`; +ToolTipTableMetadata.displayName = 'ToolTipTableMetadata'; + +const ToolTipTableValue = styled.span` + word-wrap: break-word; +`; +ToolTipTableValue.displayName = 'ToolTipTableValue'; + +export const HeaderToolTipContent = React.memo<{ header: ColumnHeaderOptions }>(({ header }) => ( + <> + {!isEmpty(header.category) && ( +

+ + {i18n.CATEGORY} + {':'} + + {header.category} +

+ )} +

+ + {i18n.FIELD} + {':'} + + {header.id} +

+

+ + {i18n.TYPE} + {':'} + + + + {header.type} + +

+ {!isEmpty(header.description) && ( +

+ + {i18n.DESCRIPTION} + {':'} + + + {header.description} + +

+ )} + +)); +HeaderToolTipContent.displayName = 'HeaderToolTipContent'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/column_headers/helpers.test.ts b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/helpers.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/column_headers/helpers.test.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/helpers.test.ts diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/helpers.ts b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/helpers.ts new file mode 100644 index 00000000000000..7c29f1498d0df7 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/helpers.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get } from 'lodash/fp'; + +import { BrowserFields } from '../../../../../common/containers/source'; +import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; +import { + DEFAULT_COLUMN_MIN_WIDTH, + DEFAULT_DATE_COLUMN_MIN_WIDTH, + SHOW_CHECK_BOXES_COLUMN_WIDTH, + EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH, + DEFAULT_ACTIONS_COLUMN_WIDTH, +} from '../constants'; + +/** Enriches the column headers with field details from the specified browserFields */ +export const getColumnHeaders = ( + headers: ColumnHeaderOptions[], + browserFields: BrowserFields +): ColumnHeaderOptions[] => { + return headers.map(header => { + const splitHeader = header.id.split('.'); // source.geo.city_name -> [source, geo, city_name] + + return { + ...header, + ...get( + [splitHeader.length > 1 ? splitHeader[0] : 'base', 'fields', header.id], + browserFields + ), + }; + }); +}; + +export const getColumnWidthFromType = (type: string): number => + type !== 'date' ? DEFAULT_COLUMN_MIN_WIDTH : DEFAULT_DATE_COLUMN_MIN_WIDTH; + +/** Returns the (fixed) width of the Actions column */ +export const getActionsColumnWidth = ( + isEventViewer: boolean, + showCheckboxes = false, + additionalActionWidth = 0 +): number => + (showCheckboxes ? SHOW_CHECK_BOXES_COLUMN_WIDTH : 0) + + (isEventViewer ? EVENTS_VIEWER_ACTIONS_COLUMN_WIDTH : DEFAULT_ACTIONS_COLUMN_WIDTH) + + additionalActionWidth; diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/index.test.tsx new file mode 100644 index 00000000000000..446e6f2758e4c5 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/index.test.tsx @@ -0,0 +1,113 @@ +/* + * 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 { DEFAULT_ACTIONS_COLUMN_WIDTH } from '../constants'; +import { defaultHeaders } from './default_headers'; +import { Direction } from '../../../../../graphql/types'; +import { mockBrowserFields } from '../../../../../common/containers/source/mock'; +import { Sort } from '../sort'; +import { TestProviders } from '../../../../../common/mock/test_providers'; +import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; + +import { ColumnHeadersComponent } from '.'; + +describe('ColumnHeaders', () => { + const mount = useMountAppended(); + + describe('rendering', () => { + const sort: Sort = { + columnId: 'fooColumn', + sortDirection: Direction.desc, + }; + + test('renders correctly against snapshot', () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); + + test('it renders the field browser', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="field-browser"]') + .first() + .exists() + ).toEqual(true); + }); + + test('it renders every column header', () => { + const wrapper = mount( + + + + ); + + defaultHeaders.forEach(h => { + expect( + wrapper + .find('[data-test-subj="headers-group"]') + .first() + .text() + ).toContain(h.id); + }); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/index.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/index.tsx new file mode 100644 index 00000000000000..7a5ce5ac3c7c9f --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/index.tsx @@ -0,0 +1,253 @@ +/* + * 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 { EuiCheckbox } from '@elastic/eui'; +import { noop } from 'lodash/fp'; +import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import { Droppable, DraggableChildrenFn } from 'react-beautiful-dnd'; +import deepEqual from 'fast-deep-equal'; + +import { DragEffects } from '../../../../../common/components/drag_and_drop/draggable_wrapper'; +import { DraggableFieldBadge } from '../../../../../common/components/draggables/field_badge'; +import { BrowserFields } from '../../../../../common/containers/source'; +import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; +import { + DRAG_TYPE_FIELD, + droppableTimelineColumnsPrefix, +} from '../../../../../common/components/drag_and_drop/helpers'; +import { StatefulFieldsBrowser } from '../../../fields_browser'; +import { FIELD_BROWSER_HEIGHT, FIELD_BROWSER_WIDTH } from '../../../fields_browser/helpers'; +import { + OnColumnRemoved, + OnColumnResized, + OnColumnSorted, + OnFilterChange, + OnSelectAll, + OnUpdateColumns, +} from '../../events'; +import { + EventsTh, + EventsThContent, + EventsThead, + EventsThGroupActions, + EventsThGroupData, + EventsTrHeader, +} from '../../styles'; +import { Sort } from '../sort'; +import { EventsSelect } from './events_select'; +import { ColumnHeader } from './column_header'; + +interface Props { + actionsColumnWidth: number; + browserFields: BrowserFields; + columnHeaders: ColumnHeaderOptions[]; + isEventViewer?: boolean; + isSelectAllChecked: boolean; + onColumnRemoved: OnColumnRemoved; + onColumnResized: OnColumnResized; + onColumnSorted: OnColumnSorted; + onFilterChange?: OnFilterChange; + onSelectAll: OnSelectAll; + onUpdateColumns: OnUpdateColumns; + showEventsSelect: boolean; + showSelectAllCheckbox: boolean; + sort: Sort; + timelineId: string; + toggleColumn: (column: ColumnHeaderOptions) => void; +} + +interface DraggableContainerProps { + children: React.ReactNode; + onMount: () => void; + onUnmount: () => void; +} + +export const DraggableContainer = React.memo( + ({ children, onMount, onUnmount }) => { + useEffect(() => { + onMount(); + + return () => onUnmount(); + }, [onMount, onUnmount]); + + return <>{children}; + } +); + +DraggableContainer.displayName = 'DraggableContainer'; + +/** Renders the timeline header columns */ +export const ColumnHeadersComponent = ({ + actionsColumnWidth, + browserFields, + columnHeaders, + isEventViewer = false, + isSelectAllChecked, + onColumnRemoved, + onColumnResized, + onColumnSorted, + onSelectAll, + onUpdateColumns, + onFilterChange = noop, + showEventsSelect, + showSelectAllCheckbox, + sort, + timelineId, + toggleColumn, +}: Props) => { + const [draggingIndex, setDraggingIndex] = useState(null); + + const handleSelectAllChange = useCallback( + (event: React.ChangeEvent) => { + onSelectAll({ isSelected: event.currentTarget.checked }); + }, + [onSelectAll] + ); + + const renderClone: DraggableChildrenFn = useCallback( + (dragProvided, dragSnapshot, rubric) => { + // TODO: Remove after github.com/DefinitelyTyped/DefinitelyTyped/pull/43057 is merged + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const index = (rubric as any).source.index; + const header = columnHeaders[index]; + + const onMount = () => setDraggingIndex(index); + const onUnmount = () => setDraggingIndex(null); + + return ( + + + + + + + + ); + }, + [columnHeaders, setDraggingIndex] + ); + + const ColumnHeaderList = useMemo( + () => + columnHeaders.map((header, draggableIndex) => ( + + )), + [ + columnHeaders, + timelineId, + draggingIndex, + onColumnRemoved, + onFilterChange, + onColumnResized, + sort, + ] + ); + + return ( + + + + {showEventsSelect && ( + + + + + + )} + {showSelectAllCheckbox && ( + + + + + + )} + + + + + + + + + {(dropProvided, snapshot) => ( + <> + + {ColumnHeaderList} + + + )} + + + + ); +}; + +export const ColumnHeaders = React.memo( + ColumnHeadersComponent, + (prevProps, nextProps) => + prevProps.actionsColumnWidth === nextProps.actionsColumnWidth && + prevProps.isEventViewer === nextProps.isEventViewer && + prevProps.isSelectAllChecked === nextProps.isSelectAllChecked && + prevProps.onColumnRemoved === nextProps.onColumnRemoved && + prevProps.onColumnResized === nextProps.onColumnResized && + prevProps.onColumnSorted === nextProps.onColumnSorted && + prevProps.onSelectAll === nextProps.onSelectAll && + prevProps.onUpdateColumns === nextProps.onUpdateColumns && + prevProps.onFilterChange === nextProps.onFilterChange && + prevProps.showEventsSelect === nextProps.showEventsSelect && + prevProps.showSelectAllCheckbox === nextProps.showSelectAllCheckbox && + prevProps.sort === nextProps.sort && + prevProps.timelineId === nextProps.timelineId && + prevProps.toggleColumn === nextProps.toggleColumn && + deepEqual(prevProps.columnHeaders, nextProps.columnHeaders) && + deepEqual(prevProps.browserFields, nextProps.browserFields) +); diff --git a/x-pack/plugins/siem/public/components/timeline/body/column_headers/range_picker/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/range_picker/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/column_headers/range_picker/index.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/range_picker/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/timeline/body/column_headers/range_picker/index.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/range_picker/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/column_headers/range_picker/index.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/range_picker/index.tsx diff --git a/x-pack/plugins/siem/public/components/timeline/body/column_headers/range_picker/ranges.ts b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/range_picker/ranges.ts similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/column_headers/range_picker/ranges.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/range_picker/ranges.ts diff --git a/x-pack/plugins/siem/public/components/timeline/body/column_headers/range_picker/translations.ts b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/range_picker/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/column_headers/range_picker/translations.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/range_picker/translations.ts diff --git a/x-pack/plugins/siem/public/components/timeline/body/column_headers/text_filter/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/text_filter/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/column_headers/text_filter/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/text_filter/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/timeline/body/column_headers/text_filter/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/text_filter/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/column_headers/text_filter/index.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/text_filter/index.test.tsx diff --git a/x-pack/plugins/siem/public/components/timeline/body/column_headers/text_filter/index.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/text_filter/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/column_headers/text_filter/index.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/text_filter/index.tsx diff --git a/x-pack/plugins/siem/public/components/timeline/body/column_headers/translations.ts b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/column_headers/translations.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/body/column_headers/translations.ts diff --git a/x-pack/plugins/siem/public/components/timeline/body/column_id.ts b/x-pack/plugins/siem/public/timelines/components/timeline/body/column_id.ts similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/column_id.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/body/column_id.ts diff --git a/x-pack/plugins/siem/public/components/timeline/body/constants.ts b/x-pack/plugins/siem/public/timelines/components/timeline/body/constants.ts similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/constants.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/body/constants.ts diff --git a/x-pack/plugins/siem/public/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/body/data_driven_columns/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/data_driven_columns/index.test.tsx new file mode 100644 index 00000000000000..8a2cc88eea9106 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/data_driven_columns/index.test.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 { shallow } from 'enzyme'; + +import React from 'react'; + +import { mockTimelineData } from '../../../../../common/mock'; +import { defaultHeaders } from '../column_headers/default_headers'; +import { columnRenderers } from '../renderers'; + +import { DataDrivenColumns } from '.'; + +describe('Columns', () => { + const headersSansTimestamp = defaultHeaders.filter(h => h.id !== '@timestamp'); + + test('it renders the expected columns', () => { + const wrapper = shallow( + + ); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/data_driven_columns/index.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/data_driven_columns/index.tsx new file mode 100644 index 00000000000000..da00e4054a763d --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/data_driven_columns/index.tsx @@ -0,0 +1,66 @@ +/* + * 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 { getOr } from 'lodash/fp'; + +import { Ecs, TimelineNonEcsData } from '../../../../../graphql/types'; +import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; +import { OnColumnResized } from '../../events'; +import { EventsTd, EventsTdContent, EventsTdGroupData } from '../../styles'; +import { ColumnRenderer } from '../renderers/column_renderer'; +import { getColumnRenderer } from '../renderers/get_column_renderer'; + +interface Props { + _id: string; + columnHeaders: ColumnHeaderOptions[]; + columnRenderers: ColumnRenderer[]; + data: TimelineNonEcsData[]; + ecsData: Ecs; + onColumnResized: OnColumnResized; + timelineId: string; +} + +export const DataDrivenColumns = React.memo( + ({ _id, columnHeaders, columnRenderers, data, ecsData, timelineId }) => ( + + {columnHeaders.map(header => ( + + + {getColumnRenderer(header.id, columnRenderers, data).renderColumn({ + columnName: header.id, + eventId: _id, + field: header, + linkValues: getOr([], header.linkField ?? '', ecsData), + timelineId, + truncate: true, + values: getMappedNonEcsValue({ + data, + fieldName: header.id, + }), + })} + + + ))} + + ) +); + +DataDrivenColumns.displayName = 'DataDrivenColumns'; + +const getMappedNonEcsValue = ({ + data, + fieldName, +}: { + data: TimelineNonEcsData[]; + fieldName: string; +}): string[] | undefined => { + const item = data.find(d => d.field === fieldName); + if (item != null && item.value != null) { + return item.value; + } + return undefined; +}; diff --git a/x-pack/plugins/siem/public/components/timeline/body/events/event_column_view.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/events/event_column_view.tsx similarity index 95% rename from x-pack/plugins/siem/public/components/timeline/body/events/event_column_view.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/events/event_column_view.tsx index daf9c3d8b1f963..2b143d34d3814e 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/events/event_column_view.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/events/event_column_view.tsx @@ -7,9 +7,9 @@ import React, { useMemo } from 'react'; import uuid from 'uuid'; -import { TimelineNonEcsData, Ecs } from '../../../../graphql/types'; -import { Note } from '../../../../lib/note'; -import { ColumnHeaderOptions } from '../../../../store/timeline/model'; +import { TimelineNonEcsData, Ecs } from '../../../../../graphql/types'; +import { Note } from '../../../../../common/lib/note'; +import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; import { AssociateNote, UpdateNote } from '../../../notes/helpers'; import { OnColumnResized, OnPinEvent, OnRowSelected, OnUnPinEvent } from '../../events'; import { EventsTdContent, EventsTrData } from '../../styles'; diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/events/index.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/events/index.tsx new file mode 100644 index 00000000000000..fc892f5b8e6b13 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/events/index.tsx @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; + +import { BrowserFields } from '../../../../../common/containers/source'; +import { TimelineItem, TimelineNonEcsData } from '../../../../../graphql/types'; +import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; +import { maxDelay } from '../../../../../common/lib/helpers/scheduler'; +import { Note } from '../../../../../common/lib/note'; +import { AddNoteToEvent, UpdateNote } from '../../../notes/helpers'; +import { + OnColumnResized, + OnPinEvent, + OnRowSelected, + OnUnPinEvent, + OnUpdateColumns, +} from '../../events'; +import { EventsTbody } from '../../styles'; +import { ColumnRenderer } from '../renderers/column_renderer'; +import { RowRenderer } from '../renderers/row_renderer'; +import { StatefulEvent } from './stateful_event'; +import { eventIsPinned } from '../helpers'; + +interface Props { + actionsColumnWidth: number; + addNoteToEvent: AddNoteToEvent; + browserFields: BrowserFields; + columnHeaders: ColumnHeaderOptions[]; + columnRenderers: ColumnRenderer[]; + containerElementRef: HTMLDivElement; + data: TimelineItem[]; + eventIdToNoteIds: Readonly>; + getNotesByIds: (noteIds: string[]) => Note[]; + id: string; + isEventViewer?: boolean; + loadingEventIds: Readonly; + onColumnResized: OnColumnResized; + onPinEvent: OnPinEvent; + onRowSelected: OnRowSelected; + onUpdateColumns: OnUpdateColumns; + onUnPinEvent: OnUnPinEvent; + pinnedEventIds: Readonly>; + rowRenderers: RowRenderer[]; + selectedEventIds: Readonly>; + showCheckboxes: boolean; + toggleColumn: (column: ColumnHeaderOptions) => void; + updateNote: UpdateNote; +} + +const EventsComponent: React.FC = ({ + actionsColumnWidth, + addNoteToEvent, + browserFields, + columnHeaders, + columnRenderers, + containerElementRef, + data, + eventIdToNoteIds, + getNotesByIds, + id, + isEventViewer = false, + loadingEventIds, + onColumnResized, + onPinEvent, + onRowSelected, + onUpdateColumns, + onUnPinEvent, + pinnedEventIds, + rowRenderers, + selectedEventIds, + showCheckboxes, + toggleColumn, + updateNote, +}) => ( + + {data.map((event, i) => ( + + ))} + +); + +export const Events = React.memo(EventsComponent); diff --git a/x-pack/plugins/siem/public/components/timeline/body/events/stateful_event.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/events/stateful_event.tsx similarity index 94% rename from x-pack/plugins/siem/public/components/timeline/body/events/stateful_event.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/events/stateful_event.tsx index 6e5c292064dc63..61c58095189285 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/events/stateful_event.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/events/stateful_event.tsx @@ -8,14 +8,14 @@ import React, { useEffect, useRef, useState, useCallback } from 'react'; import uuid from 'uuid'; import VisibilitySensor from 'react-visibility-sensor'; -import { BrowserFields } from '../../../../containers/source'; -import { TimelineDetailsQuery } from '../../../../containers/timeline/details'; -import { TimelineItem, DetailItem, TimelineNonEcsData } from '../../../../graphql/types'; -import { requestIdleCallbackViaScheduler } from '../../../../lib/helpers/scheduler'; -import { Note } from '../../../../lib/note'; -import { ColumnHeaderOptions } from '../../../../store/timeline/model'; +import { BrowserFields } from '../../../../../common/containers/source'; +import { TimelineDetailsQuery } from '../../../../containers/details'; +import { TimelineItem, DetailItem, TimelineNonEcsData } from '../../../../../graphql/types'; +import { requestIdleCallbackViaScheduler } from '../../../../../common/lib/helpers/scheduler'; +import { Note } from '../../../../../common/lib/note'; +import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; import { AddNoteToEvent, UpdateNote } from '../../../notes/helpers'; -import { SkeletonRow } from '../../../skeleton_row'; +import { SkeletonRow } from '../../skeleton_row'; import { OnColumnResized, OnPinEvent, @@ -31,7 +31,7 @@ import { getRowRenderer } from '../renderers/get_row_renderer'; import { RowRenderer } from '../renderers/row_renderer'; import { getEventType } from '../helpers'; import { NoteCards } from '../../../notes/note_cards'; -import { useEventDetailsWidthContext } from '../../../events_viewer/event_details_width_context'; +import { useEventDetailsWidthContext } from '../../../../../common/components/events_viewer/event_details_width_context'; import { EventColumnView } from './event_column_view'; interface Props { diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/helpers.test.ts b/x-pack/plugins/siem/public/timelines/components/timeline/body/helpers.test.ts new file mode 100644 index 00000000000000..e237e99df9ada4 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/helpers.test.ts @@ -0,0 +1,228 @@ +/* + * 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 { Ecs } from '../../../../graphql/types'; + +import { eventHasNotes, eventIsPinned, getPinTooltip, stringifyEvent } from './helpers'; + +describe('helpers', () => { + describe('stringifyEvent', () => { + test('it omits __typename when it appears at arbitrary levels', () => { + const toStringify: Ecs = { + __typename: 'level 0', + _id: '4', + timestamp: '2018-11-08T19:03:25.937Z', + host: { + __typename: 'level 1', + name: ['suricata'], + ip: ['192.168.0.1'], + }, + event: { + id: ['4'], + category: ['Attempted Administrator Privilege Gain'], + type: ['Alert'], + module: ['suricata'], + severity: [1], + }, + source: { + ip: ['192.168.0.3'], + port: [53], + }, + destination: { + ip: ['192.168.0.3'], + port: [6343], + }, + suricata: { + eve: { + flow_id: [4], + proto: [''], + alert: { + signature: ['ET PHONE HOME Stack Overflow (CVE-2019-90210)'], + signature_id: [4], + __typename: 'level 2', + }, + }, + }, + user: { + id: ['4'], + name: ['jack.black'], + }, + geo: { + region_name: ['neither'], + country_iso_code: ['sasquatch'], + }, + } as Ecs; // as cast so that `__typename` can be added for the tests even though it is not part of ECS + const expected: Ecs = { + _id: '4', + timestamp: '2018-11-08T19:03:25.937Z', + host: { + name: ['suricata'], + ip: ['192.168.0.1'], + }, + event: { + id: ['4'], + category: ['Attempted Administrator Privilege Gain'], + type: ['Alert'], + module: ['suricata'], + severity: [1], + }, + source: { + ip: ['192.168.0.3'], + port: [53], + }, + destination: { + ip: ['192.168.0.3'], + port: [6343], + }, + suricata: { + eve: { + flow_id: [4], + proto: [''], + alert: { + signature: ['ET PHONE HOME Stack Overflow (CVE-2019-90210)'], + signature_id: [4], + }, + }, + }, + user: { + id: ['4'], + name: ['jack.black'], + }, + geo: { + region_name: ['neither'], + country_iso_code: ['sasquatch'], + }, + }; + expect(JSON.parse(stringifyEvent(toStringify))).toEqual(expected); + }); + + test('it omits null and undefined values at arbitrary levels, for arbitrary data types', () => { + const expected: Ecs = { + _id: '4', + host: {}, + event: { + id: ['4'], + category: ['theory'], + type: ['Alert'], + module: ['me'], + severity: [1], + }, + source: { + port: [53], + }, + destination: { + ip: ['192.168.0.3'], + port: [6343], + }, + suricata: { + eve: { + flow_id: [4], + proto: [''], + alert: { + signature: ['dance moves'], + }, + }, + }, + user: { + id: ['4'], + name: ['no use for a'], + }, + geo: { + region_name: ['bizzaro'], + country_iso_code: ['world'], + }, + }; + const toStringify: Ecs = { + _id: '4', + timestamp: null, + host: { + name: null, + ip: null, + }, + event: { + id: ['4'], + category: ['theory'], + type: ['Alert'], + module: ['me'], + severity: [1], + }, + source: { + ip: undefined, + port: [53], + }, + destination: { + ip: ['192.168.0.3'], + port: [6343], + }, + suricata: { + eve: { + flow_id: [4], + proto: [''], + alert: { + signature: ['dance moves'], + signature_id: undefined, + }, + }, + }, + user: { + id: ['4'], + name: ['no use for a'], + }, + geo: { + region_name: ['bizzaro'], + country_iso_code: ['world'], + }, + }; + expect(JSON.parse(stringifyEvent(toStringify))).toEqual(expected); + }); + }); + + describe('eventHasNotes', () => { + test('it returns false for when notes is empty', () => { + expect(eventHasNotes([])).toEqual(false); + }); + + test('it returns true when notes is non-empty', () => { + expect(eventHasNotes(['8af859e2-e4f8-4754-b702-4f227f15aae5'])).toEqual(true); + }); + }); + + describe('getPinTooltip', () => { + test('it indicates the event may NOT be unpinned when `isPinned` is `true` and the event has notes', () => { + expect(getPinTooltip({ isPinned: true, eventHasNotes: true })).toEqual( + 'This event cannot be unpinned because it has notes' + ); + }); + + test('it indicates the event is pinned when `isPinned` is `true` and the event does NOT have notes', () => { + expect(getPinTooltip({ isPinned: true, eventHasNotes: false })).toEqual('Pinned event'); + }); + + test('it indicates the event is NOT pinned when `isPinned` is `false` and the event has notes', () => { + expect(getPinTooltip({ isPinned: false, eventHasNotes: true })).toEqual('Unpinned event'); + }); + + test('it indicates the event is NOT pinned when `isPinned` is `false` and the event does NOT have notes', () => { + expect(getPinTooltip({ isPinned: false, eventHasNotes: false })).toEqual('Unpinned event'); + }); + }); + + describe('eventIsPinned', () => { + test('returns true when the specified event id is contained in the pinnedEventIds', () => { + const eventId = 'race-for-the-prize'; + const pinnedEventIds = { [eventId]: true, 'waiting-for-superman': true }; + + expect(eventIsPinned({ eventId, pinnedEventIds })).toEqual(true); + }); + + test('returns false when the specified event id is NOT contained in the pinnedEventIds', () => { + const eventId = 'safety-pin'; + const pinnedEventIds = { 'thumb-tack': true }; + + expect(eventIsPinned({ eventId, pinnedEventIds })).toEqual(false); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/helpers.ts b/x-pack/plugins/siem/public/timelines/components/timeline/body/helpers.ts new file mode 100644 index 00000000000000..a3eb3cc651f7ac --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/helpers.ts @@ -0,0 +1,89 @@ +/* + * 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 { isEmpty, noop } from 'lodash/fp'; + +import { Ecs, TimelineItem, TimelineNonEcsData } from '../../../../graphql/types'; +import { EventType } from '../../../../timelines/store/timeline/model'; +import { OnPinEvent, OnUnPinEvent } from '../events'; + +import * as i18n from './translations'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const omitTypenameAndEmpty = (k: string, v: any): any | undefined => + k !== '__typename' && v != null ? v : undefined; + +export const stringifyEvent = (ecs: Ecs): string => JSON.stringify(ecs, omitTypenameAndEmpty, 2); + +export const eventHasNotes = (noteIds: string[]): boolean => !isEmpty(noteIds); + +export const getPinTooltip = ({ + isPinned, + // eslint-disable-next-line no-shadow + eventHasNotes, +}: { + isPinned: boolean; + eventHasNotes: boolean; +}) => (isPinned && eventHasNotes ? i18n.PINNED_WITH_NOTES : isPinned ? i18n.PINNED : i18n.UNPINNED); + +export interface IsPinnedParams { + eventId: string; + pinnedEventIds: Readonly>; +} + +export const eventIsPinned = ({ eventId, pinnedEventIds }: IsPinnedParams): boolean => + pinnedEventIds[eventId] === true; + +export interface GetPinOnClickParams { + allowUnpinning: boolean; + eventId: string; + onPinEvent: OnPinEvent; + onUnPinEvent: OnUnPinEvent; + isEventPinned: boolean; +} + +export const getPinOnClick = ({ + allowUnpinning, + eventId, + onPinEvent, + onUnPinEvent, + isEventPinned, +}: GetPinOnClickParams): (() => void) => { + if (!allowUnpinning) { + return noop; + } + return isEventPinned ? () => onUnPinEvent(eventId) : () => onPinEvent(eventId); +}; + +/** + * Creates mapping of eventID -> fieldData for given fieldsToKeep. Used to store additional field + * data necessary for custom timeline actions in conjunction with selection state + * @param timelineData + * @param eventIds + * @param fieldsToKeep + */ +export const getEventIdToDataMapping = ( + timelineData: TimelineItem[], + eventIds: string[], + fieldsToKeep: string[] +): Record => { + return timelineData.reduce((acc, v) => { + const fvm = eventIds.includes(v._id) + ? { [v._id]: v.data.filter(ti => fieldsToKeep.includes(ti.field)) } + : {}; + return { + ...acc, + ...fvm, + }; + }, {}); +}; + +/** Return eventType raw or signal */ +export const getEventType = (event: Ecs): Omit => { + if (!isEmpty(event.signal?.rule?.id)) { + return 'signal'; + } + return 'raw'; +}; diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/index.test.tsx new file mode 100644 index 00000000000000..c2c3f4dd7f12ef --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/index.test.tsx @@ -0,0 +1,355 @@ +/* + * 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 { mockBrowserFields } from '../../../../common/containers/source/mock'; +import { Direction } from '../../../../graphql/types'; +import { defaultHeaders, mockTimelineData } from '../../../../common/mock'; +import { TestProviders } from '../../../../common/mock/test_providers'; + +import { Body, BodyProps } from '.'; +import { columnRenderers, rowRenderers } from './renderers'; +import { Sort } from './sort'; +import { wait } from '../../../../common/lib/helpers'; +import { useMountAppended } from '../../../../common/utils/use_mount_appended'; + +const testBodyHeight = 700; +const mockGetNotesByIds = (eventId: string[]) => []; +const mockSort: Sort = { + columnId: '@timestamp', + sortDirection: Direction.desc, +}; + +jest.mock( + 'react-visibility-sensor', + () => ({ children }: { children: (args: { isVisible: boolean }) => React.ReactNode }) => + children({ isVisible: true }) +); + +jest.mock('../../../../common/lib/helpers/scheduler', () => ({ + requestIdleCallbackViaScheduler: (callback: () => void, opts?: unknown) => { + callback(); + }, + maxDelay: () => 3000, +})); + +describe('Body', () => { + const mount = useMountAppended(); + + describe('rendering', () => { + test('it renders the column headers', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="column-headers"]') + .first() + .exists() + ).toEqual(true); + }); + + test('it renders the scroll container', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="timeline-body"]') + .first() + .exists() + ).toEqual(true); + }); + + test('it renders events', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="events"]') + .first() + .exists() + ).toEqual(true); + }); + + test('it renders a tooltip for timestamp', async () => { + const headersJustTimestamp = defaultHeaders.filter(h => h.id === '@timestamp'); + + const wrapper = mount( + + + + ); + wrapper.update(); + await wait(); + wrapper.update(); + headersJustTimestamp.forEach(() => { + expect( + wrapper + .find('[data-test-subj="data-driven-columns"]') + .first() + .find('[data-test-subj="localized-date-tool-tip"]') + .exists() + ).toEqual(true); + }); + }); + }); + + describe('action on event', () => { + const dispatchAddNoteToEvent = jest.fn(); + const dispatchOnPinEvent = jest.fn(); + + const addaNoteToEvent = (wrapper: ReturnType, note: string) => { + wrapper + .find('[data-test-subj="add-note"]') + .first() + .find('button') + .simulate('click'); + wrapper.update(); + wrapper + .find('[data-test-subj="new-note-tabs"] textarea') + .simulate('change', { target: { value: note } }); + wrapper.update(); + wrapper + .find('button[data-test-subj="add-note"]') + .first() + .simulate('click'); + wrapper.update(); + }; + + // We are doing that because we need to wrapped this component with redux + // and redux does not like to be updated and since we need to update our + // child component (BODY) and we do not want to scare anyone with this error + // we are hiding it!!! + // eslint-disable-next-line no-console + const originalError = console.error; + beforeAll(() => { + // eslint-disable-next-line no-console + console.error = (...args: string[]) => { + if (/ does not support changing `store` on the fly/.test(args[0])) { + return; + } + originalError.call(console, ...args); + }; + }); + + beforeEach(() => { + dispatchAddNoteToEvent.mockClear(); + dispatchOnPinEvent.mockClear(); + }); + + test('Add a Note to an event', () => { + const wrapper = mount( + + + + ); + addaNoteToEvent(wrapper, 'hello world'); + + expect(dispatchAddNoteToEvent).toHaveBeenCalled(); + expect(dispatchOnPinEvent).toHaveBeenCalled(); + }); + + test('Add two Note to an event', () => { + const Proxy = (props: BodyProps) => ( + + + + ); + + const wrapper = mount( + + ); + addaNoteToEvent(wrapper, 'hello world'); + dispatchAddNoteToEvent.mockClear(); + dispatchOnPinEvent.mockClear(); + wrapper.setProps({ pinnedEventIds: { 1: true } }); + wrapper.update(); + addaNoteToEvent(wrapper, 'new hello world'); + expect(dispatchAddNoteToEvent).toHaveBeenCalled(); + expect(dispatchOnPinEvent).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/index.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/index.tsx new file mode 100644 index 00000000000000..391d19cb7855c9 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/index.tsx @@ -0,0 +1,169 @@ +/* + * 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, { useMemo, useRef } from 'react'; + +import { BrowserFields } from '../../../../common/containers/source'; +import { TimelineItem, TimelineNonEcsData } from '../../../../graphql/types'; +import { Note } from '../../../../common/lib/note'; +import { ColumnHeaderOptions } from '../../../../timelines/store/timeline/model'; +import { AddNoteToEvent, UpdateNote } from '../../notes/helpers'; +import { + OnColumnRemoved, + OnColumnResized, + OnColumnSorted, + OnFilterChange, + OnPinEvent, + OnRowSelected, + OnSelectAll, + OnUnPinEvent, + OnUpdateColumns, +} from '../events'; +import { EventsTable, TimelineBody, TimelineBodyGlobalStyle } from '../styles'; +import { ColumnHeaders } from './column_headers'; +import { getActionsColumnWidth } from './column_headers/helpers'; +import { Events } from './events'; +import { ColumnRenderer } from './renderers/column_renderer'; +import { RowRenderer } from './renderers/row_renderer'; +import { Sort } from './sort'; +import { useTimelineTypeContext } from '../timeline_context'; + +export interface BodyProps { + addNoteToEvent: AddNoteToEvent; + browserFields: BrowserFields; + columnHeaders: ColumnHeaderOptions[]; + columnRenderers: ColumnRenderer[]; + data: TimelineItem[]; + getNotesByIds: (noteIds: string[]) => Note[]; + height?: number; + id: string; + isEventViewer?: boolean; + isSelectAllChecked: boolean; + eventIdToNoteIds: Readonly>; + loadingEventIds: Readonly; + onColumnRemoved: OnColumnRemoved; + onColumnResized: OnColumnResized; + onColumnSorted: OnColumnSorted; + onRowSelected: OnRowSelected; + onSelectAll: OnSelectAll; + onFilterChange: OnFilterChange; + onPinEvent: OnPinEvent; + onUpdateColumns: OnUpdateColumns; + onUnPinEvent: OnUnPinEvent; + pinnedEventIds: Readonly>; + rowRenderers: RowRenderer[]; + selectedEventIds: Readonly>; + showCheckboxes: boolean; + sort: Sort; + toggleColumn: (column: ColumnHeaderOptions) => void; + updateNote: UpdateNote; +} + +/** Renders the timeline body */ +export const Body = React.memo( + ({ + addNoteToEvent, + browserFields, + columnHeaders, + columnRenderers, + data, + eventIdToNoteIds, + getNotesByIds, + height, + id, + isEventViewer = false, + isSelectAllChecked, + loadingEventIds, + onColumnRemoved, + onColumnResized, + onColumnSorted, + onRowSelected, + onSelectAll, + onFilterChange, + onPinEvent, + onUpdateColumns, + onUnPinEvent, + pinnedEventIds, + rowRenderers, + selectedEventIds, + showCheckboxes, + sort, + toggleColumn, + updateNote, + }) => { + const containerElementRef = useRef(null); + const timelineTypeContext = useTimelineTypeContext(); + const additionalActionWidth = useMemo( + () => timelineTypeContext.timelineActions?.reduce((acc, v) => acc + v.width, 0) ?? 0, + [timelineTypeContext.timelineActions] + ); + const actionsColumnWidth = useMemo( + () => getActionsColumnWidth(isEventViewer, showCheckboxes, additionalActionWidth), + [isEventViewer, showCheckboxes, additionalActionWidth] + ); + + const columnWidths = useMemo( + () => + columnHeaders.reduce((totalWidth, header) => totalWidth + header.width, actionsColumnWidth), + [actionsColumnWidth, columnHeaders] + ); + + return ( + <> + + + + + + + + + + ); + } +); +Body.displayName = 'Body'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/mini_map/date_ranges.test.ts b/x-pack/plugins/siem/public/timelines/components/timeline/body/mini_map/date_ranges.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/mini_map/date_ranges.test.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/body/mini_map/date_ranges.test.ts diff --git a/x-pack/plugins/siem/public/components/timeline/body/mini_map/date_ranges.ts b/x-pack/plugins/siem/public/timelines/components/timeline/body/mini_map/date_ranges.ts similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/mini_map/date_ranges.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/body/mini_map/date_ranges.ts diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/args.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/__snapshots__/args.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/args.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/__snapshots__/args.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/empty_column_renderer.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/__snapshots__/empty_column_renderer.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/empty_column_renderer.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/__snapshots__/empty_column_renderer.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/formatted_field.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/__snapshots__/formatted_field.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/formatted_field.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/__snapshots__/formatted_field.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/__snapshots__/get_column_renderer.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/get_row_renderer.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/__snapshots__/get_row_renderer.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/get_row_renderer.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/__snapshots__/get_row_renderer.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/host_working_dir.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/__snapshots__/host_working_dir.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/host_working_dir.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/__snapshots__/host_working_dir.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/__snapshots__/plain_column_renderer.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_row_renderer.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/__snapshots__/plain_row_renderer.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/plain_row_renderer.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/__snapshots__/plain_row_renderer.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/process_draggable.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/__snapshots__/process_draggable.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/process_draggable.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/__snapshots__/process_draggable.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/unknown_column_renderer.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/__snapshots__/unknown_column_renderer.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/unknown_column_renderer.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/__snapshots__/unknown_column_renderer.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/user_host_working_dir.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/__snapshots__/user_host_working_dir.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/__snapshots__/user_host_working_dir.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/__snapshots__/user_host_working_dir.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/args.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/args.test.tsx similarity index 95% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/args.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/args.test.tsx index 53a20544124400..e7e7d1d47f4783 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/args.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/args.test.tsx @@ -7,8 +7,8 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { useMountAppended } from '../../../../utils/use_mount_appended'; -import { TestProviders } from '../../../../mock'; +import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; +import { TestProviders } from '../../../../../common/mock'; import { ArgsComponent } from './args'; describe('Args', () => { diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/args.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/args.tsx similarity index 95% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/args.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/args.tsx index 22367ec879851a..f421b471282be9 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/args.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/args.tsx @@ -6,7 +6,7 @@ import React from 'react'; -import { DraggableBadge } from '../../../draggables'; +import { DraggableBadge } from '../../../../../common/components/draggables'; import { isNillEmptyOrNotFinite, TokensFlexItem } from './helpers'; interface Props { diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_details.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/__snapshots__/generic_details.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_details.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/__snapshots__/generic_details.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_file_details.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/__snapshots__/generic_file_details.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_file_details.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/__snapshots__/generic_file_details.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/__snapshots__/generic_row_renderer.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/primary_secondary_user_info.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/__snapshots__/primary_secondary_user_info.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/primary_secondary_user_info.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/__snapshots__/primary_secondary_user_info.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/session_user_host_working_dir.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/__snapshots__/session_user_host_working_dir.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/__snapshots__/session_user_host_working_dir.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/__snapshots__/session_user_host_working_dir.test.tsx.snap diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/generic_details.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/generic_details.test.tsx new file mode 100644 index 00000000000000..b4c95d383593af --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/generic_details.test.tsx @@ -0,0 +1,485 @@ +/* + * 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 { BrowserFields } from '../../../../../../common/containers/source'; +import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; +import { mockTimelineData, TestProviders } from '../../../../../../common/mock'; +import { AuditdGenericDetails, AuditdGenericLine } from './generic_details'; +import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; + +describe('GenericDetails', () => { + const mount = useMountAppended(); + + describe('rendering', () => { + test('it renders the default AuditAcquiredCredsDetails', () => { + // I cannot and do not want to use BrowserFields for the mocks for the snapshot tests as they are too heavy + const browserFields: BrowserFields = {}; + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); + + test('it returns auditd if the data does contain auditd data', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual( + 'Sessionalice@zeek-sanfranin/generic-text-123gpgconf(5402)gpgconf--list-dirsagent-socketgpgconf --list-dirs agent-socket' + ); + }); + + test('it returns null for text if the data contains no auditd data', () => { + const wrapper = shallow( + + ); + expect(wrapper.isEmptyRender()).toBeTruthy(); + }); + }); + + describe('#AuditdConnectedToLine', () => { + test('it returns pretty output if you send in all your happy path data', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual( + 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123process-name-1(123)arg1arg2arg3process-title-1with resultsuccess' + ); + }); + + test('it returns a session with username if username, primary, and secondary all equal each other ', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual( + 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123process-name-1(123)arg1arg2arg3process-title-1with resultsuccess' + ); + }); + + test('it returns a session with username if primary and secondary equal unset', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual( + 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123process-name-1(123)arg1arg2arg3process-title-1with resultsuccess' + ); + }); + + test('it returns a session with username if primary and secondary equal unset with different casing', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual( + 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123process-name-1(123)arg1arg2arg3process-title-1with resultsuccess' + ); + }); + + test('it returns a session with username if primary and secondary are undefined', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual( + 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123process-name-1(123)arg1arg2arg3process-title-1with resultsuccess' + ); + }); + + test('it returns a session with "as" wording if username, primary, and secondary are all different', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual( + 'Sessionsession-1[username-2]as[username-3]@host-1inworking-directory-1generic-text-123process-name-1(123)arg1arg2arg3process-title-1with resultsuccess' + ); + }); + + test('it returns a session with "as" wording if username and primary are the same but secondary is different', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual( + 'Sessionsession-1[username-1]as[username-2]@host-1inworking-directory-1generic-text-123process-name-1(123)arg1arg2arg3process-title-1with resultsuccess' + ); + }); + + test('it returns a session with primary if username and secondary are unset with different casing', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual( + 'Sessionsession-1[username-primary]@host-1inworking-directory-1generic-text-123process-name-1(123)arg1arg2arg3process-title-1with resultsuccess' + ); + }); + + test('it returns a session with primary if username and secondary are undefined', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual( + 'Sessionsession-1[username-primary]@host-1inworking-directory-1generic-text-123process-name-1(123)arg1arg2arg3process-title-1with resultsuccess' + ); + }); + + test('it returns just a session if only given an id', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual('Session'); + }); + + test('it returns only session and hostName if only hostname and an id is given', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual('Session@some-host-name'); + }); + + test('it returns only a session and user name if only a user name and id is given', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual('Sessionsome-user-name'); + }); + + test('it returns only a process name if only given a process name and id', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual('Sessiongeneric-text-123some-process-name'); + }); + + test('it returns session, user name, and process title if process title with id is given', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual('Sessionsome-user-namesome-process-title'); + }); + + test('it returns only a working directory if that is all that is given with a id', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual('Sessioninsome-working-directory'); + }); + + test('it returns only the session and args with id if that is all that is given (very unlikely situation)', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual('Sessionarg1arg2arg 3'); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/generic_details.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/generic_details.tsx new file mode 100644 index 00000000000000..1e82519285da38 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/generic_details.tsx @@ -0,0 +1,159 @@ +/* + * 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 { EuiFlexGroup, EuiSpacer } from '@elastic/eui'; +import { get } from 'lodash/fp'; +import React from 'react'; + +import { BrowserFields } from '../../../../../../common/containers/source'; +import { Ecs } from '../../../../../../graphql/types'; +import { DraggableBadge } from '../../../../../../common/components/draggables'; + +import * as i18n from './translations'; +import { NetflowRenderer } from '../netflow'; +import { TokensFlexItem, Details } from '../helpers'; +import { ProcessDraggable } from '../process_draggable'; +import { Args } from '../args'; +import { SessionUserHostWorkingDir } from './session_user_host_working_dir'; + +interface Props { + id: string; + hostName: string | null | undefined; + result: string | null | undefined; + userName: string | null | undefined; + primary: string | null | undefined; + contextId: string; + text: string; + secondary: string | null | undefined; + processName: string | null | undefined; + processPid: number | null | undefined; + processExecutable: string | null | undefined; + processTitle: string | null | undefined; + workingDirectory: string | null | undefined; + args: string[] | null | undefined; + session: string | null | undefined; +} + +export const AuditdGenericLine = React.memo( + ({ + id, + contextId, + hostName, + userName, + primary, + processName, + processPid, + processExecutable, + processTitle, + secondary, + workingDirectory, + args, + result, + session, + text, + }) => ( + + + {processExecutable != null && ( + + {text} + + )} + + + + + {result != null && ( + + {i18n.WITH_RESULT} + + )} + + + + + ) +); + +AuditdGenericLine.displayName = 'AuditdGenericLine'; + +interface GenericDetailsProps { + browserFields: BrowserFields; + data: Ecs; + contextId: string; + text: string; + timelineId: string; +} + +export const AuditdGenericDetails = React.memo( + ({ data, contextId, text, timelineId }) => { + const id = data._id; + const session: string | null | undefined = get('auditd.session[0]', data); + const hostName: string | null | undefined = get('host.name[0]', data); + const userName: string | null | undefined = get('user.name[0]', data); + const result: string | null | undefined = get('auditd.result[0]', data); + const processPid: number | null | undefined = get('process.pid[0]', data); + const processName: string | null | undefined = get('process.name[0]', data); + const processExecutable: string | null | undefined = get('process.executable[0]', data); + const processTitle: string | null | undefined = get('process.title[0]', data); + const workingDirectory: string | null | undefined = get('process.working_directory[0]', data); + const primary: string | null | undefined = get('auditd.summary.actor.primary[0]', data); + const secondary: string | null | undefined = get('auditd.summary.actor.secondary[0]', data); + const args: string[] | null | undefined = get('process.args', data); + if (data.process != null) { + return ( +
+ + + +
+ ); + } else { + return null; + } + } +); + +AuditdGenericDetails.displayName = 'AuditdGenericDetails'; diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/generic_file_details.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/generic_file_details.test.tsx new file mode 100644 index 00000000000000..0990280879a140 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/generic_file_details.test.tsx @@ -0,0 +1,520 @@ +/* + * 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 { BrowserFields } from '../../../../../../common/containers/source'; +import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; +import { mockTimelineData, TestProviders } from '../../../../../../common/mock'; +import { AuditdGenericFileDetails, AuditdGenericFileLine } from './generic_file_details'; +import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; + +describe('GenericFileDetails', () => { + const mount = useMountAppended(); + + describe('rendering', () => { + test('it renders the default GenericFileDetails', () => { + // I cannot and do not want to use BrowserFields for the mocks for the snapshot tests as they are too heavy + const browserFields: BrowserFields = {}; + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); + + test('it returns auditd if the data does contain auditd data', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual( + 'Sessionalice@zeek-sanfranin/generic-text-123usinggpgconf(5402)gpgconf--list-dirsagent-socketgpgconf --list-dirs agent-socket' + ); + }); + + test('it returns null for text if the data contains no auditd data', () => { + const wrapper = shallow( + + ); + expect(wrapper.isEmptyRender()).toBeTruthy(); + }); + }); + + describe('#AuditdGenericFileLine', () => { + test('it returns pretty output if you send in all your happy path data', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual( + 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1(123)arg1arg2arg3process-title-1with resultsuccess' + ); + }); + + test('it returns a session with username if username, primary, and secondary all equal each other ', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual( + 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1(123)arg1arg2arg3process-title-1with resultsuccess' + ); + }); + + test('it returns a session with username if primary and secondary equal unset', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual( + 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1(123)arg1arg2arg3process-title-1with resultsuccess' + ); + }); + + test('it returns a session with username if primary and secondary equal unset with different casing', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual( + 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1(123)arg1arg2arg3process-title-1with resultsuccess' + ); + }); + + test('it returns a session with username if primary and secondary are undefined', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual( + 'Sessionsession-1username-1@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1(123)arg1arg2arg3process-title-1with resultsuccess' + ); + }); + + test('it returns a session with "as" wording if username, primary, and secondary are all different', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual( + 'Sessionsession-1[username-2]as[username-3]@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1(123)arg1arg2arg3process-title-1with resultsuccess' + ); + }); + + test('it returns a session with "as" wording if username and primary are the same but secondary is different', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual( + 'Sessionsession-1[username-1]as[username-2]@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1(123)arg1arg2arg3process-title-1with resultsuccess' + ); + }); + + test('it returns a session with primary if username and secondary are unset with different casing', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual( + 'Sessionsession-1[username-primary]@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1(123)arg1arg2arg3process-title-1with resultsuccess' + ); + }); + + test('it returns a session with primary if username and secondary are undefined', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual( + 'Sessionsession-1[username-primary]@host-1inworking-directory-1generic-text-123/somepathusingprocess-name-1(123)arg1arg2arg3process-title-1with resultsuccess' + ); + }); + + test('it returns just session if only session id is given', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual('Session'); + }); + + test('it returns only session and hostName if only hostname and an id is given', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual('Session@some-host-name'); + }); + + test('it returns only a session and user name if only a user name and id is given', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual('Sessionsome-user-name'); + }); + + test('it returns only a process name if only given a process name and id', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual('Sessiongeneric-text-123usingsome-process-name'); + }); + + test('it returns session user name and title if process title with id is given', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual('Sessionsome-user-namesome-process-title'); + }); + + test('it returns only a working directory if that is all that is given with a id', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual('Sessioninsome-working-directory'); + }); + + test('it returns only the session and args with id if that is all that is given (very unlikely situation)', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual('Sessionarg1arg2arg 3'); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/generic_file_details.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/generic_file_details.tsx new file mode 100644 index 00000000000000..d9149bae891903 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/generic_file_details.tsx @@ -0,0 +1,182 @@ +/* + * 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 { EuiFlexGroup, EuiSpacer, IconType } from '@elastic/eui'; +import { get } from 'lodash/fp'; +import React from 'react'; + +import { BrowserFields } from '../../../../../../common/containers/source'; +import { Ecs } from '../../../../../../graphql/types'; +import { DraggableBadge } from '../../../../../../common/components/draggables'; + +import * as i18n from './translations'; +import { NetflowRenderer } from '../netflow'; +import { TokensFlexItem, Details } from '../helpers'; +import { ProcessDraggable } from '../process_draggable'; +import { Args } from '../args'; +import { SessionUserHostWorkingDir } from './session_user_host_working_dir'; + +interface Props { + id: string; + hostName: string | null | undefined; + userName: string | null | undefined; + result: string | null | undefined; + primary: string | null | undefined; + fileIcon: IconType; + contextId: string; + text: string; + secondary: string | null | undefined; + filePath: string | null | undefined; + processName: string | null | undefined; + processPid: number | null | undefined; + processExecutable: string | null | undefined; + processTitle: string | null | undefined; + workingDirectory: string | null | undefined; + args: string[] | null | undefined; + session: string | null | undefined; +} + +export const AuditdGenericFileLine = React.memo( + ({ + id, + contextId, + hostName, + userName, + result, + primary, + secondary, + filePath, + processName, + processPid, + processExecutable, + processTitle, + workingDirectory, + args, + session, + text, + fileIcon, + }) => ( + + + {(filePath != null || processExecutable != null) && ( + + {text} + + )} + + + + {processExecutable != null && ( + + {i18n.USING} + + )} + + + + + {result != null && ( + + {i18n.WITH_RESULT} + + )} + + + + + ) +); + +AuditdGenericFileLine.displayName = 'AuditdGenericFileLine'; + +interface GenericDetailsProps { + browserFields: BrowserFields; + data: Ecs; + contextId: string; + text: string; + fileIcon: IconType; + timelineId: string; +} + +export const AuditdGenericFileDetails = React.memo( + ({ data, contextId, text, fileIcon = 'document', timelineId }) => { + const id = data._id; + const session: string | null | undefined = get('auditd.session[0]', data); + const hostName: string | null | undefined = get('host.name[0]', data); + const userName: string | null | undefined = get('user.name[0]', data); + const result: string | null | undefined = get('auditd.result[0]', data); + const processPid: number | null | undefined = get('process.pid[0]', data); + const processName: string | null | undefined = get('process.name[0]', data); + const processExecutable: string | null | undefined = get('process.executable[0]', data); + const processTitle: string | null | undefined = get('process.title[0]', data); + const workingDirectory: string | null | undefined = get('process.working_directory[0]', data); + const filePath: string | null | undefined = get('file.path[0]', data); + const primary: string | null | undefined = get('auditd.summary.actor.primary[0]', data); + const secondary: string | null | undefined = get('auditd.summary.actor.secondary[0]', data); + const args: string[] | null | undefined = get('process.args', data); + + if (data.process != null) { + return ( +
+ + + +
+ ); + } else { + return null; + } + } +); + +AuditdGenericFileDetails.displayName = 'AuditdGenericFileDetails'; diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx new file mode 100644 index 00000000000000..ae5e7e2ef789b7 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.test.tsx @@ -0,0 +1,148 @@ +/* + * 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 { cloneDeep } from 'lodash/fp'; +import React from 'react'; + +import { BrowserFields } from '../../../../../../common/containers/source'; +import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; +import { Ecs } from '../../../../../../graphql/types'; +import { mockTimelineData, TestProviders } from '../../../../../../common/mock'; +import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; +import { RowRenderer } from '../row_renderer'; +import { + createGenericAuditRowRenderer, + createGenericFileRowRenderer, +} from './generic_row_renderer'; + +jest.mock('../../../../../../overview/components/events_by_dataset'); + +describe('GenericRowRenderer', () => { + const mount = useMountAppended(); + + describe('#createGenericAuditRowRenderer', () => { + let nonAuditd: Ecs; + let auditd: Ecs; + let connectedToRenderer: RowRenderer; + beforeEach(() => { + nonAuditd = cloneDeep(mockTimelineData[0].ecs); + auditd = cloneDeep(mockTimelineData[26].ecs); + connectedToRenderer = createGenericAuditRowRenderer({ + actionName: 'connected-to', + text: 'some text', + }); + }); + test('renders correctly against snapshot', () => { + // I cannot and do not want to use BrowserFields mocks for the snapshot tests as they are too heavy + const browserFields: BrowserFields = {}; + const children = connectedToRenderer.renderRow({ + browserFields, + data: auditd, + timelineId: 'test', + }); + + const wrapper = shallow({children}); + expect(wrapper).toMatchSnapshot(); + }); + + test('should return false if not a auditd datum', () => { + expect(connectedToRenderer.isInstance(nonAuditd)).toBe(false); + }); + + test('should return true if it is a auditd datum', () => { + expect(connectedToRenderer.isInstance(auditd)).toBe(true); + }); + + test('should return false when action is set to some other value', () => { + if (auditd.event != null && auditd.event.action != null) { + auditd.event.action[0] = 'some other value'; + expect(connectedToRenderer.isInstance(auditd)).toBe(false); + } else { + // will fail and give you an error if either is not defined as a mock + expect(auditd.event).toBeDefined(); + } + }); + + test('should render a auditd row', () => { + const children = connectedToRenderer.renderRow({ + browserFields: mockBrowserFields, + data: auditd, + timelineId: 'test', + }); + const wrapper = mount( + + {children} + + ); + expect(wrapper.text()).toContain( + 'Session246alice@zeek-londonsome textwget(1490)wget www.example.comwith resultsuccessDestination93.184.216.34:80' + ); + }); + }); + + describe('#createGenericFileRowRenderer', () => { + let nonAuditd: Ecs; + let auditdFile: Ecs; + let fileToRenderer: RowRenderer; + + beforeEach(() => { + nonAuditd = cloneDeep(mockTimelineData[0].ecs); + auditdFile = cloneDeep(mockTimelineData[27].ecs); + fileToRenderer = createGenericFileRowRenderer({ + actionName: 'opened-file', + text: 'some text', + }); + }); + + test('renders correctly against snapshot', () => { + // I cannot and do not want to use BrowserFields mocks for the snapshot tests as they are too heavy + const browserFields: BrowserFields = {}; + const children = fileToRenderer.renderRow({ + browserFields, + data: auditdFile, + timelineId: 'test', + }); + + const wrapper = shallow({children}); + expect(wrapper).toMatchSnapshot(); + }); + + test('should return false if not a auditd datum', () => { + expect(fileToRenderer.isInstance(nonAuditd)).toBe(false); + }); + + test('should return true if it is a auditd datum', () => { + expect(fileToRenderer.isInstance(auditdFile)).toBe(true); + }); + + test('should return false when action is set to some other value', () => { + if (auditdFile.event != null && auditdFile.event.action != null) { + auditdFile.event.action[0] = 'some other value'; + expect(fileToRenderer.isInstance(auditdFile)).toBe(false); + } else { + // will fail and give you an error if either is not defined as a mock + expect(auditdFile.event).toBeDefined(); + } + }); + + test('should render a auditd row', () => { + const children = fileToRenderer.renderRow({ + browserFields: mockBrowserFields, + data: auditdFile, + timelineId: 'test', + }); + const wrapper = mount( + + {children} + + ); + expect(wrapper.text()).toContain( + 'Sessionunsetroot@zeek-londonin/some text/proc/15990/attr/currentusingsystemd-journal(27244)/lib/systemd/systemd-journaldwith resultsuccess' + ); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/generic_row_renderer.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/generic_row_renderer.tsx diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx similarity index 97% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx index 598769e854b422..41e35427ae2542 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/primary_secondary_user_info.test.tsx @@ -7,9 +7,9 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { TestProviders } from '../../../../../mock'; +import { TestProviders } from '../../../../../../common/mock'; import { PrimarySecondaryUserInfo, nilOrUnSet } from './primary_secondary_user_info'; -import { useMountAppended } from '../../../../../utils/use_mount_appended'; +import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; describe('UserPrimarySecondary', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/primary_secondary_user_info.tsx similarity index 97% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/primary_secondary_user_info.tsx index a54042d3de9d87..8c9191181d93b5 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/primary_secondary_user_info.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/primary_secondary_user_info.tsx @@ -7,7 +7,7 @@ import { EuiFlexGroup } from '@elastic/eui'; import React from 'react'; -import { DraggableBadge } from '../../../../draggables'; +import { DraggableBadge } from '../../../../../../common/components/draggables'; import * as i18n from './translations'; import { TokensFlexItem } from '../helpers'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/session_user_host_working_dir.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/session_user_host_working_dir.test.tsx similarity index 97% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/session_user_host_working_dir.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/session_user_host_working_dir.test.tsx index a0a9977f5765e3..d1e67c25bd79c5 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/session_user_host_working_dir.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/session_user_host_working_dir.test.tsx @@ -8,9 +8,9 @@ import { EuiFlexItem } from '@elastic/eui'; import { shallow } from 'enzyme'; import React from 'react'; -import { TestProviders } from '../../../../../mock'; +import { TestProviders } from '../../../../../../common/mock'; import { SessionUserHostWorkingDir } from './session_user_host_working_dir'; -import { useMountAppended } from '../../../../../utils/use_mount_appended'; +import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; describe('SessionUserHostWorkingDir', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/session_user_host_working_dir.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/session_user_host_working_dir.tsx similarity index 96% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/session_user_host_working_dir.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/session_user_host_working_dir.tsx index 6a6b55bb817c84..fb2fd7a4b04b05 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/session_user_host_working_dir.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/session_user_host_working_dir.tsx @@ -6,7 +6,7 @@ import React from 'react'; -import { DraggableBadge } from '../../../../draggables'; +import { DraggableBadge } from '../../../../../../common/components/draggables'; import * as i18n from './translations'; import { TokensFlexItem } from '../helpers'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/translations.ts b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/auditd/translations.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/auditd/translations.ts diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/bytes/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/bytes/index.test.tsx new file mode 100644 index 00000000000000..06f392683cbf17 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/bytes/index.test.tsx @@ -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 React from 'react'; + +import { TestProviders } from '../../../../../../common/mock'; +import { PreferenceFormattedBytes } from '../../../../../../common/components/formatted_bytes'; +import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; + +import { Bytes } from '.'; + +describe('Bytes', () => { + const mount = useMountAppended(); + + test('it renders the expected formatted bytes', () => { + const wrapper = mount( + + + + ); + expect( + wrapper + .find(PreferenceFormattedBytes) + .first() + .text() + ).toEqual('1.2MB'); + }); +}); diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/bytes/index.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/bytes/index.tsx new file mode 100644 index 00000000000000..a8dfe939d28dd8 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/bytes/index.tsx @@ -0,0 +1,35 @@ +/* + * 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 { DefaultDraggable } from '../../../../../../common/components/draggables'; +import { PreferenceFormattedBytes } from '../../../../../../common/components/formatted_bytes'; + +export const BYTES_FORMAT = 'bytes'; + +/** + * Renders draggable text containing the value of a field representing a + * duration of time, (e.g. `event.duration`) + */ +export const Bytes = React.memo<{ + contextId: string; + eventId: string; + fieldName: string; + value?: string | null; +}>(({ contextId, eventId, fieldName, value }) => ( + + + +)); + +Bytes.displayName = 'Bytes'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/column_renderer.ts b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/column_renderer.ts similarity index 82% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/column_renderer.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/column_renderer.ts index a13de90e7aed3d..4a89fea8c51067 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/column_renderer.ts +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/column_renderer.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TimelineNonEcsData } from '../../../../graphql/types'; -import { ColumnHeaderOptions } from '../../../../store/timeline/model'; +import { TimelineNonEcsData } from '../../../../../graphql/types'; +import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; export interface ColumnRenderer { isInstance: (columnName: string, data: TimelineNonEcsData[]) => boolean; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/constants.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/constants.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/constants.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/constants.tsx diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx similarity index 82% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx index a7c9d10e82a2fd..ba77709459c28e 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details.test.tsx @@ -11,10 +11,10 @@ import React from 'react'; -import { TestProviders } from '../../../../../mock'; -import { mockBrowserFields } from '../../../../../../public/containers/source/mock'; -import { mockEndgameDnsRequest } from '../../../../../../public/mock/mock_endgame_ecs_data'; -import { useMountAppended } from '../../../../../utils/use_mount_appended'; +import { TestProviders } from '../../../../../../common/mock'; +import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; +import { mockEndgameDnsRequest } from '../../../../../../common/mock/mock_endgame_ecs_data'; +import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; import { DnsRequestEventDetails } from './dns_request_event_details'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details.tsx similarity index 94% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details.tsx index 824e8c00de307a..74ed5b2a6587fa 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details.tsx @@ -8,9 +8,9 @@ import { EuiSpacer } from '@elastic/eui'; import { get } from 'lodash/fp'; import React from 'react'; -import { BrowserFields } from '../../../../../containers/source'; +import { BrowserFields } from '../../../../../../common/containers/source'; import { Details } from '../helpers'; -import { Ecs } from '../../../../../graphql/types'; +import { Ecs } from '../../../../../../graphql/types'; import { NetflowRenderer } from '../netflow'; import { DnsRequestEventDetailsLine } from './dns_request_event_details_line'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx similarity index 98% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx index e12eacd73559de..1d46e4c3eb02d7 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details_line.test.tsx @@ -11,10 +11,10 @@ import React from 'react'; -import { TestProviders } from '../../../../../mock'; +import { TestProviders } from '../../../../../../common/mock'; import { DnsRequestEventDetailsLine } from './dns_request_event_details_line'; -import { useMountAppended } from '../../../../../utils/use_mount_appended'; +import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; describe('DnsRequestEventDetailsLine', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details_line.tsx similarity index 98% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details_line.tsx index c7a08620bebbbe..eafe64f13c25cf 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/dns/dns_request_event_details_line.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/dns/dns_request_event_details_line.tsx @@ -7,7 +7,7 @@ import { EuiFlexGroup } from '@elastic/eui'; import React from 'react'; -import { DraggableBadge } from '../../../../draggables'; +import { DraggableBadge } from '../../../../../../common/components/draggables'; import { isNillEmptyOrNotFinite, TokensFlexItem } from '../helpers'; import { ProcessDraggableWithNonExistentProcess } from '../process_draggable'; import { UserHostWorkingDir } from '../user_host_working_dir'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/dns/translations.ts b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/dns/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/dns/translations.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/dns/translations.ts diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/empty_column_renderer.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/empty_column_renderer.test.tsx similarity index 91% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/empty_column_renderer.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/empty_column_renderer.test.tsx index b31d01b8e94a0f..4514ce5e9bb069 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/empty_column_renderer.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/empty_column_renderer.test.tsx @@ -8,10 +8,10 @@ import { shallow } from 'enzyme'; import { cloneDeep } from 'lodash/fp'; import React from 'react'; -import { TimelineNonEcsData } from '../../../../graphql/types'; -import { defaultHeaders, mockTimelineData, TestProviders } from '../../../../mock'; -import { useMountAppended } from '../../../../utils/use_mount_appended'; -import { getEmptyValue } from '../../../empty_value'; +import { TimelineNonEcsData } from '../../../../../graphql/types'; +import { defaultHeaders, mockTimelineData, TestProviders } from '../../../../../common/mock'; +import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; +import { getEmptyValue } from '../../../../../common/components/empty_value'; import { deleteItemIdx, findItem } from './helpers'; import { emptyColumnRenderer } from './empty_column_renderer'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/empty_column_renderer.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/empty_column_renderer.tsx similarity index 81% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/empty_column_renderer.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/empty_column_renderer.tsx index 45ef46616718d1..9769e23b57aff4 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/empty_column_renderer.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/empty_column_renderer.tsx @@ -8,11 +8,14 @@ import React from 'react'; -import { TimelineNonEcsData } from '../../../../graphql/types'; -import { ColumnHeaderOptions } from '../../../../store/timeline/model'; -import { DraggableWrapper, DragEffects } from '../../../drag_and_drop/draggable_wrapper'; -import { escapeDataProviderId } from '../../../drag_and_drop/helpers'; -import { getEmptyValue } from '../../../empty_value'; +import { TimelineNonEcsData } from '../../../../../graphql/types'; +import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; +import { + DraggableWrapper, + DragEffects, +} from '../../../../../common/components/drag_and_drop/draggable_wrapper'; +import { escapeDataProviderId } from '../../../../../common/components/drag_and_drop/helpers'; +import { getEmptyValue } from '../../../../../common/components/empty_value'; import { EXISTS_OPERATOR } from '../../data_providers/data_provider'; import { Provider } from '../../data_providers/provider'; import { ColumnRenderer } from './column_renderer'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx similarity index 92% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx index 72b879d4ade78b..e84cb93b871781 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details.test.tsx @@ -11,15 +11,15 @@ import React from 'react'; -import { TestProviders } from '../../../../../mock'; -import { mockBrowserFields } from '../../../../../../public/containers/source/mock'; +import { TestProviders } from '../../../../../../common/mock'; +import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; import { mockEndgameAdminLogon, mockEndgameExplicitUserLogon, mockEndgameUserLogon, mockEndgameUserLogoff, -} from '../../../../../../public/mock/mock_endgame_ecs_data'; -import { useMountAppended } from '../../../../../utils/use_mount_appended'; +} from '../../../../../../common/mock/mock_endgame_ecs_data'; +import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; import { EndgameSecurityEventDetails } from './endgame_security_event_details'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details.tsx similarity index 96% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details.tsx index 35a88f52f05a3a..11580e2536ff7e 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details.tsx @@ -8,8 +8,8 @@ import { EuiSpacer } from '@elastic/eui'; import { get } from 'lodash/fp'; import React from 'react'; -import { BrowserFields } from '../../../../../containers/source'; -import { Ecs } from '../../../../../graphql/types'; +import { BrowserFields } from '../../../../../../common/containers/source'; +import { Ecs } from '../../../../../../graphql/types'; import { NetflowRenderer } from '../netflow'; import { EndgameSecurityEventDetailsLine } from './endgame_security_event_details_line'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx similarity index 99% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx index 4e522f6ed5c94f..b2b4b021e5db53 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.test.tsx @@ -11,10 +11,10 @@ import React from 'react'; -import { TestProviders } from '../../../../../mock'; +import { TestProviders } from '../../../../../../common/mock'; import { EndgameSecurityEventDetailsLine } from './endgame_security_event_details_line'; -import { useMountAppended } from '../../../../../utils/use_mount_appended'; +import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; describe('EndgameSecurityEventDetailsLine', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.tsx similarity index 99% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.tsx index c2c42ba0e4ddcf..c2bccc24fd9945 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/endgame_security_event_details_line.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/endgame/endgame_security_event_details_line.tsx @@ -7,7 +7,7 @@ import { EuiFlexGroup } from '@elastic/eui'; import React from 'react'; -import { DraggableBadge } from '../../../../draggables'; +import { DraggableBadge } from '../../../../../../common/components/draggables'; import { isNillEmptyOrNotFinite, TokensFlexItem } from '../helpers'; import { ProcessDraggableWithNonExistentProcess } from '../process_draggable'; import { UserHostWorkingDir } from '../user_host_working_dir'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/helpers.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/endgame/helpers.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/helpers.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/endgame/helpers.test.tsx diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/helpers.ts b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/endgame/helpers.ts similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/helpers.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/endgame/helpers.ts diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/translations.ts b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/endgame/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/endgame/translations.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/endgame/translations.ts diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/exit_code_draggable.test.tsx similarity index 95% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/exit_code_draggable.test.tsx index 4da236bfa34c30..4471c26ef8fd7b 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/exit_code_draggable.test.tsx @@ -6,8 +6,8 @@ import React from 'react'; -import { TestProviders } from '../../../../mock'; -import { useMountAppended } from '../../../../utils/use_mount_appended'; +import { TestProviders } from '../../../../../common/mock'; +import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; import { ExitCodeDraggable } from './exit_code_draggable'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/exit_code_draggable.tsx similarity index 94% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/exit_code_draggable.tsx index 7671e3f0509a53..8aba73f5373e91 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/exit_code_draggable.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/exit_code_draggable.tsx @@ -6,7 +6,7 @@ import React from 'react'; -import { DraggableBadge } from '../../../draggables'; +import { DraggableBadge } from '../../../../../common/components/draggables'; import { isNillEmptyOrNotFinite, TokensFlexItem } from './helpers'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/file_draggable.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/file_draggable.test.tsx similarity index 95% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/file_draggable.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/file_draggable.test.tsx index d800821f8d8a5f..70e0e74675cd2f 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/file_draggable.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/file_draggable.test.tsx @@ -6,10 +6,10 @@ import React from 'react'; -import { TestProviders } from '../../../../mock'; +import { TestProviders } from '../../../../../common/mock'; import { FileDraggable } from './file_draggable'; -import { useMountAppended } from '../../../../utils/use_mount_appended'; +import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; describe('FileDraggable', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/file_draggable.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/file_draggable.tsx similarity index 97% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/file_draggable.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/file_draggable.tsx index e4871c6479c6be..bdf223d215a1cc 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/file_draggable.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/file_draggable.tsx @@ -6,7 +6,7 @@ import React from 'react'; -import { DraggableBadge } from '../../../draggables'; +import { DraggableBadge } from '../../../../../common/components/draggables'; import { isNillEmptyOrNotFinite, TokensFlexItem } from './helpers'; import * as i18n from './translations'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/formatted_field.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/formatted_field.test.tsx similarity index 96% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/formatted_field.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/formatted_field.test.tsx index 73f7b004ca3f71..64f4656e7e790a 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/formatted_field.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/formatted_field.test.tsx @@ -8,14 +8,14 @@ import { shallow } from 'enzyme'; import { get } from 'lodash/fp'; import React from 'react'; -import { mockTimelineData, TestProviders } from '../../../../mock'; -import { getEmptyValue } from '../../../empty_value'; -import { useMountAppended } from '../../../../utils/use_mount_appended'; +import { mockTimelineData, TestProviders } from '../../../../../common/mock'; +import { getEmptyValue } from '../../../../../common/components/empty_value'; +import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; import { FormattedFieldValue } from './formatted_field'; import { HOST_NAME_FIELD_NAME } from './constants'; -jest.mock('../../../../lib/kibana'); +jest.mock('../../../../../common/lib/kibana'); describe('Events', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/formatted_field.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/formatted_field.tsx similarity index 88% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/formatted_field.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/formatted_field.tsx index 0f650d6386194b..d03f0573dc2b0a 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/formatted_field.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/formatted_field.tsx @@ -8,16 +8,19 @@ import { EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { isNumber, isString, isEmpty } from 'lodash/fp'; import React from 'react'; -import { DefaultDraggable } from '../../../draggables'; -import { Bytes, BYTES_FORMAT } from '../../../bytes'; +import { DefaultDraggable } from '../../../../../common/components/draggables'; +import { Bytes, BYTES_FORMAT } from './bytes'; import { Duration, EVENT_DURATION_FIELD_NAME } from '../../../duration'; -import { getOrEmptyTagFromValue, getEmptyTagValue } from '../../../empty_value'; -import { FormattedDate } from '../../../formatted_date'; -import { FormattedIp } from '../../../formatted_ip'; -import { HostDetailsLink } from '../../../links'; +import { + getOrEmptyTagFromValue, + getEmptyTagValue, +} from '../../../../../common/components/empty_value'; +import { FormattedDate } from '../../../../../common/components/formatted_date'; +import { FormattedIp } from '../../../../components/formatted_ip'; +import { HostDetailsLink } from '../../../../../common/components/links'; -import { Port, PORT_NAMES } from '../../../port'; -import { TruncatableText } from '../../../truncatable_text'; +import { Port, PORT_NAMES } from '../../../../../network/components/port'; +import { TruncatableText } from '../../../../../common/components/truncatable_text'; import { DATE_FIELD_TYPE, HOST_NAME_FIELD_NAME, diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/formatted_field_helpers.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/formatted_field_helpers.tsx similarity index 89% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/formatted_field_helpers.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/formatted_field_helpers.tsx index 7c9accd4cef49e..bbf8c5af3be970 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/formatted_field_helpers.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/formatted_field_helpers.tsx @@ -9,13 +9,13 @@ import { isString, isEmpty } from 'lodash/fp'; import React from 'react'; import styled from 'styled-components'; -import { DefaultDraggable } from '../../../draggables'; -import { getEmptyTagValue } from '../../../empty_value'; -import { getRuleDetailsUrl } from '../../../link_to/redirect_to_detection_engine'; -import { TruncatableText } from '../../../truncatable_text'; +import { DefaultDraggable } from '../../../../../common/components/draggables'; +import { getEmptyTagValue } from '../../../../../common/components/empty_value'; +import { getRuleDetailsUrl } from '../../../../../common/components/link_to/redirect_to_detection_engine'; +import { TruncatableText } from '../../../../../common/components/truncatable_text'; -import { isUrlInvalid } from '../../../../pages/detection_engine/rules/components/step_about_rule/helpers'; -import endPointSvg from '../../../../utils/logo_endpoint/64_color.svg'; +import { isUrlInvalid } from '../../../../../alerts/components/rules/step_about_rule/helpers'; +import endPointSvg from '../../../../../common/utils/logo_endpoint/64_color.svg'; import * as i18n from './translations'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/get_column_renderer.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/get_column_renderer.test.tsx similarity index 89% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/get_column_renderer.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/get_column_renderer.test.tsx index 25d5c71caf48ab..12b093bd517c8c 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/get_column_renderer.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/get_column_renderer.test.tsx @@ -8,16 +8,16 @@ import { shallow } from 'enzyme'; import { cloneDeep } from 'lodash/fp'; import React from 'react'; -import { TimelineNonEcsData } from '../../../../graphql/types'; -import { mockTimelineData } from '../../../../mock'; -import { TestProviders } from '../../../../mock/test_providers'; -import { getEmptyValue } from '../../../empty_value'; +import { TimelineNonEcsData } from '../../../../../graphql/types'; +import { mockTimelineData } from '../../../../../common/mock'; +import { TestProviders } from '../../../../../common/mock/test_providers'; +import { getEmptyValue } from '../../../../../common/components/empty_value'; import { defaultHeaders } from '../column_headers/default_headers'; import { columnRenderers } from '.'; import { getColumnRenderer } from './get_column_renderer'; import { getValues, findItem, deleteItemIdx } from './helpers'; -import { useMountAppended } from '../../../../utils/use_mount_appended'; +import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; describe('get_column_renderer', () => { let nonSuricata: TimelineNonEcsData[]; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/get_column_renderer.ts b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/get_column_renderer.ts similarity index 91% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/get_column_renderer.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/get_column_renderer.ts index 22aa14d598c135..03d041aef1e702 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/get_column_renderer.ts +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/get_column_renderer.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TimelineNonEcsData } from '../../../../graphql/types'; +import { TimelineNonEcsData } from '../../../../../graphql/types'; import { ColumnRenderer } from './column_renderer'; const unhandledColumnRenderer = (): never => { diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx similarity index 93% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx index 7ad8cfed5256ba..3222f8a2362db8 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/get_row_renderer.test.tsx @@ -8,11 +8,11 @@ import { shallow } from 'enzyme'; import { cloneDeep } from 'lodash'; import React from 'react'; -import { mockBrowserFields } from '../../../../containers/source/mock'; -import { Ecs } from '../../../../graphql/types'; -import { mockTimelineData } from '../../../../mock'; -import { TestProviders } from '../../../../mock/test_providers'; -import { useMountAppended } from '../../../../utils/use_mount_appended'; +import { mockBrowserFields } from '../../../../../common/containers/source/mock'; +import { Ecs } from '../../../../../graphql/types'; +import { mockTimelineData } from '../../../../../common/mock'; +import { TestProviders } from '../../../../../common/mock/test_providers'; +import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; import { rowRenderers } from '.'; import { getRowRenderer } from './get_row_renderer'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.ts b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/get_row_renderer.ts similarity index 92% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/get_row_renderer.ts index b5a585d4638198..2e90c589e65329 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/get_row_renderer.ts +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/get_row_renderer.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Ecs } from '../../../../graphql/types'; +import { Ecs } from '../../../../../graphql/types'; import { RowRenderer } from './row_renderer'; const unhandledRowRenderer = (): never => { diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/helpers.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/helpers.test.tsx new file mode 100644 index 00000000000000..82704d544b8b9f --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/helpers.test.tsx @@ -0,0 +1,284 @@ +/* + * 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 { cloneDeep } from 'lodash/fp'; + +import { TimelineNonEcsData } from '../../../../../graphql/types'; +import { mockTimelineData } from '../../../../../common/mock'; +import { + deleteItemIdx, + findItem, + getValues, + isFileEvent, + isNillEmptyOrNotFinite, + isProcessStoppedOrTerminationEvent, + showVia, +} from './helpers'; + +describe('helpers', () => { + describe('#deleteItemIdx', () => { + let mockDatum: TimelineNonEcsData[]; + beforeEach(() => { + mockDatum = cloneDeep(mockTimelineData[0].data); + }); + + test('should delete part of a value value', () => { + const deleted = deleteItemIdx(mockDatum, 1); + const expected: TimelineNonEcsData[] = [ + { field: '@timestamp', value: ['2018-11-05T19:03:25.937Z'] }, + // { field: 'event.category', value: ['Access'] <-- deleted entry + { field: 'event.category', value: ['Access'] }, + { field: 'event.action', value: ['Action'] }, + { field: 'host.name', value: ['apache'] }, + { field: 'source.ip', value: ['192.168.0.1'] }, + { field: 'destination.ip', value: ['192.168.0.3'] }, + { field: 'destination.bytes', value: ['123456'] }, + { field: 'user.name', value: ['john.dee'] }, + ]; + expect(deleted).toEqual(expected); + }); + + test('should not delete any part of the value, when the value when out of bounds', () => { + const deleted = deleteItemIdx(mockDatum, 1000); + const expected: TimelineNonEcsData[] = [ + { field: '@timestamp', value: ['2018-11-05T19:03:25.937Z'] }, + { field: 'event.severity', value: ['3'] }, + { field: 'event.category', value: ['Access'] }, + { field: 'event.action', value: ['Action'] }, + { field: 'host.name', value: ['apache'] }, + { field: 'source.ip', value: ['192.168.0.1'] }, + { field: 'destination.ip', value: ['192.168.0.3'] }, + { field: 'destination.bytes', value: ['123456'] }, + { field: 'user.name', value: ['john.dee'] }, + ]; + expect(deleted).toEqual(expected); + }); + }); + + describe('#findItem', () => { + let mockDatum: TimelineNonEcsData[]; + beforeEach(() => { + mockDatum = cloneDeep(mockTimelineData[0].data); + }); + test('should find an index with non-zero', () => { + expect(findItem(mockDatum, 'event.severity')).toEqual(1); + }); + + test('should return -1 with a field not found', () => { + expect(findItem(mockDatum, 'event.made-up')).toEqual(-1); + }); + }); + + describe('#getValues', () => { + let mockDatum: TimelineNonEcsData[]; + beforeEach(() => { + mockDatum = cloneDeep(mockTimelineData[0].data); + }); + + test('should return a valid value', () => { + expect(getValues('event.severity', mockDatum)).toEqual(['3']); + }); + + test('should return undefined when the value is not found', () => { + expect(getValues('event.made-up-value', mockDatum)).toBeUndefined(); + }); + + test('should return an undefined when the value found is null', () => { + const nullValue: TimelineNonEcsData[] = [ + { field: '@timestamp', value: ['2018-11-05T19:03:25.937Z'] }, + { field: 'event.severity', value: ['3'] }, + { field: 'event.category', value: ['Access'] }, + { field: 'event.action', value: ['Action'] }, + { field: 'host.name', value: ['apache'] }, + { field: 'source.ip', value: ['192.168.0.1'] }, + { field: 'destination.ip', value: ['192.168.0.3'] }, + { field: 'destination.bytes', value: ['123456'] }, + { field: 'user.name', value: null }, + ]; + expect(getValues('user.name', nullValue)).toBeUndefined(); + }); + + test('should return an undefined when the value found is undefined', () => { + const nullValue: TimelineNonEcsData[] = [ + { field: '@timestamp', value: ['2018-11-05T19:03:25.937Z'] }, + { field: 'event.severity', value: ['3'] }, + { field: 'event.category', value: ['Access'] }, + { field: 'event.action', value: ['Action'] }, + { field: 'host.name', value: ['apache'] }, + { field: 'source.ip', value: ['192.168.0.1'] }, + { field: 'destination.ip', value: ['192.168.0.3'] }, + { field: 'destination.bytes', value: ['123456'] }, + { field: 'user.name', value: undefined }, + ]; + expect(getValues('user.name', nullValue)).toBeUndefined(); + }); + + test('should return an undefined when the value is not present', () => { + const nullValue: TimelineNonEcsData[] = [ + { field: '@timestamp', value: ['2018-11-05T19:03:25.937Z'] }, + { field: 'event.severity', value: ['3'] }, + { field: 'event.category', value: ['Access'] }, + { field: 'event.action', value: ['Action'] }, + { field: 'host.name', value: ['apache'] }, + { field: 'source.ip', value: ['192.168.0.1'] }, + { field: 'destination.ip', value: ['192.168.0.3'] }, + { field: 'destination.bytes', value: ['123456'] }, + { field: 'user.name' }, + ]; + expect(getValues('user.name', nullValue)).toBeUndefined(); + }); + }); + + describe('#isNillEmptyOrNotFinite', () => { + test('undefined returns true', () => { + expect(isNillEmptyOrNotFinite(undefined)).toBe(true); + }); + + test('null returns true', () => { + expect(isNillEmptyOrNotFinite(null)).toBe(true); + }); + + test('empty string returns true', () => { + expect(isNillEmptyOrNotFinite('')).toBe(true); + }); + + test('empty array returns true', () => { + expect(isNillEmptyOrNotFinite([])).toBe(true); + }); + + test('NaN returns true', () => { + expect(isNillEmptyOrNotFinite(NaN)).toBe(true); + }); + + test('Infinity returns true', () => { + expect(isNillEmptyOrNotFinite(Infinity)).toBe(true); + }); + + test('a single space string returns false', () => { + expect(isNillEmptyOrNotFinite(' ')).toBe(false); + }); + + test('a simple string returns false', () => { + expect(isNillEmptyOrNotFinite('a simple string')).toBe(false); + }); + + test('the number 0 returns false', () => { + expect(isNillEmptyOrNotFinite(0)).toBe(false); + }); + + test('a non-empty array return false', () => { + expect(isNillEmptyOrNotFinite(['non empty array'])).toBe(false); + }); + }); + + describe('#showVia', () => { + test('undefined returns false', () => { + expect(showVia(undefined)).toBe(false); + }); + + test('null returns false', () => { + expect(showVia(null)).toBe(false); + }); + + test('empty string returns false', () => { + expect(showVia('')).toBe(false); + }); + + test('a random string returns false', () => { + expect(showVia('a random string')).toBe(false); + }); + + describe('valid values', () => { + const validValues = ['file_create_event', 'created', 'file_delete_event', 'deleted']; + + validValues.forEach(eventAction => { + test(`${eventAction} returns true`, () => { + expect(showVia(eventAction)).toBe(true); + }); + }); + + validValues.forEach(value => { + const upperCaseValue = value.toUpperCase(); + + test(`${upperCaseValue} (upper case) returns true`, () => { + expect(showVia(upperCaseValue)).toBe(true); + }); + }); + }); + }); + + describe('#isFileEvent', () => { + test('returns true when both eventCategory and eventDataset are file', () => { + expect(isFileEvent({ eventCategory: 'file', eventDataset: 'file' })).toBe(true); + }); + + test('returns false when eventCategory and eventDataset are undefined', () => { + expect(isFileEvent({ eventCategory: undefined, eventDataset: undefined })).toBe(false); + }); + + test('returns false when eventCategory and eventDataset are null', () => { + expect(isFileEvent({ eventCategory: null, eventDataset: null })).toBe(false); + }); + + test('returns false when eventCategory and eventDataset are random values', () => { + expect( + isFileEvent({ eventCategory: 'random category', eventDataset: 'random dataset' }) + ).toBe(false); + }); + + test('returns true when just eventCategory is file', () => { + expect(isFileEvent({ eventCategory: 'file', eventDataset: undefined })).toBe(true); + }); + + test('returns true when just eventDataset is file', () => { + expect(isFileEvent({ eventCategory: null, eventDataset: 'file' })).toBe(true); + }); + + test('returns true when just eventCategory is File with a capitol F', () => { + expect(isFileEvent({ eventCategory: 'File', eventDataset: '' })).toBe(true); + }); + + test('returns true when just eventDataset is File with a capitol F', () => { + expect(isFileEvent({ eventCategory: 'random', eventDataset: 'File' })).toBe(true); + }); + }); + + describe('#isProcessStoppedOrTerminationEvent', () => { + test('returns false when eventAction is undefined', () => { + expect(isProcessStoppedOrTerminationEvent(undefined)).toBe(false); + }); + + test('returns false when eventAction is null', () => { + expect(isProcessStoppedOrTerminationEvent(null)).toBe(false); + }); + + test('returns false when eventAction is an empty string', () => { + expect(isProcessStoppedOrTerminationEvent('')).toBe(false); + }); + + test('returns false when eventAction is a random value', () => { + expect(isProcessStoppedOrTerminationEvent('a random value')).toBe(false); + }); + + describe('valid values', () => { + const validValues = ['process_stopped', 'termination_event']; + + validValues.forEach(value => { + test(`returns true when eventAction is ${value}`, () => { + expect(isProcessStoppedOrTerminationEvent(value)).toBe(true); + }); + }); + + validValues.forEach(value => { + const upperCaseValue = value.toUpperCase(); + + test(`returns true when eventAction is (upper case) ${upperCaseValue}`, () => { + expect(isProcessStoppedOrTerminationEvent(upperCaseValue)).toBe(true); + }); + }); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/helpers.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/helpers.tsx new file mode 100644 index 00000000000000..7cda5aa3c59f78 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/helpers.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexItem } from '@elastic/eui'; +import { isNumber, isEmpty } from 'lodash/fp'; +import styled from 'styled-components'; + +import { TimelineNonEcsData } from '../../../../../graphql/types'; + +export const deleteItemIdx = (data: TimelineNonEcsData[], idx: number) => [ + ...data.slice(0, idx), + ...data.slice(idx + 1), +]; + +export const findItem = (data: TimelineNonEcsData[], field: string): number => + data.findIndex(d => d.field === field); + +export const getValues = (field: string, data: TimelineNonEcsData[]): string[] | undefined => { + const obj = data.find(d => d.field === field); + if (obj != null && obj.value != null) { + return obj.value; + } + return undefined; +}; + +export const Details = styled.div` + margin: 5px 0 5px 10px; + & .euiBadge { + margin: 2px 0 2px 0; + } +`; +Details.displayName = 'Details'; + +export const TokensFlexItem = styled(EuiFlexItem)` + margin-left: 3px; +`; +TokensFlexItem.displayName = 'TokensFlexItem'; + +export function isNillEmptyOrNotFinite(value: string | number | T[] | null | undefined) { + return isNumber(value) ? !isFinite(value) : isEmpty(value); +} + +export const isFileEvent = ({ + eventCategory, + eventDataset, +}: { + eventCategory: string | null | undefined; + eventDataset: string | null | undefined; +}) => + (eventCategory != null && eventCategory.toLowerCase() === 'file') || + (eventDataset != null && eventDataset.toLowerCase() === 'file'); + +export const isProcessStoppedOrTerminationEvent = ( + eventAction: string | null | undefined +): boolean => ['process_stopped', 'termination_event'].includes(`${eventAction}`.toLowerCase()); + +export const showVia = (eventAction: string | null | undefined): boolean => + ['file_create_event', 'created', 'file_delete_event', 'deleted'].includes( + `${eventAction}`.toLowerCase() + ); diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/host_working_dir.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/host_working_dir.test.tsx similarity index 94% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/host_working_dir.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/host_working_dir.test.tsx index d84dfcc5618826..85a000bbcaf638 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/host_working_dir.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/host_working_dir.test.tsx @@ -7,8 +7,8 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { mockTimelineData, TestProviders } from '../../../../mock'; -import { useMountAppended } from '../../../../utils/use_mount_appended'; +import { mockTimelineData, TestProviders } from '../../../../../common/mock'; +import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; import { HostWorkingDir } from './host_working_dir'; describe('HostWorkingDir', () => { diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/host_working_dir.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/host_working_dir.tsx similarity index 94% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/host_working_dir.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/host_working_dir.tsx index db49df30be473f..89d46dd287ffd8 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/host_working_dir.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/host_working_dir.tsx @@ -6,7 +6,7 @@ import React from 'react'; -import { DraggableBadge } from '../../../draggables'; +import { DraggableBadge } from '../../../../../common/components/draggables'; import * as i18n from './translations'; import { TokensFlexItem } from './helpers'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/index.ts b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/index.ts similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/index.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/index.ts diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/netflow.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/netflow.tsx similarity index 86% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/netflow.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/netflow.tsx index 0990301b6e2b92..0492450df51347 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/netflow.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/netflow.tsx @@ -7,22 +7,28 @@ import { get } from 'lodash/fp'; import React from 'react'; -import { Ecs } from '../../../../graphql/types'; -import { asArrayIfExists } from '../../../../lib/helpers'; +import { Ecs } from '../../../../../graphql/types'; +import { asArrayIfExists } from '../../../../../common/lib/helpers'; import { TLS_CLIENT_CERTIFICATE_FINGERPRINT_SHA1_FIELD_NAME, TLS_SERVER_CERTIFICATE_FINGERPRINT_SHA1_FIELD_NAME, } from '../../../certificate_fingerprint'; import { EVENT_DURATION_FIELD_NAME } from '../../../duration'; -import { ID_FIELD_NAME } from '../../../event_details/event_id'; -import { DESTINATION_IP_FIELD_NAME, SOURCE_IP_FIELD_NAME } from '../../../ip'; +import { ID_FIELD_NAME } from '../../../../../common/components/event_details/event_id'; +import { + DESTINATION_IP_FIELD_NAME, + SOURCE_IP_FIELD_NAME, +} from '../../../../../network/components/ip'; import { JA3_HASH_FIELD_NAME } from '../../../ja3_fingerprint'; import { Netflow } from '../../../netflow'; import { EVENT_END_FIELD_NAME, EVENT_START_FIELD_NAME, } from '../../../netflow/netflow_columns/duration_event_start_end'; -import { DESTINATION_PORT_FIELD_NAME, SOURCE_PORT_FIELD_NAME } from '../../../port'; +import { + DESTINATION_PORT_FIELD_NAME, + SOURCE_PORT_FIELD_NAME, +} from '../../../../../network/components/port'; import { DESTINATION_GEO_CITY_NAME_FIELD_NAME, DESTINATION_GEO_CONTINENT_NAME_FIELD_NAME, @@ -34,13 +40,13 @@ import { SOURCE_GEO_COUNTRY_ISO_CODE_FIELD_NAME, SOURCE_GEO_COUNTRY_NAME_FIELD_NAME, SOURCE_GEO_REGION_NAME_FIELD_NAME, -} from '../../../source_destination/geo_fields'; +} from '../../../../../network/components/source_destination/geo_fields'; import { DESTINATION_BYTES_FIELD_NAME, DESTINATION_PACKETS_FIELD_NAME, SOURCE_BYTES_FIELD_NAME, SOURCE_PACKETS_FIELD_NAME, -} from '../../../source_destination/source_destination_arrows'; +} from '../../../../../network/components/source_destination/source_destination_arrows'; import { NETWORK_BYTES_FIELD_NAME, NETWORK_COMMUNITY_ID_FIELD_NAME, @@ -48,7 +54,7 @@ import { NETWORK_PACKETS_FIELD_NAME, NETWORK_PROTOCOL_FIELD_NAME, NETWORK_TRANSPORT_FIELD_NAME, -} from '../../../source_destination/field_names'; +} from '../../../../../network/components/source_destination/field_names'; interface NetflowRendererProps { data: Ecs; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/netflow/__snapshots__/netflow_row_renderer.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/netflow/__snapshots__/netflow_row_renderer.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/netflow/__snapshots__/netflow_row_renderer.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/netflow/__snapshots__/netflow_row_renderer.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx similarity index 91% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx index e5375302f5bab2..9c620f5cf6701f 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.test.tsx @@ -7,11 +7,11 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { BrowserFields } from '../../../../../containers/source'; -import { mockBrowserFields } from '../../../../../containers/source/mock'; -import { Ecs } from '../../../../../graphql/types'; -import { getMockNetflowData, TestProviders } from '../../../../../mock'; -import { useMountAppended } from '../../../../../utils/use_mount_appended'; +import { BrowserFields } from '../../../../../../common/containers/source'; +import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; +import { Ecs } from '../../../../../../graphql/types'; +import { getMockNetflowData, TestProviders } from '../../../../../../common/mock'; +import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; import { eventActionMatches, diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx similarity index 90% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx index 10d80e1952f40c..7926b447196fb9 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/netflow/netflow_row_renderer.tsx @@ -10,14 +10,17 @@ import { get } from 'lodash/fp'; import React from 'react'; import styled from 'styled-components'; -import { asArrayIfExists } from '../../../../../lib/helpers'; +import { asArrayIfExists } from '../../../../../../common/lib/helpers'; import { TLS_CLIENT_CERTIFICATE_FINGERPRINT_SHA1_FIELD_NAME, TLS_SERVER_CERTIFICATE_FINGERPRINT_SHA1_FIELD_NAME, } from '../../../../certificate_fingerprint'; import { EVENT_DURATION_FIELD_NAME } from '../../../../duration'; -import { ID_FIELD_NAME } from '../../../../event_details/event_id'; -import { DESTINATION_IP_FIELD_NAME, SOURCE_IP_FIELD_NAME } from '../../../../ip'; +import { ID_FIELD_NAME } from '../../../../../../common/components/event_details/event_id'; +import { + DESTINATION_IP_FIELD_NAME, + SOURCE_IP_FIELD_NAME, +} from '../../../../../../network/components/ip'; import { JA3_HASH_FIELD_NAME } from '../../../../ja3_fingerprint'; import { Netflow } from '../../../../netflow'; import { @@ -28,7 +31,10 @@ import { PROCESS_NAME_FIELD_NAME, USER_NAME_FIELD_NAME, } from '../../../../netflow/netflow_columns/user_process'; -import { DESTINATION_PORT_FIELD_NAME, SOURCE_PORT_FIELD_NAME } from '../../../../port'; +import { + DESTINATION_PORT_FIELD_NAME, + SOURCE_PORT_FIELD_NAME, +} from '../../../../../../network/components/port'; import { NETWORK_BYTES_FIELD_NAME, NETWORK_COMMUNITY_ID_FIELD_NAME, @@ -36,7 +42,7 @@ import { NETWORK_PACKETS_FIELD_NAME, NETWORK_PROTOCOL_FIELD_NAME, NETWORK_TRANSPORT_FIELD_NAME, -} from '../../../../source_destination/field_names'; +} from '../../../../../../network/components/source_destination/field_names'; import { DESTINATION_GEO_CITY_NAME_FIELD_NAME, DESTINATION_GEO_CONTINENT_NAME_FIELD_NAME, @@ -48,13 +54,13 @@ import { SOURCE_GEO_COUNTRY_ISO_CODE_FIELD_NAME, SOURCE_GEO_COUNTRY_NAME_FIELD_NAME, SOURCE_GEO_REGION_NAME_FIELD_NAME, -} from '../../../../source_destination/geo_fields'; +} from '../../../../../../network/components/source_destination/geo_fields'; import { DESTINATION_BYTES_FIELD_NAME, DESTINATION_PACKETS_FIELD_NAME, SOURCE_BYTES_FIELD_NAME, SOURCE_PACKETS_FIELD_NAME, -} from '../../../../source_destination/source_destination_arrows'; +} from '../../../../../../network/components/source_destination/source_destination_arrows'; import { RowRenderer, RowRendererContainer } from '../row_renderer'; const Details = styled.div` diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/parent_process_draggable.test.tsx similarity index 95% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/parent_process_draggable.test.tsx index 684def7386da04..0a173f766ae19b 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/parent_process_draggable.test.tsx @@ -6,10 +6,10 @@ import React from 'react'; -import { TestProviders } from '../../../../mock'; +import { TestProviders } from '../../../../../common/mock'; import { ParentProcessDraggable } from './parent_process_draggable'; -import { useMountAppended } from '../../../../utils/use_mount_appended'; +import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; describe('ParentProcessDraggable', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/parent_process_draggable.tsx similarity index 96% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/parent_process_draggable.tsx index 1402743ef8a519..12d23e2f0b604f 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/parent_process_draggable.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/parent_process_draggable.tsx @@ -6,7 +6,7 @@ import React from 'react'; -import { DraggableBadge } from '../../../draggables'; +import { DraggableBadge } from '../../../../../common/components/draggables'; import { isNillEmptyOrNotFinite, TokensFlexItem } from './helpers'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/parse_query_value.test.ts b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/parse_query_value.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/parse_query_value.test.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/parse_query_value.test.ts diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/parse_query_value.ts b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/parse_query_value.ts similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/parse_query_value.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/parse_query_value.ts diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/parse_value.test.ts b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/parse_value.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/parse_value.test.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/parse_value.test.ts diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/parse_value.ts b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/parse_value.ts similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/parse_value.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/parse_value.ts diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/plain_column_renderer.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/plain_column_renderer.test.tsx similarity index 96% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/plain_column_renderer.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/plain_column_renderer.test.tsx index 8a22307767a40b..b80b3cf9a375a8 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/plain_column_renderer.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/plain_column_renderer.test.tsx @@ -8,10 +8,10 @@ import { shallow } from 'enzyme'; import { cloneDeep } from 'lodash/fp'; import React from 'react'; -import { TimelineNonEcsData } from '../../../../graphql/types'; -import { defaultHeaders, mockTimelineData, TestProviders } from '../../../../mock'; -import { getEmptyValue } from '../../../empty_value'; -import { useMountAppended } from '../../../../utils/use_mount_appended'; +import { TimelineNonEcsData } from '../../../../../graphql/types'; +import { defaultHeaders, mockTimelineData, TestProviders } from '../../../../../common/mock'; +import { getEmptyValue } from '../../../../../common/components/empty_value'; +import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; import { plainColumnRenderer } from './plain_column_renderer'; import { getValues, deleteItemIdx, findItem } from './helpers'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/plain_column_renderer.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/plain_column_renderer.tsx similarity index 88% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/plain_column_renderer.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/plain_column_renderer.tsx index f6a61889c501b7..d2881382b17014 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/plain_column_renderer.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/plain_column_renderer.tsx @@ -7,9 +7,9 @@ import { head } from 'lodash/fp'; import React from 'react'; -import { TimelineNonEcsData } from '../../../../graphql/types'; -import { ColumnHeaderOptions } from '../../../../store/timeline/model'; -import { getEmptyTagValue } from '../../../empty_value'; +import { TimelineNonEcsData } from '../../../../../graphql/types'; +import { ColumnHeaderOptions } from '../../../../../timelines/store/timeline/model'; +import { getEmptyTagValue } from '../../../../../common/components/empty_value'; import { ColumnRenderer } from './column_renderer'; import { FormattedFieldValue } from './formatted_field'; import { parseValue } from './parse_value'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/plain_row_renderer.test.tsx similarity index 88% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/plain_row_renderer.test.tsx index 467f507e8be7d6..82d42988ef34f7 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/plain_row_renderer.test.tsx @@ -10,9 +10,9 @@ import { cloneDeep } from 'lodash'; import React from 'react'; import { ThemeProvider } from 'styled-components'; -import { mockBrowserFields } from '../../../../containers/source/mock'; -import { Ecs } from '../../../../graphql/types'; -import { mockTimelineData } from '../../../../mock'; +import { mockBrowserFields } from '../../../../../common/containers/source/mock'; +import { Ecs } from '../../../../../graphql/types'; +import { mockTimelineData } from '../../../../../common/mock'; import { plainRowRenderer } from './plain_row_renderer'; describe('plain_row_renderer', () => { diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/plain_row_renderer.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/plain_row_renderer.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/plain_row_renderer.tsx diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/process_draggable.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/process_draggable.test.tsx similarity index 99% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/process_draggable.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/process_draggable.test.tsx index 8cc7323ed358f8..91ae94940f7f4f 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/process_draggable.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/process_draggable.test.tsx @@ -7,9 +7,9 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { TestProviders } from '../../../../mock'; +import { TestProviders } from '../../../../../common/mock'; import { ProcessDraggable, ProcessDraggableWithNonExistentProcess } from './process_draggable'; -import { useMountAppended } from '../../../../utils/use_mount_appended'; +import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; describe('ProcessDraggable', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/process_draggable.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/process_draggable.tsx similarity index 97% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/process_draggable.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/process_draggable.tsx index 35512c60629dd0..6e900fb3cab4d7 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/process_draggable.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/process_draggable.tsx @@ -6,7 +6,7 @@ import React from 'react'; -import { DraggableBadge } from '../../../draggables'; +import { DraggableBadge } from '../../../../../common/components/draggables'; import { isNillEmptyOrNotFinite } from './helpers'; import * as i18n from './translations'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/process_hash.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/process_hash.test.tsx similarity index 94% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/process_hash.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/process_hash.test.tsx index 08a9d29967db27..55cc61edb064e1 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/process_hash.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/process_hash.test.tsx @@ -6,8 +6,8 @@ import React from 'react'; -import { TestProviders } from '../../../../mock'; -import { useMountAppended } from '../../../../utils/use_mount_appended'; +import { TestProviders } from '../../../../../common/mock'; +import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; import { ProcessHash } from './process_hash'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/process_hash.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/process_hash.tsx similarity index 96% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/process_hash.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/process_hash.tsx index b6696d38dc1c53..9658ed89a60877 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/process_hash.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/process_hash.tsx @@ -8,7 +8,7 @@ import { EuiFlexGroup } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; -import { DraggableBadge } from '../../../draggables'; +import { DraggableBadge } from '../../../../../common/components/draggables'; import { isNillEmptyOrNotFinite, TokensFlexItem } from './helpers'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/row_renderer.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/row_renderer.tsx similarity index 87% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/row_renderer.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/row_renderer.tsx index 2d9f877fe4af0d..5cee0a0118dd2e 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/row_renderer.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/row_renderer.tsx @@ -6,8 +6,8 @@ import React from 'react'; -import { BrowserFields } from '../../../../containers/source'; -import { Ecs } from '../../../../graphql/types'; +import { BrowserFields } from '../../../../../common/containers/source'; +import { Ecs } from '../../../../../graphql/types'; import { EventsTrSupplement } from '../../styles'; interface RowRendererContainerProps { diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_details.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_details.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_details.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_details.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_row_renderer.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/suricata/__snapshots__/suricata_signature.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx similarity index 83% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx index 027aa0df8bcdd3..d5040cb2523704 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/suricata/suricata_details.test.tsx @@ -7,10 +7,10 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { mockBrowserFields } from '../../../../../containers/source/mock'; -import { mockTimelineData } from '../../../../../mock'; -import { TestProviders } from '../../../../../mock/test_providers'; -import { useMountAppended } from '../../../../../utils/use_mount_appended'; +import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; +import { mockTimelineData } from '../../../../../../common/mock'; +import { TestProviders } from '../../../../../../common/mock/test_providers'; +import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; import { SuricataDetails } from './suricata_details'; describe('SuricataDetails', () => { diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/suricata/suricata_details.tsx similarity index 91% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/suricata/suricata_details.tsx index 17f5f236265edf..c21b609a0f91e9 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_details.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/suricata/suricata_details.tsx @@ -9,8 +9,8 @@ import { get } from 'lodash/fp'; import React from 'react'; import styled from 'styled-components'; -import { BrowserFields } from '../../../../../containers/source'; -import { Ecs } from '../../../../../graphql/types'; +import { BrowserFields } from '../../../../../../common/containers/source'; +import { Ecs } from '../../../../../../graphql/types'; import { NetflowRenderer } from '../netflow'; import { SuricataSignature } from './suricata_signature'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_links.test.ts b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/suricata/suricata_links.test.ts similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_links.test.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/suricata/suricata_links.test.ts diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_links.ts b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/suricata/suricata_links.ts similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_links.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/suricata/suricata_links.ts diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_refs.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/suricata/suricata_refs.tsx similarity index 92% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_refs.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/suricata/suricata_refs.tsx index dd773bb88ef686..08992216bf74d4 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_refs.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/suricata/suricata_refs.tsx @@ -8,7 +8,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; -import { ExternalLinkIcon } from '../../../../external_link_icon'; +import { ExternalLinkIcon } from '../../../../../../common/components/external_link_icon'; import { getLinksFromSignature } from './suricata_links'; const LinkEuiFlexItem = styled(EuiFlexItem)` diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx similarity index 85% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx index b26d8ce3693b4c..a10cd9dc97f6d4 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.test.tsx @@ -8,12 +8,12 @@ import { shallow } from 'enzyme'; import { cloneDeep } from 'lodash/fp'; import React from 'react'; -import { mockBrowserFields } from '../../../../../containers/source/mock'; -import { Ecs } from '../../../../../graphql/types'; -import { mockTimelineData } from '../../../../../mock'; -import { TestProviders } from '../../../../../mock/test_providers'; +import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; +import { Ecs } from '../../../../../../graphql/types'; +import { mockTimelineData } from '../../../../../../common/mock'; +import { TestProviders } from '../../../../../../common/mock/test_providers'; import { suricataRowRenderer } from './suricata_row_renderer'; -import { useMountAppended } from '../../../../../utils/use_mount_appended'; +import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; describe('suricata_row_renderer', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/suricata/suricata_row_renderer.tsx diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.test.tsx similarity index 93% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.test.tsx index beae16af558ed5..245e538f691933 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.test.tsx @@ -7,8 +7,8 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { TestProviders } from '../../../../../mock'; -import { useMountAppended } from '../../../../../utils/use_mount_appended'; +import { TestProviders } from '../../../../../../common/mock'; +import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; import { SuricataSignature, Tokens, diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.tsx similarity index 87% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.tsx index 66c559729cccdf..3ae88a1e7c57df 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/suricata/suricata_signature.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/suricata/suricata_signature.tsx @@ -8,15 +8,18 @@ import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; -import { DragEffects, DraggableWrapper } from '../../../../drag_and_drop/draggable_wrapper'; -import { escapeDataProviderId } from '../../../../drag_and_drop/helpers'; -import { ExternalLinkIcon } from '../../../../external_link_icon'; -import { GoogleLink } from '../../../../links'; -import { Provider } from '../../../../timeline/data_providers/provider'; +import { + DragEffects, + DraggableWrapper, +} from '../../../../../../common/components/drag_and_drop/draggable_wrapper'; +import { escapeDataProviderId } from '../../../../../../common/components/drag_and_drop/helpers'; +import { ExternalLinkIcon } from '../../../../../../common/components/external_link_icon'; +import { GoogleLink } from '../../../../../../common/components/links'; +import { Provider } from '../../../data_providers/provider'; import { TokensFlexItem } from '../helpers'; import { getBeginningTokens } from './suricata_links'; -import { DefaultDraggable } from '../../../../draggables'; +import { DefaultDraggable } from '../../../../../../common/components/draggables'; import { IS_OPERATOR } from '../../../data_providers/data_provider'; export const SURICATA_SIGNATURE_FIELD_NAME = 'suricata.eve.alert.signature'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/auth_ssh.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/__snapshots__/auth_ssh.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/auth_ssh.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/__snapshots__/auth_ssh.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_details.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/__snapshots__/generic_details.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_details.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/__snapshots__/generic_details.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_file_details.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/__snapshots__/generic_file_details.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_file_details.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/__snapshots__/generic_file_details.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_row_renderer.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/__snapshots__/generic_row_renderer.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/generic_row_renderer.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/__snapshots__/generic_row_renderer.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/package.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/__snapshots__/package.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/system/__snapshots__/package.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/__snapshots__/package.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/system/auth_ssh.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/auth_ssh.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/system/auth_ssh.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/auth_ssh.test.tsx diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/system/auth_ssh.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/auth_ssh.tsx similarity index 93% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/system/auth_ssh.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/auth_ssh.tsx index 0ff2eec35314d8..431f1b5e974d59 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/system/auth_ssh.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/auth_ssh.tsx @@ -6,7 +6,7 @@ import React from 'react'; -import { DraggableBadge } from '../../../../draggables'; +import { DraggableBadge } from '../../../../../../common/components/draggables'; import { TokensFlexItem } from '../helpers'; interface Props { diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/generic_details.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/generic_details.test.tsx new file mode 100644 index 00000000000000..e622c91e8b8700 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/generic_details.test.tsx @@ -0,0 +1,542 @@ +/* + * 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 { BrowserFields } from '../../../../../../common/containers/source'; +import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; +import { mockTimelineData, TestProviders } from '../../../../../../common/mock'; +import { SystemGenericDetails, SystemGenericLine } from './generic_details'; +import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; + +describe('SystemGenericDetails', () => { + const mount = useMountAppended(); + + describe('rendering', () => { + test('it renders the default SystemGenericDetails', () => { + // I cannot and do not want to use BrowserFields for the mocks for the snapshot tests as they are too heavy + const browserFields: BrowserFields = {}; + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); + + test('it returns system rendering if the data does contain system data', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual( + 'Braden@zeek-london[generic-text-123](6278)with resultfailureSource128.199.212.120' + ); + }); + }); + + describe('#SystemGenericLine', () => { + test('it returns pretty output if you send in all your happy path data', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual( + '[username-123]\\[userDomain-123]@[hostname-123]in[working-directory-123][generic-text-123][processName-123](123)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' + ); + }); + + test('it returns nothing if data is all null', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual(''); + }); + + test('it can return only the host name', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual('[hostname-123]'); + }); + + test('it can return the host, message', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual('[hostname-123][message-123]'); + }); + + test('it can return the host, message, outcome', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual('[hostname-123]with result[outcome-123][message-123]'); + }); + + test('it can return the host, message, outcome, packageName', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual( + '[hostname-123]with result[outcome-123][packageName-123][message-123]' + ); + }); + + test('it can return the host, message, outcome, packageName, pacakgeSummary', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual( + '[hostname-123]with result[outcome-123][packageName-123][packageSummary-123][message-123]' + ); + }); + + test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual( + '[hostname-123]with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' + ); + }); + + test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual( + '[hostname-123][processExecutable-123]with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' + ); + }); + + test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual( + '[hostname-123][processExecutable-123](123)with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' + ); + }); + + test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual( + '[hostname-123][processName-123](123)with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' + ); + }); + + test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName, sshMethod', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual( + '[hostname-123][processName-123](123)with result[outcome-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' + ); + }); + + test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName, sshMethod, sshSignature', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual( + '[hostname-123][processName-123](123)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' + ); + }); + + test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName, sshMethod, sshSignature, text', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual( + '[hostname-123][text-123][processName-123](123)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' + ); + }); + + test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName, sshMethod, sshSignature, text, userDomain, username', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual( + '[username-123]\\[userDomain-123]@[hostname-123][text-123][processName-123](123)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' + ); + }); + + test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processPid, processName, sshMethod, sshSignature, text, userDomain, username, working-directory', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual( + '[username-123]\\[userDomain-123]@[hostname-123]in[working-directory-123][text-123][processName-123](123)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' + ); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/generic_details.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/generic_details.tsx new file mode 100644 index 00000000000000..e849732d07f6ff --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/generic_details.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 { EuiFlexGroup, EuiSpacer } from '@elastic/eui'; +import { get } from 'lodash/fp'; +import React from 'react'; + +import { BrowserFields } from '../../../../../../common/containers/source'; +import { Ecs } from '../../../../../../graphql/types'; +import { DraggableBadge } from '../../../../../../common/components/draggables'; +import { OverflowField } from '../../../../../../common/components/tables/helpers'; + +import * as i18n from './translations'; +import { NetflowRenderer } from '../netflow'; +import { UserHostWorkingDir } from '../user_host_working_dir'; +import { Details, TokensFlexItem } from '../helpers'; +import { ProcessDraggable } from '../process_draggable'; +import { Package } from './package'; +import { AuthSsh } from './auth_ssh'; +import { Badge } from '../../../../../../common/components/page'; + +interface Props { + contextId: string; + hostName: string | null | undefined; + id: string; + message: string | null | undefined; + outcome: string | null | undefined; + packageName: string | null | undefined; + packageSummary: string | null | undefined; + packageVersion: string | null | undefined; + processExecutable: string | null | undefined; + processPid: number | null | undefined; + processName: string | null | undefined; + sshMethod: string | null | undefined; + sshSignature: string | null | undefined; + text: string | null | undefined; + userDomain: string | null | undefined; + userName: string | null | undefined; + workingDirectory: string | null | undefined; +} + +export const SystemGenericLine = React.memo( + ({ + contextId, + hostName, + id, + message, + outcome, + packageName, + packageSummary, + packageVersion, + processPid, + processName, + processExecutable, + sshSignature, + sshMethod, + text, + userDomain, + userName, + workingDirectory, + }) => ( + <> + + + + {text} + + + + + {outcome != null && ( + + {i18n.WITH_RESULT} + + )} + + + + + + + {message != null && ( + <> + + + + + + + + + + )} + + ) +); + +SystemGenericLine.displayName = 'SystemGenericLine'; + +interface GenericDetailsProps { + browserFields: BrowserFields; + data: Ecs; + contextId: string; + text: string; + timelineId: string; +} + +export const SystemGenericDetails = React.memo( + ({ data, contextId, text, timelineId }) => { + const id = data._id; + const message: string | null = data.message != null ? data.message[0] : null; + const hostName: string | null | undefined = get('host.name[0]', data); + const userDomain: string | null | undefined = get('user.domain[0]', data); + const userName: string | null | undefined = get('user.name[0]', data); + const outcome: string | null | undefined = get('event.outcome[0]', data); + const packageName: string | null | undefined = get('system.audit.package.name[0]', data); + const packageSummary: string | null | undefined = get('system.audit.package.summary[0]', data); + const packageVersion: string | null | undefined = get('system.audit.package.version[0]', data); + const processPid: number | null | undefined = get('process.pid[0]', data); + const processName: string | null | undefined = get('process.name[0]', data); + const processExecutable: string | null | undefined = get('process.executable[0]', data); + const sshSignature: string | null | undefined = get('system.auth.ssh.signature[0]', data); + const sshMethod: string | null | undefined = get('system.auth.ssh.method[0]', data); + const workingDirectory: string | null | undefined = get('process.working_directory[0]', data); + + return ( +
+ + + +
+ ); + } +); + +SystemGenericDetails.displayName = 'SystemGenericDetails'; diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/generic_file_details.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/generic_file_details.test.tsx new file mode 100644 index 00000000000000..d8784233b664dc --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/generic_file_details.test.tsx @@ -0,0 +1,1599 @@ +/* + * 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 { BrowserFields } from '../../../../../../common/containers/source'; +import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; +import { mockTimelineData, TestProviders } from '../../../../../../common/mock'; +import { SystemGenericFileDetails, SystemGenericFileLine } from './generic_file_details'; +import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; + +describe('SystemGenericFileDetails', () => { + const mount = useMountAppended(); + + describe('rendering', () => { + test('it renders the default SystemGenericDetails', () => { + // I cannot and do not want to use BrowserFields for the mocks for the snapshot tests as they are too heavy + const browserFields: BrowserFields = {}; + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); + + test('it returns system rendering if the data does contain system data', () => { + const wrapper = mount( + + + + ); + expect(wrapper.text()).toEqual( + 'Evan@zeek-london[generic-text-123](6278)with resultfailureSource128.199.212.120' + ); + }); + }); + + describe('#SystemGenericFileLine', () => { + test('it returns pretty output if you send in all your happy path data', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual( + '[username-123]\\[userDomain-123]@[hostname-123]in[working-directory-123][generic-text-123][fileName-123]in[filePath-123][processName-123](123)[arg-1][arg-2][arg-3][some-title-123]with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' + ); + }); + + test('it returns nothing if data is all null', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual('an unknown process'); + }); + + test('it can return only the host name', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual('[hostname-123]an unknown process'); + }); + + test('it can return the host, message', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual('[hostname-123]an unknown process[message-123]'); + }); + + test('it can return the host, message, outcome', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual( + '[hostname-123]an unknown processwith result[outcome-123][message-123]' + ); + }); + + test('it can return the host, message, outcome, packageName', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual( + '[hostname-123]an unknown processwith result[outcome-123][packageName-123][message-123]' + ); + }); + + test('it can return the host, message, outcome, packageName, pacakgeSummary', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual( + '[hostname-123]an unknown processwith result[outcome-123][packageName-123][packageSummary-123][message-123]' + ); + }); + + test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual( + '[hostname-123]an unknown processwith result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' + ); + }); + + test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual( + '[hostname-123][packageVersion-123]with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][message-123]' + ); + }); + + test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual( + '[hostname-123][packageVersion-123]with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][processHashMd5-123][message-123]' + ); + }); + + test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual( + '[hostname-123][packageVersion-123]with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha1-123][processHashMd5-123][message-123]' + ); + }); + + test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual( + '[hostname-123][packageVersion-123]with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' + ); + }); + + test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual( + '[hostname-123][processExecutable-123](123)with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' + ); + }); + + test('it can return the host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual( + '[hostname-123][processName-123](123)via parent process(456)with result[outcome-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' + ); + }); + + test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual( + '[hostname-123][processName-123](123)with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' + ); + }); + + test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual( + '[hostname-123][processName-123](123)with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' + ); + }); + + test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual( + '[hostname-123][text-123][processName-123](123)with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' + ); + }); + + test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text, userDomain', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual( + '\\[userDomain-123][hostname-123][text-123][processName-123](123)with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' + ); + }); + + test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text, userDomain, username', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual( + '[username-123]\\[userDomain-123]@[hostname-123][text-123][processName-123](123)with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' + ); + }); + + test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text, userDomain, username, working-directory', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual( + '[username-123]\\[userDomain-123]@[hostname-123]in[working-directory-123][text-123][processName-123](123)with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' + ); + }); + + test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text, userDomain, username, working-directory, process-title', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual( + '[username-123]\\[userDomain-123]@[hostname-123]in[working-directory-123][text-123][processName-123](123)[process-title-123]with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' + ); + }); + + test('it can return the endgameExitCode, endgameParentProcessName, eventAction, host, message, outcome, packageName, pacakgeSummary, packageVersion, packageExecutable, processHashMd5, processHashSha1, processHashSha256, processPid, processPpid, processName, sshMethod, sshSignature, text, userDomain, username, working-directory, process-title, args', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual( + '[username-123]\\[userDomain-123]@[hostname-123]in[working-directory-123][text-123][processName-123](123)[arg-1][arg-2][arg-3][process-title-123]with exit code[endgameExitCode-123]via parent process[endgameParentProcessName-123](456)with result[outcome-123][sshSignature-123][sshMethod-123][packageName-123][packageVersion-123][packageSummary-123][processHashSha256-123][processHashSha1-123][processHashMd5-123][message-123]' + ); + }); + + test('it renders a FileDraggable when endgameFileName and endgameFilePath are provided, but fileName and filePath are NOT provided', () => { + const wrapper = mount( + +
+ +
+
+ ); + expect(wrapper.text()).toEqual('[endgameFileName]in[endgameFilePath]an unknown process'); + }); + + test('it prefers to render fileName and filePath over endgameFileName and endgameFilePath respectfully when all of those fields are provided', () => { + const wrapper = mount( + +
+ +
+
+ ); + + expect(wrapper.text()).toEqual('[fileName]in[filePath]an unknown process'); + }); + + ['file_create_event', 'created', 'file_delete_event', 'deleted'].forEach(eventAction => { + test(`it renders the text "via" when eventAction is ${eventAction}`, () => { + const wrapper = mount( + +
+ +
+
+ ); + + expect(wrapper.text().includes('via')).toBe(true); + }); + }); + + test('it does NOT render the text "via" when eventAction is not a whitelisted action', () => { + const eventAction = 'a_non_whitelisted_event_action'; + + const wrapper = mount( + +
+ +
+
+ ); + + expect(wrapper.text().includes('via')).toBe(false); + }); + + test('it renders a ParentProcessDraggable when eventAction is NOT "process_stopped" and NOT "termination_event"', () => { + const eventAction = 'something_else'; + + const wrapper = mount( + +
+ +
+
+ ); + + expect(wrapper.text()).toEqual( + 'an unknown processvia parent process[endgameParentProcessName](456)' + ); + }); + + test('it does NOT render a ParentProcessDraggable when eventAction is "process_stopped"', () => { + const eventAction = 'process_stopped'; + + const wrapper = mount( + +
+ +
+
+ ); + + expect(wrapper.text()).toEqual('an unknown process'); + }); + + test('it does NOT render a ParentProcessDraggable when eventAction is "termination_event"', () => { + const eventAction = 'termination_event'; + + const wrapper = mount( + +
+ +
+
+ ); + + expect(wrapper.text()).toEqual('an unknown process'); + }); + + test('it returns renders the message when showMessage is true', () => { + const wrapper = mount( + +
+ +
+
+ ); + + expect(wrapper.text()).toEqual('an unknown process[message]'); + }); + + test('it does NOT render the message when showMessage is false', () => { + const wrapper = mount( + +
+ +
+
+ ); + + expect(wrapper.text()).toEqual('an unknown process'); + }); + + test('it renders a ProcessDraggableWithNonExistentProcess when endgamePid and endgameProcessName are provided, but processPid and processName are NOT provided', () => { + const wrapper = mount( + +
+ +
+
+ ); + + expect(wrapper.text()).toEqual('[endgameProcessName](789)'); + }); + + test('it prefers to render processName and processPid over endgameProcessName and endgamePid respectfully when all of those fields are provided', () => { + const wrapper = mount( + +
+ +
+
+ ); + + expect(wrapper.text()).toEqual('[processName](123)'); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/generic_file_details.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/generic_file_details.tsx new file mode 100644 index 00000000000000..8dd513539a96a1 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/generic_file_details.tsx @@ -0,0 +1,298 @@ +/* + * 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 { EuiFlexGroup, EuiSpacer } from '@elastic/eui'; +import { get } from 'lodash/fp'; +import React from 'react'; + +import { BrowserFields } from '../../../../../../common/containers/source'; +import { Ecs } from '../../../../../../graphql/types'; +import { DraggableBadge } from '../../../../../../common/components/draggables'; +import { OverflowField } from '../../../../../../common/components/tables/helpers'; + +import * as i18n from './translations'; +import { NetflowRenderer } from '../netflow'; +import { UserHostWorkingDir } from '../user_host_working_dir'; +import { Details, isProcessStoppedOrTerminationEvent, showVia, TokensFlexItem } from '../helpers'; +import { ProcessDraggableWithNonExistentProcess } from '../process_draggable'; +import { Args } from '../args'; +import { AuthSsh } from './auth_ssh'; +import { ExitCodeDraggable } from '../exit_code_draggable'; +import { FileDraggable } from '../file_draggable'; +import { Package } from './package'; +import { Badge } from '../../../../../../common/components/page'; +import { ParentProcessDraggable } from '../parent_process_draggable'; +import { ProcessHash } from '../process_hash'; + +interface Props { + args: string[] | null | undefined; + contextId: string; + endgameExitCode: string | null | undefined; + endgameFileName: string | null | undefined; + endgameFilePath: string | null | undefined; + endgameParentProcessName: string | null | undefined; + endgamePid: number | null | undefined; + endgameProcessName: string | null | undefined; + eventAction: string | null | undefined; + fileName: string | null | undefined; + filePath: string | null | undefined; + hostName: string | null | undefined; + id: string; + message: string | null | undefined; + outcome: string | null | undefined; + packageName: string | null | undefined; + packageSummary: string | null | undefined; + packageVersion: string | null | undefined; + processName: string | null | undefined; + processPid: number | null | undefined; + processPpid: number | null | undefined; + processExecutable: string | null | undefined; + processHashMd5: string | null | undefined; + processHashSha1: string | null | undefined; + processHashSha256: string | null | undefined; + processTitle: string | null | undefined; + showMessage: boolean; + sshSignature: string | null | undefined; + sshMethod: string | null | undefined; + text: string | null | undefined; + userDomain: string | null | undefined; + userName: string | null | undefined; + workingDirectory: string | null | undefined; +} + +export const SystemGenericFileLine = React.memo( + ({ + args, + contextId, + endgameExitCode, + endgameFileName, + endgameFilePath, + endgameParentProcessName, + endgamePid, + endgameProcessName, + eventAction, + fileName, + filePath, + hostName, + id, + message, + outcome, + packageName, + packageSummary, + packageVersion, + processExecutable, + processHashMd5, + processHashSha1, + processHashSha256, + processName, + processPid, + processPpid, + processTitle, + showMessage, + sshSignature, + sshMethod, + text, + userDomain, + userName, + workingDirectory, + }) => ( + <> + + + + {text} + + + {showVia(eventAction) && ( + + {i18n.VIA} + + )} + + + + + + {!isProcessStoppedOrTerminationEvent(eventAction) && ( + + )} + {outcome != null && ( + + {i18n.WITH_RESULT} + + )} + + + + + + + + + {message != null && showMessage && ( + <> + + + + + + + + + + )} + + ) +); + +SystemGenericFileLine.displayName = 'SystemGenericFileLine'; + +interface GenericDetailsProps { + browserFields: BrowserFields; + data: Ecs; + contextId: string; + showMessage?: boolean; + text: string; + timelineId: string; +} + +export const SystemGenericFileDetails = React.memo( + ({ data, contextId, showMessage = true, text, timelineId }) => { + const id = data._id; + const message: string | null = data.message != null ? data.message[0] : null; + const hostName: string | null | undefined = get('host.name[0]', data); + const endgameExitCode: string | null | undefined = get('endgame.exit_code[0]', data); + const endgameFileName: string | null | undefined = get('endgame.file_name[0]', data); + const endgameFilePath: string | null | undefined = get('endgame.file_path[0]', data); + const endgameParentProcessName: string | null | undefined = get( + 'endgame.parent_process_name[0]', + data + ); + const endgamePid: number | null | undefined = get('endgame.pid[0]', data); + const endgameProcessName: string | null | undefined = get('endgame.process_name[0]', data); + const eventAction: string | null | undefined = get('event.action[0]', data); + const fileName: string | null | undefined = get('file.name[0]', data); + const filePath: string | null | undefined = get('file.path[0]', data); + const userDomain: string | null | undefined = get('user.domain[0]', data); + const userName: string | null | undefined = get('user.name[0]', data); + const outcome: string | null | undefined = get('event.outcome[0]', data); + const packageName: string | null | undefined = get('system.audit.package.name[0]', data); + const packageSummary: string | null | undefined = get('system.audit.package.summary[0]', data); + const packageVersion: string | null | undefined = get('system.audit.package.version[0]', data); + const processHashMd5: string | null | undefined = get('process.hash.md5[0]', data); + const processHashSha1: string | null | undefined = get('process.hash.sha1[0]', data); + const processHashSha256: string | null | undefined = get('process.hash.sha256', data); + const processPid: number | null | undefined = get('process.pid[0]', data); + const processPpid: number | null | undefined = get('process.ppid[0]', data); + const processName: string | null | undefined = get('process.name[0]', data); + const sshSignature: string | null | undefined = get('system.auth.ssh.signature[0]', data); + const sshMethod: string | null | undefined = get('system.auth.ssh.method[0]', data); + const processExecutable: string | null | undefined = get('process.executable[0]', data); + const processTitle: string | null | undefined = get('process.title[0]', data); + const workingDirectory: string | null | undefined = get('process.working_directory[0]', data); + const args: string[] | null | undefined = get('process.args', data); + + return ( +
+ + + +
+ ); + } +); + +SystemGenericFileDetails.displayName = 'SystemGenericFileDetails'; diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx new file mode 100644 index 00000000000000..26cccc82896eae --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.test.tsx @@ -0,0 +1,936 @@ +/* + * 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 { cloneDeep } from 'lodash/fp'; +import React from 'react'; + +import { BrowserFields } from '../../../../../../common/containers/source'; +import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; +import { Ecs } from '../../../../../../graphql/types'; +import { + mockDnsEvent, + mockFimFileCreatedEvent, + mockFimFileDeletedEvent, + mockSocketClosedEvent, + mockSocketOpenedEvent, + mockTimelineData, + TestProviders, +} from '../../../../../../common/mock'; +import { + mockEndgameAdminLogon, + mockEndgameCreationEvent, + mockEndgameDnsRequest, + mockEndgameExplicitUserLogon, + mockEndgameFileCreateEvent, + mockEndgameFileDeleteEvent, + mockEndgameIpv4ConnectionAcceptEvent, + mockEndgameIpv6ConnectionAcceptEvent, + mockEndgameIpv4DisconnectReceivedEvent, + mockEndgameIpv6DisconnectReceivedEvent, + mockEndgameTerminationEvent, + mockEndgameUserLogoff, + mockEndgameUserLogon, +} from '../../../../../../common/mock/mock_endgame_ecs_data'; +import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; +import { RowRenderer } from '../row_renderer'; +import { + createDnsRowRenderer, + createEndgameProcessRowRenderer, + createFimRowRenderer, + createGenericSystemRowRenderer, + createGenericFileRowRenderer, + createSecurityEventRowRenderer, + createSocketRowRenderer, +} from './generic_row_renderer'; +import * as i18n from './translations'; + +jest.mock('../../../../../../overview/components/events_by_dataset'); + +describe('GenericRowRenderer', () => { + const mount = useMountAppended(); + + describe('#createGenericSystemRowRenderer', () => { + let nonSystem: Ecs; + let system: Ecs; + let connectedToRenderer: RowRenderer; + beforeEach(() => { + nonSystem = cloneDeep(mockTimelineData[0].ecs); + system = cloneDeep(mockTimelineData[29].ecs); + connectedToRenderer = createGenericSystemRowRenderer({ + actionName: 'process_started', + text: 'some text', + }); + }); + test('renders correctly against snapshot', () => { + // I cannot and do not want to use BrowserFields mocks for the snapshot tests as they are too heavy + const browserFields: BrowserFields = {}; + const children = connectedToRenderer.renderRow({ + browserFields, + data: system, + timelineId: 'test', + }); + + const wrapper = shallow({children}); + expect(wrapper).toMatchSnapshot(); + }); + + test('should return false if not a system datum', () => { + expect(connectedToRenderer.isInstance(nonSystem)).toBe(false); + }); + + test('should return true if it is a system datum', () => { + expect(connectedToRenderer.isInstance(system)).toBe(true); + }); + + test('should return false when action is set to some other value', () => { + if (system.event != null && system.event.action != null) { + system.event.action[0] = 'some other value'; + expect(connectedToRenderer.isInstance(system)).toBe(false); + } else { + // if system.event or system.event.action is not defined in the mock + // then we will get an error here + expect(system.event).toBeDefined(); + } + }); + test('should render a system row', () => { + const children = connectedToRenderer.renderRow({ + browserFields: mockBrowserFields, + data: system, + timelineId: 'test', + }); + const wrapper = mount( + + {children} + + ); + expect(wrapper.text()).toContain( + 'Evan@zeek-londonsome text(6278)with resultfailureSource128.199.212.120' + ); + }); + }); + + describe('#createGenericFileRowRenderer', () => { + let nonSystem: Ecs; + let systemFile: Ecs; + let fileToRenderer: RowRenderer; + + beforeEach(() => { + nonSystem = cloneDeep(mockTimelineData[0].ecs); + systemFile = cloneDeep(mockTimelineData[28].ecs); + fileToRenderer = createGenericFileRowRenderer({ + actionName: 'user_login', + text: 'some text', + }); + }); + + test('renders correctly against snapshot', () => { + // I cannot and do not want to use BrowserFields mocks for the snapshot tests as they are too heavy + const browserFields: BrowserFields = {}; + const children = fileToRenderer.renderRow({ + browserFields, + data: systemFile, + timelineId: 'test', + }); + + const wrapper = shallow({children}); + expect(wrapper).toMatchSnapshot(); + }); + + test('should return false if not a auditd datum', () => { + expect(fileToRenderer.isInstance(nonSystem)).toBe(false); + }); + + test('should return true if it is a auditd datum', () => { + expect(fileToRenderer.isInstance(systemFile)).toBe(true); + }); + + test('should return false when action is set to some other value', () => { + if (systemFile.event != null && systemFile.event.action != null) { + systemFile.event.action[0] = 'some other value'; + expect(fileToRenderer.isInstance(systemFile)).toBe(false); + } else { + expect(systemFile.event).toBeDefined(); + } + }); + + test('should render a system row', () => { + const children = fileToRenderer.renderRow({ + browserFields: mockBrowserFields, + data: systemFile, + timelineId: 'test', + }); + const wrapper = mount( + + {children} + + ); + expect(wrapper.text()).toContain( + 'Braden@zeek-londonsome text(6278)with resultfailureSource128.199.212.120' + ); + }); + }); + + describe('#createEndgameProcessRowRenderer', () => { + test('it renders an endgame process creation_event', () => { + const actionName = 'creation_event'; + const text = i18n.PROCESS_STARTED; + const endgameCreationEvent = { + ...mockEndgameCreationEvent, + }; + + const endgameProcessCreationEventRowRenderer = createEndgameProcessRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {endgameProcessCreationEventRowRenderer.isInstance(endgameCreationEvent) && + endgameProcessCreationEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: endgameCreationEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'Arun\\Anvi-Acer@HD-obe-8bf77f54started processMicrosoft.Photos.exe(441684)C:\\Program Files\\WindowsApps\\Microsoft.Windows.Photos_2018.18091.17210.0_x64__8wekyb3d8bbwe\\Microsoft.Photos.exe-ServerName:App.AppXzst44mncqdg84v7sv6p7yznqwssy6f7f.mcavia parent processsvchost.exe(8)d4c97ed46046893141652e2ec0056a698f6445109949d7fcabbce331146889ee12563599116157778a22600d2a163d8112aed84562d06d7235b37895b68de56687895743' + ); + }); + + test('it renders an endgame process termination_event', () => { + const actionName = 'termination_event'; + const text = i18n.TERMINATED_PROCESS; + const endgameTerminationEvent = { + ...mockEndgameTerminationEvent, + }; + + const endgameProcessTerminationEventRowRenderer = createEndgameProcessRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {endgameProcessTerminationEventRowRenderer.isInstance(endgameTerminationEvent) && + endgameProcessTerminationEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: endgameTerminationEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'Arun\\Anvi-Acer@HD-obe-8bf77f54terminated processRuntimeBroker.exe(442384)with exit code087976f3430cc99bc939e0694247c0759961a49832b87218f4313d6fc0bc3a776797255e72d5ed5c058d4785950eba7abaa057653bd4401441a21bf1abce6404f4231db4d' + ); + }); + + test('it does NOT render the event if the action name does not match', () => { + const actionName = 'does_not_match'; + const text = i18n.PROCESS_STARTED; + const endgameCreationEvent = { + ...mockEndgameCreationEvent, + }; + + const endgameProcessCreationEventRowRenderer = createEndgameProcessRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {endgameProcessCreationEventRowRenderer.isInstance(endgameCreationEvent) && + endgameProcessCreationEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: endgameCreationEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual(''); + }); + + test('it does NOT render the event when the event category is NOT process', () => { + const actionName = 'creation_event'; + const text = i18n.PROCESS_STARTED; + const endgameCreationEvent = { + ...mockEndgameCreationEvent, + event: { + ...mockEndgameCreationEvent.event, + category: ['something_else'], + }, + }; + + const endgameProcessCreationEventRowRenderer = createEndgameProcessRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {endgameProcessCreationEventRowRenderer.isInstance(endgameCreationEvent) && + endgameProcessCreationEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: endgameCreationEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual(''); + }); + + test('it does NOT render the event when both the action name and event category do NOT match', () => { + const actionName = 'does_not_match'; + const text = i18n.PROCESS_STARTED; + const endgameCreationEvent = { + ...mockEndgameCreationEvent, + event: { + ...mockEndgameCreationEvent.event, + category: ['something_else'], + }, + }; + + const endgameProcessCreationEventRowRenderer = createEndgameProcessRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {endgameProcessCreationEventRowRenderer.isInstance(endgameCreationEvent) && + endgameProcessCreationEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: endgameCreationEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual(''); + }); + }); + + describe('#createFimRowRenderer', () => { + test('it renders an endgame file_create_event', () => { + const actionName = 'file_create_event'; + const text = i18n.CREATED_FILE; + const endgameFileCreateEvent = { + ...mockEndgameFileCreateEvent, + }; + + const endgameFileCreateEventRowRenderer = createFimRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {endgameFileCreateEventRowRenderer.isInstance(endgameFileCreateEvent) && + endgameFileCreateEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: endgameFileCreateEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'Arun\\Anvi-Acer@HD-obe-8bf77f54created a fileinC:\\Users\\Arun\\AppData\\Local\\Google\\Chrome\\User Data\\Default\\63d78c21-e593-4484-b7a9-db33cd522ddc.tmpviachrome.exe(11620)' + ); + }); + + test('it renders an endgame file_delete_event', () => { + const actionName = 'file_delete_event'; + const text = i18n.DELETED_FILE; + const endgameFileDeleteEvent = { + ...mockEndgameFileDeleteEvent, + }; + + const endgameFileDeleteEventRowRenderer = createFimRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {endgameFileDeleteEventRowRenderer.isInstance(endgameFileDeleteEvent) && + endgameFileDeleteEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: endgameFileDeleteEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'SYSTEM\\NT AUTHORITY@HD-v1s-d2118419deleted a filetmp000002f6inC:\\Windows\\TEMP\\tmp00000404\\tmp000002f6viaAmSvc.exe(1084)' + ); + }); + + test('it renders a FIM (non-endgame) file created event', () => { + const actionName = 'created'; + const text = i18n.CREATED_FILE; + const fimFileCreatedEvent = { + ...mockFimFileCreatedEvent, + }; + + const fileCreatedEventRowRenderer = createFimRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {fileCreatedEventRowRenderer.isInstance(fimFileCreatedEvent) && + fileCreatedEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: fimFileCreatedEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual('foohostcreated a filein/etc/subgidviaan unknown process'); + }); + + test('it renders a FIM (non-endgame) file deleted event', () => { + const actionName = 'deleted'; + const text = i18n.DELETED_FILE; + const fimFileDeletedEvent = { + ...mockFimFileDeletedEvent, + }; + + const fileDeletedEventRowRenderer = createFimRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {fileDeletedEventRowRenderer.isInstance(fimFileDeletedEvent) && + fileDeletedEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: fimFileDeletedEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'foohostdeleted a filein/etc/gshadow.lockviaan unknown process' + ); + }); + + test('it does NOT render an event if the action name does not match', () => { + const actionName = 'does_not_match'; + const text = i18n.CREATED_FILE; + const endgameFileCreateEvent = { + ...mockEndgameFileCreateEvent, + }; + + const endgameFileCreateEventRowRenderer = createFimRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {endgameFileCreateEventRowRenderer.isInstance(endgameFileCreateEvent) && + endgameFileCreateEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: endgameFileCreateEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual(''); + }); + + test('it does NOT render an Endgame file_create_event when category is NOT file', () => { + const actionName = 'file_create_event'; + const text = i18n.CREATED_FILE; + const endgameFileCreateEvent = { + ...mockEndgameFileCreateEvent, + event: { + ...mockEndgameFileCreateEvent.event, + category: ['something_else'], + }, + }; + + const endgameFileCreateEventRowRenderer = createFimRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {endgameFileCreateEventRowRenderer.isInstance(endgameFileCreateEvent) && + endgameFileCreateEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: endgameFileCreateEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual(''); + }); + + test('it does NOT render a FIM (non-Endgame) file created event when the event dataset is NOT file', () => { + const actionName = 'created'; + const text = i18n.CREATED_FILE; + const fimFileCreatedEvent = { + ...mockFimFileCreatedEvent, + event: { + ...mockEndgameFileCreateEvent.event, + dataset: ['something_else'], + }, + }; + + const fileCreatedEventRowRenderer = createFimRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {fileCreatedEventRowRenderer.isInstance(fimFileCreatedEvent) && + fileCreatedEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: fimFileCreatedEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual(''); + }); + }); + + describe('#createSocketRowRenderer', () => { + test('it renders an Endgame ipv4_connection_accept_event', () => { + const actionName = 'ipv4_connection_accept_event'; + const text = i18n.ACCEPTED_A_CONNECTION_VIA; + const ipv4ConnectionAcceptEvent = { + ...mockEndgameIpv4ConnectionAcceptEvent, + }; + + const endgameIpv4ConnectionAcceptEventRowRenderer = createSocketRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {endgameIpv4ConnectionAcceptEventRowRenderer.isInstance(ipv4ConnectionAcceptEvent) && + endgameIpv4ConnectionAcceptEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: ipv4ConnectionAcceptEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'SYSTEM\\NT AUTHORITY@HD-gqf-0af7b4feaccepted a connection viaAmSvc.exe(1084)tcp1:network-community_idSource127.0.0.1:49306Destination127.0.0.1:49305' + ); + }); + + test('it renders an Endgame ipv6_connection_accept_event', () => { + const actionName = 'ipv6_connection_accept_event'; + const text = i18n.ACCEPTED_A_CONNECTION_VIA; + const ipv6ConnectionAcceptEvent = { + ...mockEndgameIpv6ConnectionAcceptEvent, + }; + + const endgameIpv6ConnectionAcceptEventRowRenderer = createSocketRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {endgameIpv6ConnectionAcceptEventRowRenderer.isInstance(ipv6ConnectionAcceptEvent) && + endgameIpv6ConnectionAcceptEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: ipv6ConnectionAcceptEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'SYSTEM\\NT AUTHORITY@HD-55b-3ec87f66accepted a connection via(4)tcp1:network-community_idSource::1:51324Destination::1:5357' + ); + }); + + test('it renders an Endgame ipv4_disconnect_received_event', () => { + const actionName = 'ipv4_disconnect_received_event'; + const text = i18n.DISCONNECTED_VIA; + const ipv4DisconnectReceivedEvent = { + ...mockEndgameIpv4DisconnectReceivedEvent, + }; + + const endgameIpv4DisconnectReceivedEventRowRenderer = createSocketRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {endgameIpv4DisconnectReceivedEventRowRenderer.isInstance(ipv4DisconnectReceivedEvent) && + endgameIpv4DisconnectReceivedEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: ipv4DisconnectReceivedEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'Arun\\Anvi-Acer@HD-obe-8bf77f54disconnected viachrome.exe(11620)8.1KBtcp1:LxYHJJv98b2O0fNccXu6HheXmwk=Source192.168.0.6:59356(25.78%)2.1KB(74.22%)6KBDestination10.156.162.53:443' + ); + }); + + test('it renders an Endgame ipv6_disconnect_received_event', () => { + const actionName = 'ipv6_disconnect_received_event'; + const text = i18n.DISCONNECTED_VIA; + const ipv6DisconnectReceivedEvent = { + ...mockEndgameIpv6DisconnectReceivedEvent, + }; + + const endgameIpv6DisconnectReceivedEventRowRenderer = createSocketRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {endgameIpv6DisconnectReceivedEventRowRenderer.isInstance(ipv6DisconnectReceivedEvent) && + endgameIpv6DisconnectReceivedEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: ipv6DisconnectReceivedEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'SYSTEM\\NT AUTHORITY@HD-55b-3ec87f66disconnected via(4)7.9KBtcp1:ZylzQhsB1dcptA2t4DY8S6l9o8E=Source::1:51338(96.92%)7.7KB(3.08%)249BDestination::1:2869' + ); + }); + + test('it renders a (non-Endgame) socket_opened event', () => { + const actionName = 'socket_opened'; + const text = i18n.SOCKET_OPENED; + const socketOpenedEvent = { + ...mockSocketOpenedEvent, + }; + + const socketOpenedEventRowRenderer = createSocketRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {socketOpenedEventRowRenderer.isInstance(socketOpenedEvent) && + socketOpenedEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: socketOpenedEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'root@foohostopened a socket withgoogle_accounts(2166)Outbound socket (10.4.20.1:59554 -> 10.1.2.3:80) Ooutboundtcp1:network-community_idSource10.4.20.1:59554Destination10.1.2.3:80' + ); + }); + + test('it renders a (non-Endgame) socket_closed event', () => { + const actionName = 'socket_closed'; + const text = i18n.SOCKET_CLOSED; + const socketClosedEvent = { + ...mockSocketClosedEvent, + }; + + const socketClosedEventRowRenderer = createSocketRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {socketClosedEventRowRenderer.isInstance(socketClosedEvent) && + socketClosedEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: socketClosedEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'root@foohostclosed a socket withgoogle_accounts(2166)Outbound socket (10.4.20.1:59508 -> 10.1.2.3:80) Coutboundtcp1:network-community_idSource10.4.20.1:59508Destination10.1.2.3:80' + ); + }); + + test('it does NOT render an event if the action name does not match', () => { + const actionName = 'does_not_match'; + const text = i18n.ACCEPTED_A_CONNECTION_VIA; + const ipv4ConnectionAcceptEvent = { + ...mockEndgameIpv4ConnectionAcceptEvent, + }; + + const endgameIpv4ConnectionAcceptEventRowRenderer = createSocketRowRenderer({ + actionName, + text, + }); + + const wrapper = mount( + + {endgameIpv4ConnectionAcceptEventRowRenderer.isInstance(ipv4ConnectionAcceptEvent) && + endgameIpv4ConnectionAcceptEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: ipv4ConnectionAcceptEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual(''); + }); + }); + + describe('#createSecurityEventRowRenderer', () => { + test('it renders an Endgame user_logon event', () => { + const actionName = 'user_logon'; + const userLogonEvent = { + ...mockEndgameUserLogon, + }; + + const userLogonEventRowRenderer = createSecurityEventRowRenderer({ actionName }); + + const wrapper = mount( + + {userLogonEventRowRenderer.isInstance(userLogonEvent) && + userLogonEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: userLogonEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'SYSTEM\\NT AUTHORITY@HD-v1s-d2118419successfully logged inusing logon type5 - Service(target logon ID0x3e7)viaC:\\Windows\\System32\\services.exe(432)as requested by subjectWIN-Q3DOP1UKA81$(subject logon ID0x3e7)4624' + ); + }); + + test('it renders an Endgame admin_logon event', () => { + const actionName = 'admin_logon'; + const adminLogonEvent = { + ...mockEndgameAdminLogon, + }; + + const adminLogonEventRowRenderer = createSecurityEventRowRenderer({ actionName }); + + const wrapper = mount( + + {adminLogonEventRowRenderer.isInstance(adminLogonEvent) && + adminLogonEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: adminLogonEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'With special privileges,SYSTEM\\NT AUTHORITY@HD-obe-8bf77f54successfully logged inviaC:\\Windows\\System32\\lsass.exe(964)as requested by subjectSYSTEM\\NT AUTHORITY4672' + ); + }); + + test('it renders an Endgame explicit_user_logon event', () => { + const actionName = 'explicit_user_logon'; + const explicitUserLogonEvent = { + ...mockEndgameExplicitUserLogon, + }; + + const explicitUserLogonEventRowRenderer = createSecurityEventRowRenderer({ actionName }); + + const wrapper = mount( + + {explicitUserLogonEventRowRenderer.isInstance(explicitUserLogonEvent) && + explicitUserLogonEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: explicitUserLogonEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'A login was attempted using explicit credentialsArun\\Anvi-AcertoHD-55b-3ec87f66viaC:\\Windows\\System32\\svchost.exe(1736)as requested by subjectANVI-ACER$\\WORKGROUP(subject logon ID0x3e7)4648' + ); + }); + + test('it renders an Endgame user_logoff event', () => { + const actionName = 'user_logoff'; + const userLogoffEvent = { + ...mockEndgameUserLogoff, + }; + + const userLogoffEventRowRenderer = createSecurityEventRowRenderer({ actionName }); + + const wrapper = mount( + + {userLogoffEventRowRenderer.isInstance(userLogoffEvent) && + userLogoffEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: userLogoffEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'Arun\\Anvi-Acer@HD-55b-3ec87f66logged offusing logon type2 - Interactive(target logon ID0x16db41e)viaC:\\Windows\\System32\\lsass.exe(964)4634' + ); + }); + + test('it does NOT render an event if the action name does not match', () => { + const actionName = 'does_not_match'; + const userLogonEvent = { + ...mockEndgameUserLogon, + }; + + const userLogonEventRowRenderer = createSecurityEventRowRenderer({ actionName }); + + const wrapper = mount( + + {userLogonEventRowRenderer.isInstance(userLogonEvent) && + userLogonEventRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: userLogonEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual(''); + }); + }); + + describe('#createDnsRowRenderer', () => { + test('it renders an Endgame DNS request_event', () => { + const requestEvent = { + ...mockEndgameDnsRequest, + }; + + const dnsRowRenderer = createDnsRowRenderer(); + + const wrapper = mount( + + {dnsRowRenderer.isInstance(requestEvent) && + dnsRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: requestEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'SYSTEM\\NT AUTHORITY@HD-obe-8bf77f54asked forupdate.googleapis.comwith question typeA, which resolved to10.100.197.67viaGoogleUpdate.exe(443192)3008dns' + ); + }); + + test('it renders a non-Endgame DNS event', () => { + const dnsEvent = { + ...mockDnsEvent, + }; + + const dnsRowRenderer = createDnsRowRenderer(); + + const wrapper = mount( + + {dnsRowRenderer.isInstance(dnsEvent) && + dnsRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: dnsEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual( + 'iot.example.comasked forlookup.example.comwith question typeA, which resolved to10.1.2.3(response code:NOERROR)viaan unknown process6.937500msOct 8, 2019 @ 10:05:23.241Oct 8, 2019 @ 10:05:23.248outbounddns177Budp1:network-community_idSource10.9.9.9:58732(22.60%)40B(77.40%)137BDestination10.1.1.1:53OceaniaAustralia🇦🇺AU' + ); + }); + + test('it does NOT render an event if dns.question.type is not provided', () => { + const requestEvent = { + ...mockEndgameDnsRequest, + dns: { + ...mockDnsEvent.dns, + question: { + name: ['lookup.example.com'], + }, + }, + }; + + const dnsRowRenderer = createDnsRowRenderer(); + + const wrapper = mount( + + {dnsRowRenderer.isInstance(requestEvent) && + dnsRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: requestEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual(''); + }); + + test('it does NOT render an event if dns.question.name is not provided', () => { + const requestEvent = { + ...mockEndgameDnsRequest, + dns: { + ...mockDnsEvent.dns, + question: { + type: ['A'], + }, + }, + }; + + const dnsRowRenderer = createDnsRowRenderer(); + + const wrapper = mount( + + {dnsRowRenderer.isInstance(requestEvent) && + dnsRowRenderer.renderRow({ + browserFields: mockBrowserFields, + data: requestEvent, + timelineId: 'test', + })} + + ); + + expect(wrapper.text()).toEqual(''); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/system/generic_row_renderer.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/generic_row_renderer.tsx diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/system/package.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/package.test.tsx similarity index 95% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/system/package.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/package.test.tsx index 100c8fbe5a9888..56f9452ba40b8d 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/system/package.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/package.test.tsx @@ -7,8 +7,8 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { TestProviders } from '../../../../../mock'; -import { useMountAppended } from '../../../../../utils/use_mount_appended'; +import { TestProviders } from '../../../../../../common/mock'; +import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; import { Package } from './package'; describe('Package', () => { diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/system/package.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/package.tsx similarity index 95% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/system/package.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/package.tsx index a28e850e2af968..7aa66f8db88305 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/system/package.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/package.tsx @@ -6,7 +6,7 @@ import React from 'react'; -import { DraggableBadge } from '../../../../draggables'; +import { DraggableBadge } from '../../../../../../common/components/draggables'; import { TokensFlexItem } from '../helpers'; interface Props { diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/system/translations.ts b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/system/translations.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/system/translations.ts diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/translations.ts b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/translations.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/translations.ts diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/unknown_column_renderer.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/unknown_column_renderer.test.tsx similarity index 91% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/unknown_column_renderer.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/unknown_column_renderer.test.tsx index 73d1d5cb441ef3..f49318171e8b67 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/unknown_column_renderer.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/unknown_column_renderer.test.tsx @@ -10,9 +10,9 @@ import { cloneDeep } from 'lodash'; import React from 'react'; import { ThemeProvider } from 'styled-components'; -import { TimelineNonEcsData } from '../../../../graphql/types'; -import { defaultHeaders, mockTimelineData } from '../../../../mock'; -import { getEmptyValue } from '../../../empty_value'; +import { TimelineNonEcsData } from '../../../../../graphql/types'; +import { defaultHeaders, mockTimelineData } from '../../../../../common/mock'; +import { getEmptyValue } from '../../../../../common/components/empty_value'; import { unknownColumnRenderer } from './unknown_column_renderer'; import { getValues } from './helpers'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/unknown_column_renderer.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/unknown_column_renderer.tsx similarity index 83% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/unknown_column_renderer.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/unknown_column_renderer.tsx index 4b4a4a3d43780d..49bc61f00a63e2 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/unknown_column_renderer.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/unknown_column_renderer.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getEmptyTagValue } from '../../../empty_value'; +import { getEmptyTagValue } from '../../../../../common/components/empty_value'; import { ColumnRenderer } from './column_renderer'; export const unknownColumnRenderer: ColumnRenderer = { diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/user_host_working_dir.test.tsx similarity index 98% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/user_host_working_dir.test.tsx index 45b670acb569ab..7f460d30d709c0 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/user_host_working_dir.test.tsx @@ -7,9 +7,9 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { TestProviders } from '../../../../mock'; +import { TestProviders } from '../../../../../common/mock'; import { UserHostWorkingDir } from './user_host_working_dir'; -import { useMountAppended } from '../../../../utils/use_mount_appended'; +import { useMountAppended } from '../../../../../common/utils/use_mount_appended'; describe('UserHostWorkingDir', () => { const mount = useMountAppended(); diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/user_host_working_dir.tsx similarity index 96% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/user_host_working_dir.tsx index d370afee2585f5..80585b37cfd9e6 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/user_host_working_dir.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/user_host_working_dir.tsx @@ -6,7 +6,7 @@ import React from 'react'; -import { DraggableBadge } from '../../../draggables'; +import { DraggableBadge } from '../../../../../common/components/draggables'; import { TokensFlexItem } from './helpers'; import { HostWorkingDir } from './host_working_dir'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/zeek/__snapshots__/zeek_details.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/zeek/__snapshots__/zeek_row_renderer.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_signature.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/zeek/__snapshots__/zeek_signature.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/__snapshots__/zeek_signature.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/zeek/__snapshots__/zeek_signature.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/translations.ts b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/zeek/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/translations.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/zeek/translations.ts diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx similarity index 95% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx index b45e4c41762bc7..d3ec7922342c36 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/zeek/zeek_details.test.tsx @@ -6,9 +6,9 @@ import React from 'react'; -import { mockBrowserFields } from '../../../../../containers/source/mock'; -import { mockTimelineData, TestProviders } from '../../../../../mock'; -import { useMountAppended } from '../../../../../utils/use_mount_appended'; +import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; +import { mockTimelineData, TestProviders } from '../../../../../../common/mock'; +import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; import { ZeekDetails } from './zeek_details'; describe('ZeekDetails', () => { diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/zeek/zeek_details.tsx similarity index 88% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/zeek/zeek_details.tsx index d8561186b45462..4f991429d6a4d3 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_details.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/zeek/zeek_details.tsx @@ -8,8 +8,8 @@ import { EuiSpacer } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; -import { BrowserFields } from '../../../../../containers/source'; -import { Ecs } from '../../../../../graphql/types'; +import { BrowserFields } from '../../../../../../common/containers/source'; +import { Ecs } from '../../../../../../graphql/types'; import { NetflowRenderer } from '../netflow'; import { ZeekSignature } from './zeek_signature'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx similarity index 87% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx index 456b93eb829ee5..2197ccb0ce2e00 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.test.tsx @@ -8,10 +8,10 @@ import { shallow } from 'enzyme'; import { cloneDeep } from 'lodash/fp'; import React from 'react'; -import { mockBrowserFields } from '../../../../../containers/source/mock'; -import { Ecs } from '../../../../../graphql/types'; -import { mockTimelineData, TestProviders } from '../../../../../mock'; -import { useMountAppended } from '../../../../../utils/use_mount_appended'; +import { mockBrowserFields } from '../../../../../../common/containers/source/mock'; +import { Ecs } from '../../../../../../graphql/types'; +import { mockTimelineData, TestProviders } from '../../../../../../common/mock'; +import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; import { zeekRowRenderer } from './zeek_row_renderer'; describe('zeek_row_renderer', () => { diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/zeek/zeek_row_renderer.tsx diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx similarity index 97% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx index f199b537f1be04..f416da5625042c 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.test.tsx @@ -8,9 +8,9 @@ import { shallow } from 'enzyme'; import { cloneDeep } from 'lodash/fp'; import React from 'react'; -import { Ecs } from '../../../../../graphql/types'; -import { mockTimelineData, TestProviders } from '../../../../../mock'; -import { useMountAppended } from '../../../../../utils/use_mount_appended'; +import { Ecs } from '../../../../../../graphql/types'; +import { mockTimelineData, TestProviders } from '../../../../../../common/mock'; +import { useMountAppended } from '../../../../../../common/utils/use_mount_appended'; import { ZeekSignature, extractStateValue, diff --git a/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.tsx similarity index 94% rename from x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.tsx index 4cb8140e22ceff..cdf4a8cba68abb 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/renderers/zeek/zeek_signature.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/renderers/zeek/zeek_signature.tsx @@ -9,12 +9,15 @@ import { get } from 'lodash/fp'; import React from 'react'; import styled from 'styled-components'; -import { Ecs } from '../../../../../graphql/types'; -import { DragEffects, DraggableWrapper } from '../../../../drag_and_drop/draggable_wrapper'; -import { escapeDataProviderId } from '../../../../drag_and_drop/helpers'; -import { ExternalLinkIcon } from '../../../../external_link_icon'; -import { GoogleLink, ReputationLink } from '../../../../links'; -import { Provider } from '../../../../timeline/data_providers/provider'; +import { Ecs } from '../../../../../../graphql/types'; +import { + DragEffects, + DraggableWrapper, +} from '../../../../../../common/components/drag_and_drop/draggable_wrapper'; +import { escapeDataProviderId } from '../../../../../../common/components/drag_and_drop/helpers'; +import { ExternalLinkIcon } from '../../../../../../common/components/external_link_icon'; +import { GoogleLink, ReputationLink } from '../../../../../../common/components/links'; +import { Provider } from '../../../data_providers/provider'; import { IS_OPERATOR } from '../../../data_providers/data_provider'; import * as i18n from './translations'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/sort/__snapshots__/sort_indicator.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/body/sort/__snapshots__/sort_indicator.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/sort/__snapshots__/sort_indicator.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/body/sort/__snapshots__/sort_indicator.test.tsx.snap diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/body/sort/index.ts b/x-pack/plugins/siem/public/timelines/components/timeline/body/sort/index.ts new file mode 100644 index 00000000000000..93fbe314e1dad1 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/sort/index.ts @@ -0,0 +1,17 @@ +/* + * 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 { Direction } from '../../../../../graphql/types'; +import { ColumnId } from '../column_id'; + +/** Specifies a column's sort direction */ +export type SortDirection = 'none' | Direction; + +/** Specifies which column the timeline is sorted on */ +export interface Sort { + columnId: ColumnId; + sortDirection: SortDirection; +} diff --git a/x-pack/plugins/siem/public/components/timeline/body/sort/sort_indicator.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/sort/sort_indicator.test.tsx similarity index 96% rename from x-pack/plugins/siem/public/components/timeline/body/sort/sort_indicator.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/sort/sort_indicator.test.tsx index db3e96a4e26502..43738da44b17f2 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/sort/sort_indicator.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/sort/sort_indicator.test.tsx @@ -7,7 +7,7 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; -import { Direction } from '../../../../graphql/types'; +import { Direction } from '../../../../../graphql/types'; import { getDirection, SortIndicator } from './sort_indicator'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/sort/sort_indicator.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/sort/sort_indicator.tsx similarity index 95% rename from x-pack/plugins/siem/public/components/timeline/body/sort/sort_indicator.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/sort/sort_indicator.tsx index 74fb1e5e4034c8..c148e2f6c6295d 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/sort/sort_indicator.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/sort/sort_indicator.tsx @@ -7,7 +7,7 @@ import { EuiIcon } from '@elastic/eui'; import React from 'react'; -import { Direction } from '../../../../graphql/types'; +import { Direction } from '../../../../../graphql/types'; import { SortDirection } from '.'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/stateful_body.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/stateful_body.test.tsx similarity index 96% rename from x-pack/plugins/siem/public/components/timeline/body/stateful_body.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/stateful_body.test.tsx index 4945939ac2bdc4..126f3439f46364 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/stateful_body.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/stateful_body.test.tsx @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mockBrowserFields } from '../../../containers/source/mock'; +import { mockBrowserFields } from '../../../../common/containers/source/mock'; import { defaultHeaders } from './column_headers/default_headers'; import { getColumnHeaders } from './column_headers/helpers'; diff --git a/x-pack/plugins/siem/public/components/timeline/body/stateful_body.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/body/stateful_body.tsx similarity index 95% rename from x-pack/plugins/siem/public/components/timeline/body/stateful_body.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/body/stateful_body.tsx index 76f26d3dda5afc..1aed63ff71d6d4 100644 --- a/x-pack/plugins/siem/public/components/timeline/body/stateful_body.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/body/stateful_body.tsx @@ -10,13 +10,14 @@ import React, { useCallback, useEffect } from 'react'; import { connect, ConnectedProps } from 'react-redux'; import deepEqual from 'fast-deep-equal'; -import { BrowserFields } from '../../../containers/source'; -import { TimelineItem } from '../../../graphql/types'; -import { Note } from '../../../lib/note'; -import { appSelectors, State, timelineSelectors } from '../../../store'; -import { timelineActions, appActions } from '../../../store/actions'; +import { BrowserFields } from '../../../../common/containers/source'; +import { TimelineItem } from '../../../../graphql/types'; +import { Note } from '../../../../common/lib/note'; +import { appSelectors, State } from '../../../../common/store'; +import { appActions } from '../../../../common/store/actions'; import { ColumnHeaderOptions, TimelineModel } from '../../../store/timeline/model'; import { timelineDefaults } from '../../../store/timeline/defaults'; +import { timelineActions, timelineSelectors } from '../../../store/timeline'; import { AddNoteToEvent, UpdateNote } from '../../notes/helpers'; import { OnColumnRemoved, diff --git a/x-pack/plugins/siem/public/components/timeline/body/translations.ts b/x-pack/plugins/siem/public/timelines/components/timeline/body/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/body/translations.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/body/translations.ts diff --git a/x-pack/plugins/siem/public/components/timeline/data_providers/__snapshots__/data_providers.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/__snapshots__/data_providers.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/data_providers/__snapshots__/data_providers.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/data_providers/__snapshots__/data_providers.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/timeline/data_providers/__snapshots__/empty.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/__snapshots__/empty.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/data_providers/__snapshots__/empty.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/data_providers/__snapshots__/empty.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/timeline/data_providers/__snapshots__/provider.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/__snapshots__/provider.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/data_providers/__snapshots__/provider.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/data_providers/__snapshots__/provider.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/timeline/data_providers/__snapshots__/providers.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/__snapshots__/providers.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/data_providers/__snapshots__/providers.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/data_providers/__snapshots__/providers.test.tsx.snap diff --git a/x-pack/plugins/siem/public/components/timeline/data_providers/data_provider.ts b/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/data_provider.ts similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/data_providers/data_provider.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/data_providers/data_provider.ts diff --git a/x-pack/plugins/siem/public/components/timeline/data_providers/data_providers.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/data_providers.test.tsx similarity index 94% rename from x-pack/plugins/siem/public/components/timeline/data_providers/data_providers.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/data_providers/data_providers.test.tsx index b77d37e8e31ab3..54e7cb20aeed30 100644 --- a/x-pack/plugins/siem/public/components/timeline/data_providers/data_providers.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/data_providers.test.tsx @@ -7,8 +7,8 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { TestProviders } from '../../../mock/test_providers'; -import { useMountAppended } from '../../../utils/use_mount_appended'; +import { TestProviders } from '../../../../common/mock/test_providers'; +import { useMountAppended } from '../../../../common/utils/use_mount_appended'; import { DataProviders } from '.'; import { DataProvider } from './data_provider'; diff --git a/x-pack/plugins/siem/public/components/timeline/data_providers/empty.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/empty.test.tsx similarity index 92% rename from x-pack/plugins/siem/public/components/timeline/data_providers/empty.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/data_providers/empty.test.tsx index 10586657b52a3d..9cc5704808c66c 100644 --- a/x-pack/plugins/siem/public/components/timeline/data_providers/empty.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/empty.test.tsx @@ -8,7 +8,7 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; import { Empty } from './empty'; -import { TestProviders } from '../../../mock/test_providers'; +import { TestProviders } from '../../../../common/mock/test_providers'; describe('Empty', () => { describe('rendering', () => { diff --git a/x-pack/plugins/siem/public/components/timeline/data_providers/empty.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/empty.tsx similarity index 98% rename from x-pack/plugins/siem/public/components/timeline/data_providers/empty.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/data_providers/empty.tsx index 1c225eba20b4f9..84f533977a9a30 100644 --- a/x-pack/plugins/siem/public/components/timeline/data_providers/empty.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/empty.tsx @@ -8,7 +8,7 @@ import { EuiBadge, EuiText } from '@elastic/eui'; import React from 'react'; import styled from 'styled-components'; -import { AndOrBadge } from '../../and_or_badge'; +import { AndOrBadge } from '../and_or_badge'; import * as i18n from './translations'; diff --git a/x-pack/plugins/siem/public/components/timeline/data_providers/helpers.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/helpers.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/data_providers/helpers.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/data_providers/helpers.tsx diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/index.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/index.tsx new file mode 100644 index 00000000000000..13c91f25c88004 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/index.tsx @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { rgba } from 'polished'; +import React from 'react'; +import styled from 'styled-components'; + +import { BrowserFields } from '../../../../common/containers/source'; +import { DroppableWrapper } from '../../../../common/components/drag_and_drop/droppable_wrapper'; +import { + droppableTimelineProvidersPrefix, + IS_DRAGGING_CLASS_NAME, +} from '../../../../common/components/drag_and_drop/helpers'; +import { + OnDataProviderEdited, + OnDataProviderRemoved, + OnToggleDataProviderEnabled, + OnToggleDataProviderExcluded, +} from '../events'; +import { TimelineContext } from '../timeline_context'; + +import { DataProvider } from './data_provider'; +import { Empty } from './empty'; +import { Providers } from './providers'; + +interface Props { + browserFields: BrowserFields; + id: string; + dataProviders: DataProvider[]; + onDataProviderEdited: OnDataProviderEdited; + onDataProviderRemoved: OnDataProviderRemoved; + onToggleDataProviderEnabled: OnToggleDataProviderEnabled; + onToggleDataProviderExcluded: OnToggleDataProviderExcluded; + show: boolean; +} + +const DropTargetDataProvidersContainer = styled.div` + padding: 2px 0 4px 0; + + .${IS_DRAGGING_CLASS_NAME} & .drop-target-data-providers { + background: ${({ theme }) => rgba(theme.eui.euiColorSuccess, 0.1)}; + border: 0.2rem dashed ${({ theme }) => theme.eui.euiColorSuccess}; + + & .euiTextColor--subdued { + color: ${({ theme }) => theme.eui.euiColorSuccess}; + } + + & .euiFormHelpText { + color: ${({ theme }) => theme.eui.euiColorSuccess}; + } + } +`; + +const DropTargetDataProviders = styled.div` + position: relative; + border: 0.2rem dashed ${props => props.theme.eui.euiColorMediumShade}; + border-radius: 5px; + margin: 5px 0 5px 0; + min-height: 100px; + overflow-y: auto; + background-color: ${props => props.theme.eui.euiFormBackgroundColor}; +`; + +DropTargetDataProviders.displayName = 'DropTargetDataProviders'; + +const getDroppableId = (id: string): string => `${droppableTimelineProvidersPrefix}${id}`; + +/** + * Renders the data providers section of the timeline. + * + * The data providers section is a drop target where users + * can drag-and drop new data providers into the timeline. + * + * It renders an interactive card representation of the + * data providers. It also provides uniform + * UI controls for the following actions: + * 1) removing a data provider + * 2) temporarily disabling a data provider + * 3) applying boolean negation to the data provider + * + * Given an empty collection of DataProvider[], it prompts + * the user to drop anything with a facet count into + * the data pro section. + */ +export const DataProviders = React.memo( + ({ + browserFields, + id, + dataProviders, + onDataProviderEdited, + onDataProviderRemoved, + onToggleDataProviderEnabled, + onToggleDataProviderExcluded, + show, + }) => { + return ( + + + + {({ isLoading }) => ( + <> + {dataProviders != null && dataProviders.length ? ( + + ) : ( + + + + )} + + )} + + + + ); + } +); + +DataProviders.displayName = 'DataProviders'; diff --git a/x-pack/plugins/siem/public/components/timeline/data_providers/mock/mock_data_providers.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/mock/mock_data_providers.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/data_providers/mock/mock_data_providers.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/data_providers/mock/mock_data_providers.tsx diff --git a/x-pack/plugins/siem/public/components/timeline/data_providers/provider.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/provider.test.tsx similarity index 92% rename from x-pack/plugins/siem/public/components/timeline/data_providers/provider.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/data_providers/provider.test.tsx index f0d7ca83fb391e..d6d337bb3e1d7b 100644 --- a/x-pack/plugins/siem/public/components/timeline/data_providers/provider.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/provider.test.tsx @@ -7,7 +7,7 @@ import { mount, shallow } from 'enzyme'; import React from 'react'; -import { TestProviders } from '../../../mock/test_providers'; +import { TestProviders } from '../../../../common/mock/test_providers'; import { mockDataProviders } from './mock/mock_data_providers'; import { Provider } from './provider'; diff --git a/x-pack/plugins/siem/public/components/timeline/data_providers/provider.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/provider.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/data_providers/provider.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/data_providers/provider.tsx diff --git a/x-pack/plugins/siem/public/components/timeline/data_providers/provider_badge.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/provider_badge.tsx similarity index 95% rename from x-pack/plugins/siem/public/components/timeline/data_providers/provider_badge.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/data_providers/provider_badge.tsx index 859ced39ebc4f4..b3682c0d551475 100644 --- a/x-pack/plugins/siem/public/components/timeline/data_providers/provider_badge.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/provider_badge.tsx @@ -10,8 +10,8 @@ import { isString } from 'lodash/fp'; import React, { useCallback, useMemo } from 'react'; import styled from 'styled-components'; -import { ProviderContainer } from '../../drag_and_drop/provider_container'; -import { getEmptyString } from '../../empty_value'; +import { getEmptyString } from '../../../../common/components/empty_value'; +import { ProviderContainer } from '../../../../common/components/drag_and_drop/provider_container'; import { EXISTS_OPERATOR, QueryOperator } from './data_provider'; diff --git a/x-pack/plugins/siem/public/components/timeline/data_providers/provider_item_actions.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/provider_item_actions.tsx similarity index 98% rename from x-pack/plugins/siem/public/components/timeline/data_providers/provider_item_actions.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/data_providers/provider_item_actions.tsx index 121f832221d3e6..540b1b80259a01 100644 --- a/x-pack/plugins/siem/public/components/timeline/data_providers/provider_item_actions.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/provider_item_actions.tsx @@ -12,7 +12,7 @@ import { import React, { FunctionComponent } from 'react'; import styled from 'styled-components'; -import { BrowserFields } from '../../../containers/source'; +import { BrowserFields } from '../../../../common/containers/source'; import { OnDataProviderEdited } from '../events'; import { QueryOperator, EXISTS_OPERATOR } from './data_provider'; import { StatefulEditDataProvider } from '../../edit_data_provider'; diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/provider_item_and.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/provider_item_and.tsx new file mode 100644 index 00000000000000..171112b28d789c --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/provider_item_and.tsx @@ -0,0 +1,95 @@ +/* + * 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 { EuiFlexItem } from '@elastic/eui'; +import React from 'react'; + +import { AndOrBadge } from '../and_or_badge'; +import { BrowserFields } from '../../../../common/containers/source'; +import { + OnChangeDataProviderKqlQuery, + OnDataProviderEdited, + OnDataProviderRemoved, + OnToggleDataProviderEnabled, + OnToggleDataProviderExcluded, +} from '../events'; + +import { DataProvidersAnd, IS_OPERATOR } from './data_provider'; +import { ProviderItemBadge } from './provider_item_badge'; + +interface ProviderItemAndPopoverProps { + browserFields: BrowserFields; + dataProvidersAnd: DataProvidersAnd[]; + onChangeDataProviderKqlQuery: OnChangeDataProviderKqlQuery; + onDataProviderEdited: OnDataProviderEdited; + onDataProviderRemoved: OnDataProviderRemoved; + onToggleDataProviderEnabled: OnToggleDataProviderEnabled; + onToggleDataProviderExcluded: OnToggleDataProviderExcluded; + providerId: string; + timelineId: string; +} + +export class ProviderItemAnd extends React.PureComponent { + public render() { + const { + browserFields, + dataProvidersAnd, + onDataProviderEdited, + providerId, + timelineId, + } = this.props; + + return dataProvidersAnd.map((providerAnd: DataProvidersAnd, index: number) => ( + + + + + + this.deleteAndProvider(providerId, providerAnd.id)} + field={providerAnd.queryMatch.displayField || providerAnd.queryMatch.field} + kqlQuery={providerAnd.kqlQuery} + isEnabled={providerAnd.enabled} + isExcluded={providerAnd.excluded} + onDataProviderEdited={onDataProviderEdited} + operator={providerAnd.queryMatch.operator || IS_OPERATOR} + providerId={providerId} + timelineId={timelineId} + toggleEnabledProvider={() => + this.toggleEnabledAndProvider(providerId, !providerAnd.enabled, providerAnd.id) + } + toggleExcludedProvider={() => + this.toggleExcludedAndProvider(providerId, !providerAnd.excluded, providerAnd.id) + } + val={providerAnd.queryMatch.displayValue || providerAnd.queryMatch.value} + /> + + + )); + } + + private deleteAndProvider = (providerId: string, andProviderId: string) => { + this.props.onDataProviderRemoved(providerId, andProviderId); + }; + + private toggleEnabledAndProvider = ( + providerId: string, + enabled: boolean, + andProviderId: string + ) => { + this.props.onToggleDataProviderEnabled({ providerId, enabled, andProviderId }); + }; + + private toggleExcludedAndProvider = ( + providerId: string, + excluded: boolean, + andProviderId: string + ) => { + this.props.onToggleDataProviderExcluded({ providerId, excluded, andProviderId }); + }; +} diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/provider_item_and_drag_drop.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/provider_item_and_drag_drop.tsx new file mode 100644 index 00000000000000..4a9358befc51f4 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/provider_item_and_drag_drop.tsx @@ -0,0 +1,136 @@ +/* + * 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 { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; +import { rgba } from 'polished'; +import React, { useCallback } from 'react'; +import styled from 'styled-components'; + +import { AndOrBadge } from '../and_or_badge'; +import { + OnChangeDataProviderKqlQuery, + OnChangeDroppableAndProvider, + OnDataProviderEdited, + OnDataProviderRemoved, + OnToggleDataProviderEnabled, + OnToggleDataProviderExcluded, +} from '../events'; + +import { BrowserFields } from '../../../../common/containers/source'; + +import { DataProvider } from './data_provider'; +import { ProviderItemAnd } from './provider_item_and'; + +import * as i18n from './translations'; + +const DropAndTargetDataProvidersContainer = styled(EuiFlexItem)` + margin: 0px 8px; +`; + +DropAndTargetDataProvidersContainer.displayName = 'DropAndTargetDataProvidersContainer'; + +const DropAndTargetDataProviders = styled.div<{ hasAndItem: boolean }>` + min-width: 230px; + width: auto; + border: 0.1rem dashed ${props => props.theme.eui.euiColorSuccess}; + border-radius: 5px; + text-align: center; + padding: 3px 10px; + display: flex; + justify-content: center; + align-items: center; + ${props => + props.hasAndItem + ? `&:hover { + transition: background-color 0.7s ease; + background-color: ${() => rgba(props.theme.eui.euiColorSuccess, 0.2)}; + }` + : ''}; + cursor: ${({ hasAndItem }) => (!hasAndItem ? `default` : 'inherit')}; +`; + +DropAndTargetDataProviders.displayName = 'DropAndTargetDataProviders'; + +const NumberProviderAndBadge = (styled(EuiBadge)` + margin: 0px 5px; +` as unknown) as typeof EuiBadge; + +NumberProviderAndBadge.displayName = 'NumberProviderAndBadge'; + +interface ProviderItemDropProps { + browserFields: BrowserFields; + dataProvider: DataProvider; + mousePosition?: { x: number; y: number; boundLeft: number; boundTop: number }; + onChangeDataProviderKqlQuery: OnChangeDataProviderKqlQuery; + onChangeDroppableAndProvider: OnChangeDroppableAndProvider; + onDataProviderEdited: OnDataProviderEdited; + onDataProviderRemoved: OnDataProviderRemoved; + onToggleDataProviderEnabled: OnToggleDataProviderEnabled; + onToggleDataProviderExcluded: OnToggleDataProviderExcluded; + timelineId: string; +} + +export const ProviderItemAndDragDrop = React.memo( + ({ + browserFields, + dataProvider, + onChangeDataProviderKqlQuery, + onChangeDroppableAndProvider, + onDataProviderEdited, + onDataProviderRemoved, + onToggleDataProviderEnabled, + onToggleDataProviderExcluded, + timelineId, + }) => { + const onMouseEnter = useCallback(() => onChangeDroppableAndProvider(dataProvider.id), [ + onChangeDroppableAndProvider, + dataProvider.id, + ]); + const onMouseLeave = useCallback(() => onChangeDroppableAndProvider(''), [ + onChangeDroppableAndProvider, + ]); + const hasAndItem = dataProvider.and.length > 0; + return ( + + + + {hasAndItem && ( + + {dataProvider.and.length} + + )} + + {i18n.DROP_HERE_TO_ADD_AN} + + + + + + + ); + } +); + +ProviderItemAndDragDrop.displayName = 'ProviderItemAndDragDrop'; diff --git a/x-pack/plugins/siem/public/components/timeline/data_providers/provider_item_badge.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/provider_item_badge.tsx similarity index 96% rename from x-pack/plugins/siem/public/components/timeline/data_providers/provider_item_badge.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/data_providers/provider_item_badge.tsx index b268315efb919d..b53c08a8bb10db 100644 --- a/x-pack/plugins/siem/public/components/timeline/data_providers/provider_item_badge.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/provider_item_badge.tsx @@ -8,13 +8,13 @@ import { noop } from 'lodash/fp'; import React, { useCallback, useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; -import { BrowserFields } from '../../../containers/source'; +import { BrowserFields } from '../../../../common/containers/source'; import { OnDataProviderEdited } from '../events'; import { ProviderBadge } from './provider_badge'; import { ProviderItemActions } from './provider_item_actions'; import { DataProvidersAnd, QueryOperator } from './data_provider'; -import { dragAndDropActions } from '../../../store/drag_and_drop'; +import { dragAndDropActions } from '../../../../common/store/drag_and_drop'; import { TimelineContext } from '../timeline_context'; interface ProviderItemBadgeProps { diff --git a/x-pack/plugins/siem/public/components/timeline/data_providers/providers.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/providers.test.tsx similarity index 98% rename from x-pack/plugins/siem/public/components/timeline/data_providers/providers.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/data_providers/providers.test.tsx index 43e84bac508ea0..34202d090e06f7 100644 --- a/x-pack/plugins/siem/public/components/timeline/data_providers/providers.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/providers.test.tsx @@ -7,16 +7,16 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { createKibanaCoreStartMock } from '../../../mock/kibana_core'; -import { TestProviders } from '../../../mock/test_providers'; -import { DroppableWrapper } from '../../drag_and_drop/droppable_wrapper'; -import { FilterManager } from '../../../../../../../src/plugins/data/public'; +import { createKibanaCoreStartMock } from '../../../../common/mock/kibana_core'; +import { TestProviders } from '../../../../common/mock/test_providers'; +import { DroppableWrapper } from '../../../../common/components/drag_and_drop/droppable_wrapper'; +import { FilterManager } from '../../../../../../../../src/plugins/data/public'; import { TimelineContext } from '../timeline_context'; import { mockDataProviders } from './mock/mock_data_providers'; import { Providers } from './providers'; import { DELETE_CLASS_NAME, ENABLE_CLASS_NAME, EXCLUDE_CLASS_NAME } from './provider_item_actions'; -import { useMountAppended } from '../../../utils/use_mount_appended'; +import { useMountAppended } from '../../../../common/utils/use_mount_appended'; const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; diff --git a/x-pack/plugins/siem/public/components/timeline/data_providers/providers.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/providers.tsx similarity index 98% rename from x-pack/plugins/siem/public/components/timeline/data_providers/providers.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/data_providers/providers.tsx index 8d9d0c69d53cd2..7f10a7b16c7b27 100644 --- a/x-pack/plugins/siem/public/components/timeline/data_providers/providers.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/providers.tsx @@ -10,13 +10,13 @@ import React, { useMemo } from 'react'; import { Draggable, DraggingStyle, Droppable, NotDraggingStyle } from 'react-beautiful-dnd'; import styled, { css } from 'styled-components'; -import { AndOrBadge } from '../../and_or_badge'; -import { BrowserFields } from '../../../containers/source'; +import { AndOrBadge } from '../and_or_badge'; +import { BrowserFields } from '../../../../common/containers/source'; import { getTimelineProviderDroppableId, IS_DRAGGING_CLASS_NAME, getTimelineProviderDraggableId, -} from '../../drag_and_drop/helpers'; +} from '../../../../common/components/drag_and_drop/helpers'; import { OnDataProviderEdited, OnDataProviderRemoved, diff --git a/x-pack/plugins/siem/public/components/timeline/data_providers/translations.ts b/x-pack/plugins/siem/public/timelines/components/timeline/data_providers/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/data_providers/translations.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/data_providers/translations.ts diff --git a/x-pack/plugins/siem/public/components/timeline/events.ts b/x-pack/plugins/siem/public/timelines/components/timeline/events.ts similarity index 97% rename from x-pack/plugins/siem/public/components/timeline/events.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/events.ts index f977c03ed3053a..6c9a9b8b896797 100644 --- a/x-pack/plugins/siem/public/components/timeline/events.ts +++ b/x-pack/plugins/siem/public/timelines/components/timeline/events.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ColumnHeaderOptions } from '../../store/timeline/model'; +import { ColumnHeaderOptions } from '../../../timelines/store/timeline/model'; import { ColumnId } from './body/column_id'; import { SortDirection } from './body/sort'; import { QueryOperator } from './data_providers/data_provider'; diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/expandable_event/index.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/expandable_event/index.tsx new file mode 100644 index 00000000000000..b08c6afcaf4a69 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/expandable_event/index.tsx @@ -0,0 +1,74 @@ +/* + * 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 styled from 'styled-components'; + +import { BrowserFields } from '../../../../common/containers/source'; +import { ColumnHeaderOptions } from '../../../../timelines/store/timeline/model'; +import { DetailItem } from '../../../../graphql/types'; +import { StatefulEventDetails } from '../../../../common/components/event_details/stateful_event_details'; +import { LazyAccordion } from '../../lazy_accordion'; +import { OnUpdateColumns } from '../events'; + +const ExpandableDetails = styled.div<{ hideExpandButton: boolean }>` + ${({ hideExpandButton }) => + hideExpandButton + ? ` + .euiAccordion__button { + display: none; + } + ` + : ''}; +`; + +ExpandableDetails.displayName = 'ExpandableDetails'; + +interface Props { + browserFields: BrowserFields; + columnHeaders: ColumnHeaderOptions[]; + id: string; + event: DetailItem[]; + forceExpand?: boolean; + hideExpandButton?: boolean; + onUpdateColumns: OnUpdateColumns; + timelineId: string; + toggleColumn: (column: ColumnHeaderOptions) => void; +} + +export const ExpandableEvent = React.memo( + ({ + browserFields, + columnHeaders, + event, + forceExpand = false, + id, + timelineId, + toggleColumn, + onUpdateColumns, + }) => ( + + ( + + )} + forceExpand={forceExpand} + paddingSize="none" + /> + + ) +); + +ExpandableEvent.displayName = 'ExpandableEvent'; diff --git a/x-pack/plugins/siem/public/components/timeline/expandable_event/translations.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/expandable_event/translations.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/expandable_event/translations.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/expandable_event/translations.tsx diff --git a/x-pack/plugins/siem/public/components/timeline/fetch_kql_timeline.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/fetch_kql_timeline.tsx similarity index 88% rename from x-pack/plugins/siem/public/components/timeline/fetch_kql_timeline.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/fetch_kql_timeline.tsx index 16eaa803082050..e75f87e0d6011a 100644 --- a/x-pack/plugins/siem/public/components/timeline/fetch_kql_timeline.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/fetch_kql_timeline.tsx @@ -9,11 +9,11 @@ import { connect, ConnectedProps } from 'react-redux'; import deepEqual from 'fast-deep-equal'; import { IIndexPattern } from 'src/plugins/data/public'; -import { timelineSelectors, State } from '../../store'; -import { inputsActions } from '../../store/actions'; -import { InputsModelId } from '../../store/inputs/constants'; -import { useUpdateKql } from '../../utils/kql/use_update_kql'; - +import { State } from '../../../common/store'; +import { inputsActions } from '../../../common/store/actions'; +import { InputsModelId } from '../../../common/store/inputs/constants'; +import { useUpdateKql } from '../../../common/utils/kql/use_update_kql'; +import { timelineSelectors } from '../../store/timeline'; export interface TimelineKqlFetchProps { id: string; indexPattern: IIndexPattern; diff --git a/x-pack/plugins/siem/public/components/timeline/footer/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/footer/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/footer/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/footer/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/footer/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/footer/index.test.tsx new file mode 100644 index 00000000000000..86b362aefca1a8 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/footer/index.test.tsx @@ -0,0 +1,287 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mount, shallow } from 'enzyme'; +import { getOr } from 'lodash/fp'; +import React from 'react'; + +import { TestProviders } from '../../../../common/mock/test_providers'; + +import { FooterComponent, PagingControlComponent } from './index'; +import { mockData } from './mock'; + +describe('Footer Timeline Component', () => { + const loadMore = jest.fn(); + const onChangeItemsPerPage = jest.fn(); + const getUpdatedAt = () => 1546878704036; + + describe('rendering', () => { + test('it renders the default timeline footer', () => { + const wrapper = shallow( + + ); + + expect(wrapper).toMatchSnapshot(); + }); + + test('it renders the loading panel at the beginning ', () => { + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="LoadingPanelTimeline"]').exists()).toBeTruthy(); + }); + + test('it renders the loadMore button if need to fetch more', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="TimelineMoreButton"]').exists()).toBeTruthy(); + }); + + test('it renders the Loading... in the more load button when fetching new data', () => { + const wrapper = shallow( + + ); + + const loadButton = wrapper + .find('[data-test-subj="TimelineMoreButton"]') + .dive() + .text(); + expect(wrapper.find('[data-test-subj="LoadingPanelTimeline"]').exists()).toBeFalsy(); + expect(loadButton).toContain('Loading...'); + }); + + test('it renders the Load More in the more load button when fetching new data', () => { + const wrapper = shallow( + + ); + + const loadButton = wrapper + .find('[data-test-subj="TimelineMoreButton"]') + .dive() + .text(); + expect(loadButton).toContain('Load more'); + }); + + test('it does NOT render the loadMore button because there is nothing else to fetch', () => { + const wrapper = mount( + + ); + + expect(wrapper.find('[data-test-subj="TimelineMoreButton"]').exists()).toBeFalsy(); + }); + + test('it render popover to select new itemsPerPage in timeline', () => { + const wrapper = mount( + + + + ); + + wrapper + .find('[data-test-subj="timelineSizeRowPopover"] button') + .first() + .simulate('click'); + expect(wrapper.find('[data-test-subj="timelinePickSizeRow"]').exists()).toBeTruthy(); + }); + }); + + describe('Events', () => { + test('should call loadmore when clicking on the button load more', () => { + const wrapper = mount( + + + + ); + + wrapper + .find('[data-test-subj="TimelineMoreButton"]') + .first() + .simulate('click'); + + expect(loadMore).toBeCalled(); + }); + + test('Should call onChangeItemsPerPage when you pick a new limit', () => { + const wrapper = mount( + + + + ); + + wrapper + .find('[data-test-subj="timelineSizeRowPopover"] button') + .first() + .simulate('click'); + wrapper.update(); + wrapper + .find('[data-test-subj="timelinePickSizeRow"] button') + .first() + .simulate('click'); + expect(onChangeItemsPerPage).toBeCalled(); + }); + + test('it does render the auto-refresh message instead of load more button when stream live is on', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="paging-control"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="is-live-on-message"]').exists()).toBeTruthy(); + }); + + test('it does render the load more button when stream live is off', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="paging-control"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="is-live-on-message"]').exists()).toBeFalsy(); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/footer/index.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/footer/index.tsx new file mode 100644 index 00000000000000..556f7b043e3ab5 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/footer/index.tsx @@ -0,0 +1,368 @@ +/* + * 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 { + EuiBadge, + EuiButton, + EuiButtonEmpty, + EuiContextMenuItem, + EuiContextMenuPanel, + EuiFlexGroup, + EuiFlexItem, + EuiIconTip, + EuiPopover, + EuiText, + EuiToolTip, + EuiPopoverProps, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { FC, useCallback, useEffect, useState, useMemo } from 'react'; +import styled from 'styled-components'; + +import { LoadingPanel } from '../../loading'; +import { OnChangeItemsPerPage, OnLoadMore } from '../events'; + +import { LastUpdatedAt } from './last_updated'; +import * as i18n from './translations'; +import { useTimelineTypeContext } from '../timeline_context'; +import { useEventDetailsWidthContext } from '../../../../common/components/events_viewer/event_details_width_context'; + +export const isCompactFooter = (width: number): boolean => width < 600; + +interface FixedWidthLastUpdatedContainerProps { + updatedAt: number; +} + +const FixedWidthLastUpdatedContainer = React.memo( + ({ updatedAt }) => { + const width = useEventDetailsWidthContext(); + const compact = useMemo(() => isCompactFooter(width), [width]); + + return ( + + + + ); + } +); + +FixedWidthLastUpdatedContainer.displayName = 'FixedWidthLastUpdatedContainer'; + +const FixedWidthLastUpdated = styled.div<{ compact?: boolean }>` + width: ${({ compact }) => (!compact ? 200 : 25)}px; + overflow: hidden; + text-align: end; +`; + +FixedWidthLastUpdated.displayName = 'FixedWidthLastUpdated'; + +interface HeightProp { + height: number; +} + +const FooterContainer = styled(EuiFlexGroup).attrs(({ height }) => ({ + style: { + height: `${height}px`, + }, +}))` + flex: 0; +`; + +FooterContainer.displayName = 'FooterContainer'; + +const FooterFlexGroup = styled(EuiFlexGroup)` + height: 35px; + width: 100%; +`; + +FooterFlexGroup.displayName = 'FooterFlexGroup'; + +const LoadingPanelContainer = styled.div` + padding-top: 3px; +`; + +LoadingPanelContainer.displayName = 'LoadingPanelContainer'; + +const PopoverRowItems = styled((EuiPopover as unknown) as FC)< + EuiPopoverProps & { + className?: string; + id?: string; + } +>` + .euiButtonEmpty__content { + padding: 0px 0px; + } +`; + +PopoverRowItems.displayName = 'PopoverRowItems'; + +export const ServerSideEventCount = styled.div` + margin: 0 5px 0 5px; +`; + +ServerSideEventCount.displayName = 'ServerSideEventCount'; + +/** The height of the footer, exported for use in height calculations */ +export const footerHeight = 40; // px + +/** Displays the server-side count of events */ +export const EventsCountComponent = ({ + closePopover, + isOpen, + items, + itemsCount, + onClick, + serverSideEventCount, +}: { + closePopover: () => void; + isOpen: boolean; + items: React.ReactElement[]; + itemsCount: number; + onClick: () => void; + serverSideEventCount: number; +}) => { + const timelineTypeContext = useTimelineTypeContext(); + return ( +
+ + + {itemsCount} + + + {` ${i18n.OF} `} + + } + isOpen={isOpen} + closePopover={closePopover} + panelPaddingSize="none" + > + + + + + + {serverSideEventCount} + {' '} + {timelineTypeContext.documentType ?? i18n.EVENTS} + + +
+ ); +}; + +EventsCountComponent.displayName = 'EventsCountComponent'; + +export const EventsCount = React.memo(EventsCountComponent); + +EventsCount.displayName = 'EventsCount'; + +export const PagingControlComponent = ({ + hasNextPage, + isLoading, + loadMore, +}: { + hasNextPage: boolean; + isLoading: boolean; + loadMore: () => void; +}) => ( + <> + {hasNextPage && ( + + {isLoading ? `${i18n.LOADING}...` : i18n.LOAD_MORE} + + )} + +); + +PagingControlComponent.displayName = 'PagingControlComponent'; + +export const PagingControl = React.memo(PagingControlComponent); + +PagingControl.displayName = 'PagingControl'; + +interface FooterProps { + getUpdatedAt: () => number; + hasNextPage: boolean; + height: number; + isLive: boolean; + isLoading: boolean; + itemsCount: number; + itemsPerPage: number; + itemsPerPageOptions: number[]; + nextCursor: string; + onChangeItemsPerPage: OnChangeItemsPerPage; + onLoadMore: OnLoadMore; + serverSideEventCount: number; + tieBreaker: string; +} + +/** Renders a loading indicator and paging controls */ +export const FooterComponent = ({ + getUpdatedAt, + hasNextPage, + height, + isLive, + isLoading, + itemsCount, + itemsPerPage, + itemsPerPageOptions, + nextCursor, + onChangeItemsPerPage, + onLoadMore, + serverSideEventCount, + tieBreaker, +}: FooterProps) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [paginationLoading, setPaginationLoading] = useState(false); + const [updatedAt, setUpdatedAt] = useState(null); + const timelineTypeContext = useTimelineTypeContext(); + + const loadMore = useCallback(() => { + setPaginationLoading(true); + onLoadMore(nextCursor, tieBreaker); + }, [nextCursor, tieBreaker, onLoadMore, setPaginationLoading]); + + const onButtonClick = useCallback(() => setIsPopoverOpen(!isPopoverOpen), [ + isPopoverOpen, + setIsPopoverOpen, + ]); + const closePopover = useCallback(() => setIsPopoverOpen(false), [setIsPopoverOpen]); + + useEffect(() => { + if (paginationLoading && !isLoading) { + setPaginationLoading(false); + setUpdatedAt(getUpdatedAt()); + } + + if (updatedAt === null || !isLoading) { + setUpdatedAt(getUpdatedAt()); + } + }, [isLoading]); + + if (isLoading && !paginationLoading) { + return ( + + + + ); + } + + const rowItems = + itemsPerPageOptions && + itemsPerPageOptions.map(item => ( + { + closePopover(); + onChangeItemsPerPage(item); + }} + > + {`${item} ${i18n.ROWS}`} + + )); + + return ( + + + + + + + + + + {isLive ? ( + + + {i18n.AUTO_REFRESH_ACTIVE}{' '} + + } + type="iInCircle" + /> + + + ) : ( + + )} + + + + + + + + ); +}; + +FooterComponent.displayName = 'FooterComponent'; + +export const Footer = React.memo(FooterComponent); + +Footer.displayName = 'Footer'; diff --git a/x-pack/plugins/siem/public/components/timeline/footer/last_updated.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/footer/last_updated.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/footer/last_updated.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/footer/last_updated.tsx diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/footer/mock.ts b/x-pack/plugins/siem/public/timelines/components/timeline/footer/mock.ts new file mode 100644 index 00000000000000..fcd30ee2b8500b --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/footer/mock.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 { EventsTimelineData } from '../../../../graphql/types'; + +export const mockData: { Events: EventsTimelineData } = { + Events: { + totalCount: 15546, + pageInfo: { + hasNextPage: true, + endCursor: { + value: '1546878704036', + tiebreaker: '10624', + }, + }, + edges: [ + { + cursor: { + value: '1546878704036', + tiebreaker: '10656', + }, + node: { + _id: 'Fo8nKWgBiyhPd5Zo3cib', + timestamp: '2019-01-07T16:31:44.036Z', + _index: 'auditbeat-7.0.0-2019.01.07', + destination: { + ip: ['24.168.54.169'], + port: [62123], + }, + event: { + category: null, + id: null, + module: ['system'], + severity: null, + type: null, + }, + geo: null, + host: { + name: ['siem-general'], + ip: null, + }, + source: { + ip: ['10.142.0.6'], + port: [9200], + }, + suricata: null, + }, + }, + { + cursor: { + value: '1546878704036', + tiebreaker: '10624', + }, + node: { + _id: 'F48nKWgBiyhPd5Zo3cib', + timestamp: '2019-01-07T16:31:44.036Z', + _index: 'auditbeat-7.0.0-2019.01.07', + destination: { + ip: ['24.168.54.169'], + port: [62145], + }, + event: { + category: null, + id: null, + module: ['system'], + severity: null, + type: null, + }, + geo: null, + host: { + name: ['siem-general'], + ip: null, + }, + source: { + ip: ['10.142.0.6'], + port: [9200], + }, + suricata: null, + }, + }, + ], + }, +}; diff --git a/x-pack/plugins/siem/public/components/timeline/footer/translations.ts b/x-pack/plugins/siem/public/timelines/components/timeline/footer/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/footer/translations.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/footer/translations.ts diff --git a/x-pack/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/header/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/header/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/header/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/header/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/header/index.test.tsx new file mode 100644 index 00000000000000..a3855c848cf24e --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/header/index.test.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { shallow } from 'enzyme'; +import React from 'react'; + +import { mockIndexPattern } from '../../../../common/mock'; +import { createKibanaCoreStartMock } from '../../../../common/mock/kibana_core'; +import { TestProviders } from '../../../../common/mock/test_providers'; +import { FilterManager } from '../../../../../../../../src/plugins/data/public'; +import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; +import { useMountAppended } from '../../../../common/utils/use_mount_appended'; + +import { TimelineHeader } from '.'; + +const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; + +jest.mock('../../../../common/lib/kibana'); + +describe('Header', () => { + const indexPattern = mockIndexPattern; + const mount = useMountAppended(); + + describe('rendering', () => { + test('renders correctly against snapshot', () => { + const wrapper = shallow( + + ); + expect(wrapper).toMatchSnapshot(); + }); + + test('it renders the data providers', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="dataProviders"]').exists()).toEqual(true); + }); + + test('it renders the unauthorized call out providers', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="timelineCallOutUnauthorized"]').exists()).toEqual(true); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/header/index.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/header/index.tsx new file mode 100644 index 00000000000000..974b23bedac013 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/header/index.tsx @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiCallOut } from '@elastic/eui'; +import React from 'react'; +import { FilterManager, IIndexPattern } from 'src/plugins/data/public'; +import deepEqual from 'fast-deep-equal'; + +import { DataProviders } from '../data_providers'; +import { DataProvider } from '../data_providers/data_provider'; +import { + OnDataProviderEdited, + OnDataProviderRemoved, + OnToggleDataProviderEnabled, + OnToggleDataProviderExcluded, +} from '../events'; +import { StatefulSearchOrFilter } from '../search_or_filter'; +import { BrowserFields } from '../../../../common/containers/source'; + +import * as i18n from './translations'; + +interface Props { + browserFields: BrowserFields; + dataProviders: DataProvider[]; + filterManager: FilterManager; + id: string; + indexPattern: IIndexPattern; + onDataProviderEdited: OnDataProviderEdited; + onDataProviderRemoved: OnDataProviderRemoved; + onToggleDataProviderEnabled: OnToggleDataProviderEnabled; + onToggleDataProviderExcluded: OnToggleDataProviderExcluded; + show: boolean; + showCallOutUnauthorizedMsg: boolean; +} + +const TimelineHeaderComponent: React.FC = ({ + browserFields, + id, + indexPattern, + dataProviders, + filterManager, + onDataProviderEdited, + onDataProviderRemoved, + onToggleDataProviderEnabled, + onToggleDataProviderExcluded, + show, + showCallOutUnauthorizedMsg, +}) => ( + <> + {showCallOutUnauthorizedMsg && ( + + )} + {show && ( + + )} + + + +); + +export const TimelineHeader = React.memo( + TimelineHeaderComponent, + (prevProps, nextProps) => + deepEqual(prevProps.browserFields, nextProps.browserFields) && + prevProps.id === nextProps.id && + deepEqual(prevProps.indexPattern, nextProps.indexPattern) && + deepEqual(prevProps.dataProviders, nextProps.dataProviders) && + prevProps.filterManager === nextProps.filterManager && + prevProps.onDataProviderEdited === nextProps.onDataProviderEdited && + prevProps.onDataProviderRemoved === nextProps.onDataProviderRemoved && + prevProps.onToggleDataProviderEnabled === nextProps.onToggleDataProviderEnabled && + prevProps.onToggleDataProviderExcluded === nextProps.onToggleDataProviderExcluded && + prevProps.show === nextProps.show && + prevProps.showCallOutUnauthorizedMsg === nextProps.showCallOutUnauthorizedMsg +); diff --git a/x-pack/plugins/siem/public/components/timeline/header/translations.ts b/x-pack/plugins/siem/public/timelines/components/timeline/header/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/header/translations.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/header/translations.ts diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/helpers.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/helpers.test.tsx new file mode 100644 index 00000000000000..87eb9cc45b98b2 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/helpers.test.tsx @@ -0,0 +1,398 @@ +/* + * 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 { cloneDeep } from 'lodash/fp'; +import { mockIndexPattern } from '../../../common/mock'; + +import { mockDataProviders } from './data_providers/mock/mock_data_providers'; +import { buildGlobalQuery, combineQueries } from './helpers'; +import { mockBrowserFields } from '../../../common/containers/source/mock'; +import { EsQueryConfig, Filter, esFilters } from '../../../../../../../src/plugins/data/public'; + +const cleanUpKqlQuery = (str: string) => str.replace(/\n/g, '').replace(/\s\s+/g, ' '); +const startDate = new Date('2018-03-23T18:49:23.132Z').valueOf(); +const endDate = new Date('2018-03-24T03:33:52.253Z').valueOf(); + +describe('Build KQL Query', () => { + test('Build KQL query with one data provider', () => { + const dataProviders = mockDataProviders.slice(0, 1); + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1"'); + }); + + test('Build KQL query with one data provider as timestamp (string input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = '@timestamp'; + dataProviders[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('@timestamp: 1521848183232'); + }); + + test('Buld KQL query with one data provider as timestamp (numeric input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = '@timestamp'; + dataProviders[0].queryMatch.value = 1521848183232; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('@timestamp: 1521848183232'); + }); + + test('Build KQL query with one data provider as date type (string input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = 'event.end'; + dataProviders[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('event.end: 1521848183232'); + }); + + test('Buld KQL query with one data provider as date type (numeric input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = 'event.end'; + dataProviders[0].queryMatch.value = 1521848183232; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('event.end: 1521848183232'); + }); + + test('Build KQL query with two data provider', () => { + const dataProviders = mockDataProviders.slice(0, 2); + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('(name : "Provider 1") or (name : "Provider 2" )'); + }); + + test('Build KQL query with one data provider and one and', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].and = mockDataProviders.slice(1, 2); + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and name : "Provider 2"'); + }); + + test('Build KQL query with one data provider and one and as timestamp (string input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); + dataProviders[0].and[0].queryMatch.field = '@timestamp'; + dataProviders[0].and[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and @timestamp: 1521848183232'); + }); + + test('Build KQL query with one data provider and one and as timestamp (numeric input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); + dataProviders[0].and[0].queryMatch.field = '@timestamp'; + dataProviders[0].and[0].queryMatch.value = 1521848183232; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and @timestamp: 1521848183232'); + }); + + test('Build KQL query with one data provider and one and as date type (string input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); + dataProviders[0].and[0].queryMatch.field = 'event.end'; + dataProviders[0].and[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and event.end: 1521848183232'); + }); + + test('Build KQL query with one data provider and one and as date type (numeric input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].and = cloneDeep(mockDataProviders.slice(1, 2)); + dataProviders[0].and[0].queryMatch.field = 'event.end'; + dataProviders[0].and[0].queryMatch.value = 1521848183232; + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual('name : "Provider 1" and event.end: 1521848183232'); + }); + + test('Build KQL query with two data provider and multiple and', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); + dataProviders[0].and = mockDataProviders.slice(2, 4); + dataProviders[1].and = mockDataProviders.slice(4, 5); + const kqlQuery = buildGlobalQuery(dataProviders, mockBrowserFields); + expect(cleanUpKqlQuery(kqlQuery)).toEqual( + '(name : "Provider 1" and name : "Provider 3" and name : "Provider 4") or (name : "Provider 2" and name : "Provider 5")' + ); + }); +}); + +describe('Combined Queries', () => { + const config: EsQueryConfig = { + allowLeadingWildcards: true, + queryStringOptions: {}, + ignoreFilterIfFieldNotInIndex: true, + dateFormatTZ: 'America/New_York', + }; + test('No Data Provider & No kqlQuery & and isEventViewer is false', () => { + expect( + combineQueries({ + config, + dataProviders: [], + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'search', + start: startDate, + end: endDate, + }) + ).toBeNull(); + }); + + test('No Data Provider & No kqlQuery & isEventViewer is true', () => { + const isEventViewer = true; + expect( + combineQueries({ + config, + dataProviders: [], + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'search', + start: startDate, + end: endDate, + isEventViewer, + }) + ).toEqual({ + filterQuery: + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}', + }); + }); + + test('No Data Provider & No kqlQuery & with Filters', () => { + const isEventViewer = true; + expect( + combineQueries({ + config, + dataProviders: [], + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [ + { + $state: { store: esFilters.FilterStateStore.APP_STATE }, + meta: { + alias: null, + disabled: false, + key: 'event.category', + negate: false, + params: { query: 'file' }, + type: 'phrase', + }, + query: { match_phrase: { 'event.category': 'file' } }, + }, + { + $state: { store: esFilters.FilterStateStore.APP_STATE }, + meta: { + alias: null, + disabled: false, + key: 'host.name', + negate: false, + type: 'exists', + value: 'exists', + }, + exists: { field: 'host.name' }, + } as Filter, + ], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'search', + start: startDate, + end: endDate, + isEventViewer, + }) + ).toEqual({ + filterQuery: + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}},{"exists":{"field":"host.name"}}],"should":[],"must_not":[]}}', + }); + }); + + test('Only Data Provider', () => { + const dataProviders = mockDataProviders.slice(0, 1); + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'search', + start: startDate, + end: endDate, + })!; + expect(filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + ); + }); + + test('Only Data Provider with timestamp (string input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = '@timestamp'; + dataProviders[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'search', + start: startDate, + end: endDate, + })!; + expect(filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521848183232,"lte":1521848183232}}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + ); + }); + + test('Only Data Provider with timestamp (numeric input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = '@timestamp'; + dataProviders[0].queryMatch.value = 1521848183232; + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'search', + start: startDate, + end: endDate, + })!; + expect(filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521848183232,"lte":1521848183232}}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + ); + }); + + test('Only Data Provider with a date type (string input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = 'event.end'; + dataProviders[0].queryMatch.value = '2018-03-23T23:36:23.232Z'; + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'search', + start: startDate, + end: endDate, + })!; + expect(filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match":{"event.end":1521848183232}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + ); + }); + + test('Only Data Provider with date type (numeric input)', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 1)); + dataProviders[0].queryMatch.field = 'event.end'; + dataProviders[0].queryMatch.value = 1521848183232; + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: '', language: 'kuery' }, + kqlMode: 'search', + start: startDate, + end: endDate, + })!; + expect(filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match":{"event.end":1521848183232}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + ); + }); + + test('Only KQL search/filter query', () => { + const { filterQuery } = combineQueries({ + config, + dataProviders: [], + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, + kqlMode: 'search', + start: startDate, + end: endDate, + })!; + expect(filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + ); + }); + + test('Data Provider & KQL search query', () => { + const dataProviders = mockDataProviders.slice(0, 1); + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, + kqlMode: 'search', + start: startDate, + end: endDate, + })!; + expect(filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + ); + }); + + test('Data Provider & KQL filter query', () => { + const dataProviders = mockDataProviders.slice(0, 1); + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, + kqlMode: 'filter', + start: startDate, + end: endDate, + })!; + expect(filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}]}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + ); + }); + + test('Data Provider & KQL search query multiple', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); + dataProviders[0].and = mockDataProviders.slice(2, 4); + dataProviders[1].and = mockDataProviders.slice(4, 5); + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, + kqlMode: 'search', + start: startDate, + end: endDate, + })!; + expect(filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"bool":{"should":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 3"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 4"}}],"minimum_should_match":1}}]}}]}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 2"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 5"}}],"minimum_should_match":1}}]}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + ); + }); + + test('Data Provider & KQL filter query multiple', () => { + const dataProviders = cloneDeep(mockDataProviders.slice(0, 2)); + dataProviders[0].and = mockDataProviders.slice(2, 4); + dataProviders[1].and = mockDataProviders.slice(4, 5); + const { filterQuery } = combineQueries({ + config, + dataProviders, + indexPattern: mockIndexPattern, + browserFields: mockBrowserFields, + filters: [], + kqlQuery: { query: 'host.name: "host-1"', language: 'kuery' }, + kqlMode: 'filter', + start: startDate, + end: endDate, + })!; + expect(filterQuery).toEqual( + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"filter":[{"bool":{"should":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 3"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 4"}}],"minimum_should_match":1}}]}}]}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"name":"Provider 2"}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"name":"Provider 5"}}],"minimum_should_match":1}}]}}],"minimum_should_match":1}},{"bool":{"should":[{"match_phrase":{"host.name":"host-1"}}],"minimum_should_match":1}}]}},{"bool":{"filter":[{"bool":{"should":[{"range":{"@timestamp":{"gte":1521830963132}}}],"minimum_should_match":1}},{"bool":{"should":[{"range":{"@timestamp":{"lte":1521862432253}}}],"minimum_should_match":1}}]}}]}}],"should":[],"must_not":[]}}' + ); + }); +}); diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/helpers.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/helpers.tsx new file mode 100644 index 00000000000000..776ff114734d90 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/helpers.tsx @@ -0,0 +1,160 @@ +/* + * 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 { isEmpty, isNumber, get } from 'lodash/fp'; +import memoizeOne from 'memoize-one'; + +import { escapeQueryValue, convertToBuildEsQuery } from '../../../common/lib/keury'; + +import { DataProvider, DataProvidersAnd, EXISTS_OPERATOR } from './data_providers/data_provider'; +import { BrowserFields } from '../../../common/containers/source'; +import { + IIndexPattern, + Query, + EsQueryConfig, + Filter, +} from '../../../../../../../src/plugins/data/public'; + +const convertDateFieldToQuery = (field: string, value: string | number) => + `${field}: ${isNumber(value) ? value : new Date(value).valueOf()}`; + +const getBaseFields = memoizeOne((browserFields: BrowserFields): string[] => { + const baseFields = get('base', browserFields); + if (baseFields != null && baseFields.fields != null) { + return Object.keys(baseFields.fields); + } + return []; +}); + +const getBrowserFieldPath = (field: string, browserFields: BrowserFields) => { + const splitFields = field.split('.'); + const baseFields = getBaseFields(browserFields); + if (baseFields.includes(field)) { + return ['base', 'fields', field]; + } + return [splitFields[0], 'fields', field]; +}; + +const checkIfFieldTypeIsDate = (field: string, browserFields: BrowserFields) => { + const pathBrowserField = getBrowserFieldPath(field, browserFields); + const browserField = get(pathBrowserField, browserFields); + if (browserField != null && browserField.type === 'date') { + return true; + } + return false; +}; + +const buildQueryMatch = ( + dataProvider: DataProvider | DataProvidersAnd, + browserFields: BrowserFields +) => + `${dataProvider.excluded ? 'NOT ' : ''}${ + dataProvider.queryMatch.operator !== EXISTS_OPERATOR + ? checkIfFieldTypeIsDate(dataProvider.queryMatch.field, browserFields) + ? convertDateFieldToQuery(dataProvider.queryMatch.field, dataProvider.queryMatch.value) + : `${dataProvider.queryMatch.field} : ${ + isNumber(dataProvider.queryMatch.value) + ? dataProvider.queryMatch.value + : escapeQueryValue(dataProvider.queryMatch.value) + }` + : `${dataProvider.queryMatch.field} ${EXISTS_OPERATOR}` + }`.trim(); + +const buildQueryForAndProvider = ( + dataAndProviders: DataProvidersAnd[], + browserFields: BrowserFields +) => + dataAndProviders + .reduce((andQuery, andDataProvider) => { + const prepend = (q: string) => `${q !== '' ? `${q} and ` : ''}`; + return andDataProvider.enabled + ? `${prepend(andQuery)} ${buildQueryMatch(andDataProvider, browserFields)}` + : andQuery; + }, '') + .trim(); + +export const buildGlobalQuery = (dataProviders: DataProvider[], browserFields: BrowserFields) => + dataProviders + .reduce((query, dataProvider: DataProvider, i) => { + const prepend = (q: string) => `${q !== '' ? `(${q}) or ` : ''}`; + const openParen = i > 0 ? '(' : ''; + const closeParen = i > 0 ? ')' : ''; + return dataProvider.enabled + ? `${prepend(query)}${openParen}${buildQueryMatch(dataProvider, browserFields)} + ${ + dataProvider.and.length > 0 + ? ` and ${buildQueryForAndProvider(dataProvider.and, browserFields)}` + : '' + }${closeParen}`.trim() + : query; + }, '') + .trim(); + +export const combineQueries = ({ + config, + dataProviders, + indexPattern, + browserFields, + filters = [], + kqlQuery, + kqlMode, + start, + end, + isEventViewer, +}: { + config: EsQueryConfig; + dataProviders: DataProvider[]; + indexPattern: IIndexPattern; + browserFields: BrowserFields; + filters: Filter[]; + kqlQuery: Query; + kqlMode: string; + start: number; + end: number; + isEventViewer?: boolean; +}): { filterQuery: string } | null => { + const kuery: Query = { query: '', language: kqlQuery.language }; + if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && isEmpty(filters) && !isEventViewer) { + return null; + } else if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && isEventViewer) { + kuery.query = `@timestamp >= ${start} and @timestamp <= ${end}`; + return { + filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), + }; + } else if (isEmpty(dataProviders) && isEmpty(kqlQuery.query) && !isEmpty(filters)) { + kuery.query = `@timestamp >= ${start} and @timestamp <= ${end}`; + return { + filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), + }; + } else if (isEmpty(dataProviders) && !isEmpty(kqlQuery.query)) { + kuery.query = `(${kqlQuery.query}) and @timestamp >= ${start} and @timestamp <= ${end}`; + return { + filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), + }; + } else if (!isEmpty(dataProviders) && isEmpty(kqlQuery)) { + kuery.query = `(${buildGlobalQuery( + dataProviders, + browserFields + )}) and @timestamp >= ${start} and @timestamp <= ${end}`; + return { + filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), + }; + } + const operatorKqlQuery = kqlMode === 'filter' ? 'and' : 'or'; + const postpend = (q: string) => `${!isEmpty(q) ? ` ${operatorKqlQuery} (${q})` : ''}`; + kuery.query = `((${buildGlobalQuery(dataProviders, browserFields)})${postpend( + kqlQuery.query as string + )}) and @timestamp >= ${start} and @timestamp <= ${end}`; + return { + filterQuery: convertToBuildEsQuery({ config, queries: [kuery], indexPattern, filters }), + }; +}; + +/** + * The CSS class name of a "stateful event", which appears in both + * the `Timeline` and the `Events Viewer` widget + */ +export const STATEFUL_EVENT_CSS_CLASS_NAME = 'event-column-view'; diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/index.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/index.tsx new file mode 100644 index 00000000000000..fca16ffadce843 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/index.tsx @@ -0,0 +1,277 @@ +/* + * 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, useCallback, useMemo } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; + +import { WithSource } from '../../../common/containers/source'; +import { useSignalIndex } from '../../../alerts/containers/detection_engine/signals/use_signal_index'; +import { inputsModel, inputsSelectors, State } from '../../../common/store'; +import { timelineActions, timelineSelectors } from '../../store/timeline'; +import { ColumnHeaderOptions, TimelineModel } from '../../../timelines/store/timeline/model'; +import { timelineDefaults } from '../../../timelines/store/timeline/defaults'; +import { defaultHeaders } from './body/column_headers/default_headers'; +import { + OnChangeItemsPerPage, + OnDataProviderRemoved, + OnDataProviderEdited, + OnToggleDataProviderEnabled, + OnToggleDataProviderExcluded, +} from './events'; +import { Timeline } from './timeline'; + +export interface OwnProps { + id: string; + onClose: () => void; + usersViewing: string[]; +} + +type Props = OwnProps & PropsFromRedux; + +const StatefulTimelineComponent = React.memo( + ({ + columns, + createTimeline, + dataProviders, + eventType, + end, + filters, + id, + isLive, + itemsPerPage, + itemsPerPageOptions, + kqlMode, + kqlQueryExpression, + onClose, + onDataProviderEdited, + removeColumn, + removeProvider, + show, + showCallOutUnauthorizedMsg, + sort, + start, + updateDataProviderEnabled, + updateDataProviderExcluded, + updateItemsPerPage, + upsertColumn, + usersViewing, + }) => { + const { loading, signalIndexExists, signalIndexName } = useSignalIndex(); + + const indexToAdd = useMemo(() => { + if ( + eventType && + signalIndexExists && + signalIndexName != null && + ['signal', 'all'].includes(eventType) + ) { + return [signalIndexName]; + } + return []; + }, [eventType, signalIndexExists, signalIndexName]); + + const onDataProviderRemoved: OnDataProviderRemoved = useCallback( + (providerId: string, andProviderId?: string) => + removeProvider!({ id, providerId, andProviderId }), + [id] + ); + + const onToggleDataProviderEnabled: OnToggleDataProviderEnabled = useCallback( + ({ providerId, enabled, andProviderId }) => + updateDataProviderEnabled!({ + id, + enabled, + providerId, + andProviderId, + }), + [id] + ); + + const onToggleDataProviderExcluded: OnToggleDataProviderExcluded = useCallback( + ({ providerId, excluded, andProviderId }) => + updateDataProviderExcluded!({ + id, + excluded, + providerId, + andProviderId, + }), + [id] + ); + + const onDataProviderEditedLocal: OnDataProviderEdited = useCallback( + ({ andProviderId, excluded, field, operator, providerId, value }) => + onDataProviderEdited!({ + andProviderId, + excluded, + field, + id, + operator, + providerId, + value, + }), + [id] + ); + + const onChangeItemsPerPage: OnChangeItemsPerPage = useCallback( + itemsChangedPerPage => updateItemsPerPage!({ id, itemsPerPage: itemsChangedPerPage }), + [id] + ); + + const toggleColumn = useCallback( + (column: ColumnHeaderOptions) => { + const exists = columns.findIndex(c => c.id === column.id) !== -1; + + if (!exists && upsertColumn != null) { + upsertColumn({ + column, + id, + index: 1, + }); + } + + if (exists && removeColumn != null) { + removeColumn({ + columnId: column.id, + id, + }); + } + }, + [columns, id] + ); + + useEffect(() => { + if (createTimeline != null) { + createTimeline({ id, columns: defaultHeaders, show: false }); + } + }, []); + + return ( + + {({ indexPattern, browserFields }) => ( + + )} + + ); + }, + (prevProps, nextProps) => { + return ( + prevProps.eventType === nextProps.eventType && + prevProps.end === nextProps.end && + prevProps.id === nextProps.id && + prevProps.isLive === nextProps.isLive && + prevProps.itemsPerPage === nextProps.itemsPerPage && + prevProps.kqlMode === nextProps.kqlMode && + prevProps.kqlQueryExpression === nextProps.kqlQueryExpression && + prevProps.show === nextProps.show && + prevProps.showCallOutUnauthorizedMsg === nextProps.showCallOutUnauthorizedMsg && + prevProps.start === nextProps.start && + deepEqual(prevProps.columns, nextProps.columns) && + deepEqual(prevProps.dataProviders, nextProps.dataProviders) && + deepEqual(prevProps.filters, nextProps.filters) && + deepEqual(prevProps.itemsPerPageOptions, nextProps.itemsPerPageOptions) && + deepEqual(prevProps.sort, nextProps.sort) && + deepEqual(prevProps.usersViewing, nextProps.usersViewing) + ); + } +); + +StatefulTimelineComponent.displayName = 'StatefulTimelineComponent'; + +const makeMapStateToProps = () => { + const getShowCallOutUnauthorizedMsg = timelineSelectors.getShowCallOutUnauthorizedMsg(); + const getTimeline = timelineSelectors.getTimelineByIdSelector(); + const getKqlQueryTimeline = timelineSelectors.getKqlFilterQuerySelector(); + const getInputsTimeline = inputsSelectors.getTimelineSelector(); + const mapStateToProps = (state: State, { id }: OwnProps) => { + const timeline: TimelineModel = getTimeline(state, id) ?? timelineDefaults; + const input: inputsModel.InputsRange = getInputsTimeline(state); + const { + columns, + dataProviders, + eventType, + filters, + itemsPerPage, + itemsPerPageOptions, + kqlMode, + show, + sort, + } = timeline; + const kqlQueryExpression = getKqlQueryTimeline(state, id)!; + + const timelineFilter = kqlMode === 'filter' ? filters || [] : []; + + return { + columns, + dataProviders, + eventType, + end: input.timerange.to, + filters: timelineFilter, + id, + isLive: input.policy.kind === 'interval', + itemsPerPage, + itemsPerPageOptions, + kqlMode, + kqlQueryExpression, + show, + showCallOutUnauthorizedMsg: getShowCallOutUnauthorizedMsg(state), + sort, + start: input.timerange.from, + }; + }; + return mapStateToProps; +}; + +const mapDispatchToProps = { + addProvider: timelineActions.addProvider, + createTimeline: timelineActions.createTimeline, + onDataProviderEdited: timelineActions.dataProviderEdited, + removeColumn: timelineActions.removeColumn, + removeProvider: timelineActions.removeProvider, + updateColumns: timelineActions.updateColumns, + updateDataProviderEnabled: timelineActions.updateDataProviderEnabled, + updateDataProviderExcluded: timelineActions.updateDataProviderExcluded, + updateDataProviderKqlQuery: timelineActions.updateDataProviderKqlQuery, + updateHighlightedDropAndProviderId: timelineActions.updateHighlightedDropAndProviderId, + updateItemsPerPage: timelineActions.updateItemsPerPage, + updateItemsPerPageOptions: timelineActions.updateItemsPerPageOptions, + updateSort: timelineActions.updateSort, + upsertColumn: timelineActions.upsertColumn, +}; + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const StatefulTimeline = connector(StatefulTimelineComponent); diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/insert_timeline_popover/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/insert_timeline_popover/index.test.tsx new file mode 100644 index 00000000000000..d5cfc397e1990a --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/insert_timeline_popover/index.test.tsx @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { mount } from 'enzyme'; +/* eslint-disable @kbn/eslint/module_migration */ +import routeData from 'react-router'; +/* eslint-enable @kbn/eslint/module_migration */ +import { InsertTimelinePopoverComponent } from '.'; + +const mockDispatch = jest.fn(); +jest.mock('react-redux', () => { + const reactRedux = jest.requireActual('react-redux'); + return { + ...reactRedux, + useDispatch: () => mockDispatch, + }; +}); +const mockLocation = { + pathname: '/apath', + hash: '', + search: '', + state: '', +}; +const mockLocationWithState = { + ...mockLocation, + state: { + insertTimeline: { + timelineId: 'timeline-id', + timelineSavedObjectId: '34578-3497-5893-47589-34759', + timelineTitle: 'Timeline title', + }, + }, +}; + +const onTimelineChange = jest.fn(); +const defaultProps = { + isDisabled: false, + onTimelineChange, +}; + +describe('Insert timeline popover ', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should insert a timeline when passed in the router state', () => { + jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocationWithState); + mount(); + expect(mockDispatch).toBeCalledWith({ + payload: { id: 'timeline-id', show: false }, + type: 'x-pack/siem/local/timeline/SHOW_TIMELINE', + }); + expect(onTimelineChange).toBeCalledWith('Timeline title', '34578-3497-5893-47589-34759'); + }); + it('should do nothing when router state', () => { + jest.spyOn(routeData, 'useLocation').mockReturnValue(mockLocation); + mount(); + expect(mockDispatch).toHaveBeenCalledTimes(0); + expect(onTimelineChange).toHaveBeenCalledTimes(0); + }); +}); diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/insert_timeline_popover/index.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/insert_timeline_popover/index.tsx new file mode 100644 index 00000000000000..37b1125cd673af --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/insert_timeline_popover/index.tsx @@ -0,0 +1,115 @@ +/* + * 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 { EuiButtonIcon, EuiPopover, EuiSelectableOption, EuiToolTip } from '@elastic/eui'; +import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; +import { useLocation } from 'react-router-dom'; +import { useDispatch } from 'react-redux'; + +import { OpenTimelineResult } from '../../open_timeline/types'; +import { SelectableTimeline } from '../selectable_timeline'; +import * as i18n from '../translations'; +import { timelineActions } from '../../../../timelines/store/timeline'; + +interface InsertTimelinePopoverProps { + isDisabled: boolean; + hideUntitled?: boolean; + onTimelineChange: (timelineTitle: string, timelineId: string | null) => void; +} + +interface RouterState { + insertTimeline: { + timelineId: string; + timelineSavedObjectId: string; + timelineTitle: string; + }; +} + +type Props = InsertTimelinePopoverProps; + +export const InsertTimelinePopoverComponent: React.FC = ({ + isDisabled, + hideUntitled = false, + onTimelineChange, +}) => { + const dispatch = useDispatch(); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const { state } = useLocation(); + const [routerState, setRouterState] = useState(state ?? null); + + useEffect(() => { + if (routerState && routerState.insertTimeline) { + dispatch( + timelineActions.showTimeline({ id: routerState.insertTimeline.timelineId, show: false }) + ); + onTimelineChange( + routerState.insertTimeline.timelineTitle, + routerState.insertTimeline.timelineSavedObjectId + ); + setRouterState(null); + } + }, [routerState]); + + const handleClosePopover = useCallback(() => { + setIsPopoverOpen(false); + }, []); + + const handleOpenPopover = useCallback(() => { + setIsPopoverOpen(true); + }, []); + + const insertTimelineButton = useMemo( + () => ( + {i18n.INSERT_TIMELINE}

}> + +
+ ), + [handleOpenPopover, isDisabled] + ); + + const handleGetSelectableOptions = useCallback( + ({ timelines }) => [ + ...timelines.map( + (t: OpenTimelineResult, index: number) => + ({ + description: t.description, + favorite: t.favorite, + label: t.title, + id: t.savedObjectId, + key: `${t.title}-${index}`, + title: t.title, + checked: undefined, + } as EuiSelectableOption) + ), + ], + [] + ); + + return ( + + + + ); +}; + +export const InsertTimelinePopover = memo(InsertTimelinePopoverComponent); diff --git a/x-pack/plugins/siem/public/components/timeline/insert_timeline_popover/use_insert_timeline.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/insert_timeline_popover/use_insert_timeline.tsx similarity index 87% rename from x-pack/plugins/siem/public/components/timeline/insert_timeline_popover/use_insert_timeline.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/insert_timeline_popover/use_insert_timeline.tsx index e4d828b68f3dc9..1a81c131de0150 100644 --- a/x-pack/plugins/siem/public/components/timeline/insert_timeline_popover/use_insert_timeline.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/insert_timeline_popover/use_insert_timeline.tsx @@ -5,9 +5,9 @@ */ import { useCallback, useState } from 'react'; -import { useBasePath } from '../../../lib/kibana'; -import { CursorPosition } from '../../markdown_editor'; -import { FormData, FormHook } from '../../../shared_imports'; +import { useBasePath } from '../../../../common/lib/kibana'; +import { CursorPosition } from '../../../../common/components/markdown_editor'; +import { FormData, FormHook } from '../../../../shared_imports'; export const useInsertTimeline = (form: FormHook, fieldName: string) => { const basePath = window.location.origin + useBasePath(); diff --git a/x-pack/plugins/siem/public/components/pin/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/pin/index.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/pin/index.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/pin/index.test.tsx diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/pin/index.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/pin/index.tsx new file mode 100644 index 00000000000000..800ea814fdd509 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/pin/index.tsx @@ -0,0 +1,37 @@ +/* + * 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 { EuiButtonIcon, IconSize } from '@elastic/eui'; +import { noop } from 'lodash/fp'; +import React from 'react'; + +import * as i18n from '../body/translations'; + +export type PinIcon = 'pin' | 'pinFilled'; + +export const getPinIcon = (pinned: boolean): PinIcon => (pinned ? 'pinFilled' : 'pin'); + +interface Props { + allowUnpinning: boolean; + iconSize?: IconSize; + onClick?: () => void; + pinned: boolean; +} + +export const Pin = React.memo( + ({ allowUnpinning, iconSize = 'm', onClick = noop, pinned }) => ( + + ) +); + +Pin.displayName = 'Pin'; diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/properties/helpers.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/properties/helpers.tsx new file mode 100644 index 00000000000000..1453d58c2ffd59 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/properties/helpers.tsx @@ -0,0 +1,327 @@ +/* + * 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 { + EuiBadge, + EuiButton, + EuiButtonEmpty, + EuiButtonIcon, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiModal, + EuiOverlayMask, + EuiToolTip, +} from '@elastic/eui'; +import React, { useCallback } from 'react'; +import uuid from 'uuid'; +import styled from 'styled-components'; +import { useHistory } from 'react-router-dom'; +import { useSelector } from 'react-redux'; + +import { Note } from '../../../../common/lib/note'; +import { Notes } from '../../notes'; +import { AssociateNote, UpdateNote } from '../../notes/helpers'; +import { NOTES_PANEL_WIDTH } from './notes_size'; +import { ButtonContainer, DescriptionContainer, LabelText, NameField, StyledStar } from './styles'; +import * as i18n from './translations'; +import { SiemPageName } from '../../../../app/types'; +import { timelineSelectors } from '../../../../timelines/store/timeline'; +import { State } from '../../../../common/store'; + +export const historyToolTip = 'The chronological history of actions related to this timeline'; +export const streamLiveToolTip = 'Update the Timeline as new data arrives'; +export const newTimelineToolTip = 'Create a new timeline'; + +const NotesCountBadge = (styled(EuiBadge)` + margin-left: 5px; +` as unknown) as typeof EuiBadge; + +NotesCountBadge.displayName = 'NotesCountBadge'; + +type CreateTimeline = ({ id, show }: { id: string; show?: boolean }) => void; +type UpdateIsFavorite = ({ id, isFavorite }: { id: string; isFavorite: boolean }) => void; +type UpdateTitle = ({ id, title }: { id: string; title: string }) => void; +type UpdateDescription = ({ id, description }: { id: string; description: string }) => void; + +export const StarIcon = React.memo<{ + isFavorite: boolean; + timelineId: string; + updateIsFavorite: UpdateIsFavorite; +}>(({ isFavorite, timelineId: id, updateIsFavorite }) => ( + // TODO: 1 error is: Visible, non-interactive elements with click handlers must have at least one keyboard listener + // TODO: 2 error is: Elements with the 'button' interactive role must be focusable + // TODO: Investigate this error + // eslint-disable-next-line +
updateIsFavorite({ id, isFavorite: !isFavorite })}> + {isFavorite ? ( + + + + ) : ( + + + + )} +
+)); +StarIcon.displayName = 'StarIcon'; + +interface DescriptionProps { + description: string; + timelineId: string; + updateDescription: UpdateDescription; +} + +export const Description = React.memo( + ({ description, timelineId, updateDescription }) => ( + + + updateDescription({ id: timelineId, description: e.target.value })} + placeholder={i18n.DESCRIPTION} + spellCheck={true} + value={description} + /> + + + ) +); +Description.displayName = 'Description'; + +interface NameProps { + timelineId: string; + title: string; + updateTitle: UpdateTitle; +} + +export const Name = React.memo(({ timelineId, title, updateTitle }) => ( + + updateTitle({ id: timelineId, title: e.target.value })} + placeholder={i18n.UNTITLED_TIMELINE} + spellCheck={true} + value={title} + /> + +)); +Name.displayName = 'Name'; + +interface NewCaseProps { + onClosePopover: () => void; + timelineId: string; + timelineTitle: string; +} + +export const NewCase = React.memo(({ onClosePopover, timelineId, timelineTitle }) => { + const history = useHistory(); + const { savedObjectId } = useSelector((state: State) => + timelineSelectors.selectTimeline(state, timelineId) + ); + const handleClick = useCallback(() => { + onClosePopover(); + history.push({ + pathname: `/${SiemPageName.case}/create`, + state: { + insertTimeline: { + timelineId, + timelineSavedObjectId: savedObjectId, + timelineTitle: timelineTitle.length > 0 ? timelineTitle : i18n.UNTITLED_TIMELINE, + }, + }, + }); + }, [onClosePopover, history, timelineId, timelineTitle]); + + return ( + + {i18n.ATTACH_TIMELINE_TO_NEW_CASE} + + ); +}); +NewCase.displayName = 'NewCase'; + +interface NewTimelineProps { + createTimeline: CreateTimeline; + onClosePopover: () => void; + timelineId: string; +} + +export const NewTimeline = React.memo( + ({ createTimeline, onClosePopover, timelineId }) => { + const handleClick = useCallback(() => { + createTimeline({ id: timelineId, show: true }); + onClosePopover(); + }, [createTimeline, timelineId, onClosePopover]); + + return ( + + {i18n.NEW_TIMELINE} + + ); + } +); +NewTimeline.displayName = 'NewTimeline'; + +interface NotesButtonProps { + animate?: boolean; + associateNote: AssociateNote; + getNotesByIds: (noteIds: string[]) => Note[]; + noteIds: string[]; + size: 's' | 'l'; + showNotes: boolean; + toggleShowNotes: () => void; + text?: string; + toolTip?: string; + updateNote: UpdateNote; +} + +const getNewNoteId = (): string => uuid.v4(); + +interface LargeNotesButtonProps { + noteIds: string[]; + text?: string; + toggleShowNotes: () => void; +} + +const LargeNotesButton = React.memo(({ noteIds, text, toggleShowNotes }) => ( + toggleShowNotes()} + size="m" + > + + + + + + {text && text.length ? {text} : null} + + + + {noteIds.length} + + + + +)); +LargeNotesButton.displayName = 'LargeNotesButton'; + +interface SmallNotesButtonProps { + noteIds: string[]; + toggleShowNotes: () => void; +} + +const SmallNotesButton = React.memo(({ noteIds, toggleShowNotes }) => ( + toggleShowNotes()} + /> +)); +SmallNotesButton.displayName = 'SmallNotesButton'; + +/** + * The internal implementation of the `NotesButton` + */ +const NotesButtonComponent = React.memo( + ({ + animate = true, + associateNote, + getNotesByIds, + noteIds, + showNotes, + size, + toggleShowNotes, + text, + updateNote, + }) => ( + + <> + {size === 'l' ? ( + + ) : ( + + )} + {size === 'l' && showNotes ? ( + + + + + + ) : null} + + + ) +); +NotesButtonComponent.displayName = 'NotesButtonComponent'; + +export const NotesButton = React.memo( + ({ + animate = true, + associateNote, + getNotesByIds, + noteIds, + showNotes, + size, + toggleShowNotes, + toolTip, + text, + updateNote, + }) => + showNotes ? ( + + ) : ( + + + + ) +); +NotesButton.displayName = 'NotesButton'; diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/properties/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/properties/index.test.tsx new file mode 100644 index 00000000000000..17968a5977069a --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/properties/index.test.tsx @@ -0,0 +1,469 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; +import { Provider as ReduxStoreProvider } from 'react-redux'; + +import { + mockGlobalState, + apolloClientObservable, + SUB_PLUGINS_REDUCER, +} from '../../../../common/mock'; +import { createStore, State } from '../../../../common/store'; +import { useThrottledResizeObserver } from '../../../../common/components/utils'; +import { Properties, showDescriptionThreshold, showNotesThreshold } from '.'; + +jest.mock('../../../../common/lib/kibana'); + +let mockedWidth = 1000; +jest.mock('../../../../common/components/utils'); +(useThrottledResizeObserver as jest.Mock).mockImplementation(() => ({ + width: mockedWidth, +})); + +describe('Properties', () => { + const usersViewing = ['elastic']; + + const state: State = mockGlobalState; + let store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + + beforeEach(() => { + jest.clearAllMocks(); + store = createStore(state, SUB_PLUGINS_REDUCER, apolloClientObservable); + mockedWidth = 1000; + }); + + test('renders correctly', () => { + const wrapper = mount( + + + + ); + expect(wrapper.find('[data-test-subj="timeline-properties"]').exists()).toEqual(true); + }); + + test('it renders an empty star icon when it is NOT a favorite', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="timeline-favorite-empty-star"]').exists()).toEqual(true); + }); + + test('it renders a filled star icon when it is a favorite', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="timeline-favorite-filled-star"]').exists()).toEqual(true); + }); + + test('it renders the title of the timeline', () => { + const title = 'foozle'; + + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="timeline-title"]') + .first() + .props().value + ).toEqual(title); + }); + + test('it renders the date picker with the lock icon', () => { + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="properties-left"]') + .find('[data-test-subj="timeline-date-picker-container"]') + .exists() + ).toEqual(true); + }); + + test('it renders the lock icon when isDatepickerLocked is true', () => { + const wrapper = mount( + + + + ); + expect( + wrapper + .find('[data-test-subj="properties-left"]') + .find('[data-test-subj="timeline-date-picker-lock-button"]') + .exists() + ).toEqual(true); + }); + + test('it renders the unlock icon when isDatepickerLocked is false', () => { + const wrapper = mount( + + + + ); + expect( + wrapper + .find('[data-test-subj="properties-left"]') + .find('[data-test-subj="timeline-date-picker-unlock-button"]') + .exists() + ).toEqual(true); + }); + + test('it renders a description on the left when the width is at least as wide as the threshold', () => { + const description = 'strange'; + mockedWidth = showDescriptionThreshold; + + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="properties-left"]') + .find('[data-test-subj="timeline-description"]') + .first() + .props().value + ).toEqual(description); + }); + + test('it does NOT render a description on the left when the width is less than the threshold', () => { + const description = 'strange'; + mockedWidth = showDescriptionThreshold - 1; + + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="properties-left"]') + .find('[data-test-subj="timeline-description"]') + .exists() + ).toEqual(false); + }); + + test('it renders a notes button on the left when the width is at least as wide as the threshold', () => { + mockedWidth = showNotesThreshold; + + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="properties-left"]') + .find('[data-test-subj="timeline-notes-button-large"]') + .exists() + ).toEqual(true); + }); + + test('it does NOT render a a notes button on the left when the width is less than the threshold', () => { + mockedWidth = showNotesThreshold - 1; + + const wrapper = mount( + + + + ); + + expect( + wrapper + .find('[data-test-subj="properties-left"]') + .find('[data-test-subj="timeline-notes-button-large"]') + .exists() + ).toEqual(false); + }); + + test('it renders a settings icon', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="settings-gear"]').exists()).toEqual(true); + }); + + test('it renders an avatar for the current user viewing the timeline when it has a title', () => { + const title = 'port scan'; + + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="avatar"]').exists()).toEqual(true); + }); + + test('it does NOT render an avatar for the current user viewing the timeline when it does NOT have a title', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="avatar"]').exists()).toEqual(false); + }); +}); diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/properties/index.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/properties/index.tsx new file mode 100644 index 00000000000000..502cc85ce907aa --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/properties/index.tsx @@ -0,0 +1,154 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useState, useCallback, useMemo } from 'react'; + +import { useThrottledResizeObserver } from '../../../../common/components/utils'; +import { Note } from '../../../../common/lib/note'; +import { InputsModelId } from '../../../../common/store/inputs/constants'; +import { AssociateNote, UpdateNote } from '../../notes/helpers'; + +import { TimelineProperties } from './styles'; +import { PropertiesRight } from './properties_right'; +import { PropertiesLeft } from './properties_left'; + +type CreateTimeline = ({ id, show }: { id: string; show?: boolean }) => void; +type UpdateIsFavorite = ({ id, isFavorite }: { id: string; isFavorite: boolean }) => void; +type UpdateTitle = ({ id, title }: { id: string; title: string }) => void; +type UpdateDescription = ({ id, description }: { id: string; description: string }) => void; +type ToggleLock = ({ linkToId }: { linkToId: InputsModelId }) => void; + +interface Props { + associateNote: AssociateNote; + createTimeline: CreateTimeline; + description: string; + getNotesByIds: (noteIds: string[]) => Note[]; + isDataInTimeline: boolean; + isDatepickerLocked: boolean; + isFavorite: boolean; + noteIds: string[]; + timelineId: string; + title: string; + toggleLock: ToggleLock; + updateDescription: UpdateDescription; + updateIsFavorite: UpdateIsFavorite; + updateNote: UpdateNote; + updateTitle: UpdateTitle; + usersViewing: string[]; +} + +const rightGutter = 60; // px +export const datePickerThreshold = 600; +export const showNotesThreshold = 810; +export const showDescriptionThreshold = 970; + +const starIconWidth = 30; +const nameWidth = 155; +const descriptionWidth = 165; +const noteWidth = 130; +const settingsWidth = 55; + +/** Displays the properties of a timeline, i.e. name, description, notes, etc */ +export const Properties = React.memo( + ({ + associateNote, + createTimeline, + description, + getNotesByIds, + isDataInTimeline, + isDatepickerLocked, + isFavorite, + noteIds, + timelineId, + title, + toggleLock, + updateDescription, + updateIsFavorite, + updateNote, + updateTitle, + usersViewing, + }) => { + const { ref, width = 0 } = useThrottledResizeObserver(300); + const [showActions, setShowActions] = useState(false); + const [showNotes, setShowNotes] = useState(false); + const [showTimelineModal, setShowTimelineModal] = useState(false); + + const onButtonClick = useCallback(() => setShowActions(!showActions), [showActions]); + const onToggleShowNotes = useCallback(() => setShowNotes(!showNotes), [showNotes]); + const onClosePopover = useCallback(() => setShowActions(false), []); + const onCloseTimelineModal = useCallback(() => setShowTimelineModal(false), []); + const onToggleLock = useCallback(() => toggleLock({ linkToId: 'timeline' }), [toggleLock]); + const onOpenTimelineModal = useCallback(() => { + onClosePopover(); + setShowTimelineModal(true); + }, []); + + const datePickerWidth = useMemo( + () => + width - + rightGutter - + starIconWidth - + nameWidth - + (width >= showDescriptionThreshold ? descriptionWidth : 0) - + noteWidth - + settingsWidth, + [width] + ); + + return ( + + datePickerThreshold ? datePickerThreshold : datePickerWidth + } + description={description} + getNotesByIds={getNotesByIds} + isDatepickerLocked={isDatepickerLocked} + isFavorite={isFavorite} + noteIds={noteIds} + onToggleShowNotes={onToggleShowNotes} + showDescription={width >= showDescriptionThreshold} + showNotes={showNotes} + showNotesFromWidth={width >= showNotesThreshold} + timelineId={timelineId} + title={title} + toggleLock={onToggleLock} + updateDescription={updateDescription} + updateIsFavorite={updateIsFavorite} + updateNote={updateNote} + updateTitle={updateTitle} + /> + 0} + timelineId={timelineId} + title={title} + updateDescription={updateDescription} + updateNote={updateNote} + usersViewing={usersViewing} + /> + + ); + } +); + +Properties.displayName = 'Properties'; diff --git a/x-pack/plugins/siem/public/components/timeline/properties/notes_size.ts b/x-pack/plugins/siem/public/timelines/components/timeline/properties/notes_size.ts similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/properties/notes_size.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/properties/notes_size.ts diff --git a/x-pack/plugins/siem/public/components/timeline/properties/properties_left.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/properties/properties_left.tsx similarity index 97% rename from x-pack/plugins/siem/public/components/timeline/properties/properties_left.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/properties/properties_left.tsx index 3016def8a80b10..52766422e49c38 100644 --- a/x-pack/plugins/siem/public/components/timeline/properties/properties_left.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/properties/properties_left.tsx @@ -10,8 +10,8 @@ import React from 'react'; import styled from 'styled-components'; import { Description, Name, NotesButton, StarIcon } from './helpers'; import { AssociateNote, UpdateNote } from '../../notes/helpers'; -import { Note } from '../../../lib/note'; -import { SuperDatePicker } from '../../super_date_picker'; +import { Note } from '../../../../common/lib/note'; +import { SuperDatePicker } from '../../../../common/components/super_date_picker'; import * as i18n from './translations'; diff --git a/x-pack/plugins/siem/public/components/timeline/properties/properties_right.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/properties/properties_right.tsx similarity index 97% rename from x-pack/plugins/siem/public/components/timeline/properties/properties_right.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/properties/properties_right.tsx index 59d268487cca72..3db64390b51b71 100644 --- a/x-pack/plugins/siem/public/components/timeline/properties/properties_right.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/properties/properties_right.tsx @@ -17,11 +17,11 @@ import { import { NewTimeline, Description, NotesButton, NewCase } from './helpers'; import { OpenTimelineModalButton } from '../../open_timeline/open_timeline_modal/open_timeline_modal_button'; import { OpenTimelineModal } from '../../open_timeline/open_timeline_modal'; -import { InspectButton, InspectButtonContainer } from '../../inspect'; +import { InspectButton, InspectButtonContainer } from '../../../../common/components/inspect'; import * as i18n from './translations'; import { AssociateNote } from '../../notes/helpers'; -import { Note } from '../../../lib/note'; +import { Note } from '../../../../common/lib/note'; export const PropertiesRightStyle = styled(EuiFlexGroup)` margin-right: 5px; diff --git a/x-pack/plugins/siem/public/components/timeline/properties/styles.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/properties/styles.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/properties/styles.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/properties/styles.tsx diff --git a/x-pack/plugins/siem/public/components/timeline/properties/translations.ts b/x-pack/plugins/siem/public/timelines/components/timeline/properties/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/properties/translations.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/properties/translations.ts diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/query_bar/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/query_bar/index.test.tsx new file mode 100644 index 00000000000000..546f06b60cb56a --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/query_bar/index.test.tsx @@ -0,0 +1,409 @@ +/* + * 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 { mount } from 'enzyme'; +import React from 'react'; + +import { DEFAULT_FROM, DEFAULT_TO } from '../../../../../common/constants'; +import { mockBrowserFields } from '../../../../common/containers/source/mock'; +import { convertKueryToElasticSearchQuery } from '../../../../common/lib/keury'; +import { mockIndexPattern, TestProviders } from '../../../../common/mock'; +import { createKibanaCoreStartMock } from '../../../../common/mock/kibana_core'; +import { QueryBar } from '../../../../common/components/query_bar'; +import { FilterManager } from '../../../../../../../../src/plugins/data/public'; +import { mockDataProviders } from '../data_providers/mock/mock_data_providers'; +import { buildGlobalQuery } from '../helpers'; + +import { QueryBarTimeline, QueryBarTimelineComponentProps, getDataProviderFilter } from './index'; + +const mockUiSettingsForFilterManager = createKibanaCoreStartMock().uiSettings; + +jest.mock('../../../../common/lib/kibana'); + +describe('Timeline QueryBar ', () => { + // We are doing that because we need to wrapped this component with redux + // and redux does not like to be updated and since we need to update our + // child component (BODY) and we do not want to scare anyone with this error + // we are hiding it!!! + // eslint-disable-next-line no-console + const originalError = console.error; + beforeAll(() => { + // eslint-disable-next-line no-console + console.error = (...args: string[]) => { + if (/ does not support changing `store` on the fly/.test(args[0])) { + return; + } + originalError.call(console, ...args); + }; + }); + + const mockApplyKqlFilterQuery = jest.fn(); + const mockSetFilters = jest.fn(); + const mockSetKqlFilterQueryDraft = jest.fn(); + const mockSetSavedQueryId = jest.fn(); + const mockUpdateReduxTime = jest.fn(); + + beforeEach(() => { + mockApplyKqlFilterQuery.mockClear(); + mockSetFilters.mockClear(); + mockSetKqlFilterQueryDraft.mockClear(); + mockSetSavedQueryId.mockClear(); + mockUpdateReduxTime.mockClear(); + }); + + test('check if we format the appropriate props to QueryBar', () => { + const wrapper = mount( + + + + ); + const queryBarProps = wrapper.find(QueryBar).props(); + + expect(queryBarProps.dateRangeFrom).toEqual('now-24h'); + expect(queryBarProps.dateRangeTo).toEqual('now'); + expect(queryBarProps.filterQuery).toEqual({ query: 'here: query', language: 'kuery' }); + expect(queryBarProps.savedQuery).toEqual(null); + }); + + describe('#onChangeQuery', () => { + test(' is the only reference that changed when filterQueryDraft props get updated', () => { + const Proxy = (props: QueryBarTimelineComponentProps) => ( + + + + ); + + const wrapper = mount( + + ); + const queryBarProps = wrapper.find(QueryBar).props(); + const onChangedQueryRef = queryBarProps.onChangedQuery; + const onSubmitQueryRef = queryBarProps.onSubmitQuery; + const onSavedQueryRef = queryBarProps.onSavedQuery; + + wrapper.setProps({ filterQueryDraft: { expression: 'new: one', kind: 'kuery' } }); + wrapper.update(); + + expect(onChangedQueryRef).not.toEqual(wrapper.find(QueryBar).props().onChangedQuery); + expect(onSubmitQueryRef).toEqual(wrapper.find(QueryBar).props().onSubmitQuery); + expect(onSavedQueryRef).toEqual(wrapper.find(QueryBar).props().onSavedQuery); + }); + }); + + describe('#onSubmitQuery', () => { + test(' is the only reference that changed when filterQuery props get updated', () => { + const Proxy = (props: QueryBarTimelineComponentProps) => ( + + + + ); + + const wrapper = mount( + + ); + const queryBarProps = wrapper.find(QueryBar).props(); + const onChangedQueryRef = queryBarProps.onChangedQuery; + const onSubmitQueryRef = queryBarProps.onSubmitQuery; + const onSavedQueryRef = queryBarProps.onSavedQuery; + + wrapper.setProps({ filterQuery: { expression: 'new: one', kind: 'kuery' } }); + wrapper.update(); + + expect(onSubmitQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSubmitQuery); + expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); + expect(onSavedQueryRef).toEqual(wrapper.find(QueryBar).props().onSavedQuery); + }); + + test(' is only reference that changed when timelineId props get updated', () => { + const Proxy = (props: QueryBarTimelineComponentProps) => ( + + + + ); + + const wrapper = mount( + + ); + const queryBarProps = wrapper.find(QueryBar).props(); + const onChangedQueryRef = queryBarProps.onChangedQuery; + const onSubmitQueryRef = queryBarProps.onSubmitQuery; + const onSavedQueryRef = queryBarProps.onSavedQuery; + + wrapper.setProps({ timelineId: 'new-timeline' }); + wrapper.update(); + + expect(onSubmitQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSubmitQuery); + expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); + expect(onSavedQueryRef).toEqual(wrapper.find(QueryBar).props().onSavedQuery); + }); + }); + + describe('#onSavedQuery', () => { + test('is only reference that changed when dataProviders props get updated', () => { + const Proxy = (props: QueryBarTimelineComponentProps) => ( + + + + ); + + const wrapper = mount( + + ); + const queryBarProps = wrapper.find(QueryBar).props(); + const onChangedQueryRef = queryBarProps.onChangedQuery; + const onSubmitQueryRef = queryBarProps.onSubmitQuery; + const onSavedQueryRef = queryBarProps.onSavedQuery; + + wrapper.setProps({ dataProviders: mockDataProviders.slice(1, 0) }); + wrapper.update(); + + expect(onSavedQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSavedQuery); + expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); + expect(onSubmitQueryRef).toEqual(wrapper.find(QueryBar).props().onSubmitQuery); + }); + + test('is only reference that changed when savedQueryId props get updated', () => { + const Proxy = (props: QueryBarTimelineComponentProps) => ( + + + + ); + + const wrapper = mount( + + ); + const queryBarProps = wrapper.find(QueryBar).props(); + const onChangedQueryRef = queryBarProps.onChangedQuery; + const onSubmitQueryRef = queryBarProps.onSubmitQuery; + const onSavedQueryRef = queryBarProps.onSavedQuery; + + wrapper.setProps({ + savedQueryId: 'new', + }); + wrapper.update(); + + expect(onSavedQueryRef).not.toEqual(wrapper.find(QueryBar).props().onSavedQuery); + expect(onChangedQueryRef).toEqual(wrapper.find(QueryBar).props().onChangedQuery); + expect(onSubmitQueryRef).toEqual(wrapper.find(QueryBar).props().onSubmitQuery); + }); + }); + + describe('#getDataProviderFilter', () => { + test('returns valid data provider filter with a simple bool data provider', () => { + const dataProvidersDsl = convertKueryToElasticSearchQuery( + buildGlobalQuery(mockDataProviders.slice(0, 1), mockBrowserFields), + mockIndexPattern + ); + const filter = getDataProviderFilter(dataProvidersDsl); + expect(filter).toEqual({ + $state: { + store: 'appState', + }, + bool: { + minimum_should_match: 1, + should: [ + { + match_phrase: { + name: 'Provider 1', + }, + }, + ], + }, + meta: { + alias: 'timeline-filter-drop-area', + controlledBy: 'timeline-filter-drop-area', + disabled: false, + key: 'bool', + negate: false, + type: 'custom', + value: + '{"bool":{"should":[{"match_phrase":{"name":"Provider 1"}}],"minimum_should_match":1}}', + }, + }); + }); + + test('returns valid data provider filter with an exists operator', () => { + const dataProvidersDsl = convertKueryToElasticSearchQuery( + buildGlobalQuery( + [ + { + id: `id-exists`, + name, + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'host.name', + value: '', + operator: ':*', + }, + and: [], + }, + ], + mockBrowserFields + ), + mockIndexPattern + ); + const filter = getDataProviderFilter(dataProvidersDsl); + expect(filter).toEqual({ + $state: { + store: 'appState', + }, + bool: { + minimum_should_match: 1, + should: [ + { + exists: { + field: 'host.name', + }, + }, + ], + }, + meta: { + alias: 'timeline-filter-drop-area', + controlledBy: 'timeline-filter-drop-area', + disabled: false, + key: 'bool', + negate: false, + type: 'custom', + value: '{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}}', + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/query_bar/index.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/query_bar/index.tsx new file mode 100644 index 00000000000000..07a769751cb0fc --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/query_bar/index.tsx @@ -0,0 +1,319 @@ +/* + * 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 { isEmpty } from 'lodash/fp'; +import React, { memo, useCallback, useState, useEffect } from 'react'; +import { Subscription } from 'rxjs'; +import deepEqual from 'fast-deep-equal'; + +import { + IIndexPattern, + Query, + Filter, + esFilters, + FilterManager, + SavedQuery, + SavedQueryTimeFilter, +} from '../../../../../../../../src/plugins/data/public'; + +import { BrowserFields } from '../../../../common/containers/source'; +import { convertKueryToElasticSearchQuery } from '../../../../common/lib/keury'; +import { KueryFilterQuery, KueryFilterQueryKind } from '../../../../common/store'; +import { KqlMode } from '../../../../timelines/store/timeline/model'; +import { useSavedQueryServices } from '../../../../common/utils/saved_query_services'; +import { DispatchUpdateReduxTime } from '../../../../common/components/super_date_picker'; +import { QueryBar } from '../../../../common/components/query_bar'; +import { DataProvider } from '../data_providers/data_provider'; +import { buildGlobalQuery } from '../helpers'; + +export interface QueryBarTimelineComponentProps { + applyKqlFilterQuery: (expression: string, kind: KueryFilterQueryKind) => void; + browserFields: BrowserFields; + dataProviders: DataProvider[]; + filters: Filter[]; + filterManager: FilterManager; + filterQuery: KueryFilterQuery; + filterQueryDraft: KueryFilterQuery; + from: number; + fromStr: string; + kqlMode: KqlMode; + indexPattern: IIndexPattern; + isRefreshPaused: boolean; + refreshInterval: number; + savedQueryId: string | null; + setFilters: (filters: Filter[]) => void; + setKqlFilterQueryDraft: (expression: string, kind: KueryFilterQueryKind) => void; + setSavedQueryId: (savedQueryId: string | null) => void; + timelineId: string; + to: number; + toStr: string; + updateReduxTime: DispatchUpdateReduxTime; +} + +const timelineFilterDropArea = 'timeline-filter-drop-area'; + +export const QueryBarTimeline = memo( + ({ + applyKqlFilterQuery, + browserFields, + dataProviders, + filters, + filterManager, + filterQuery, + filterQueryDraft, + from, + fromStr, + kqlMode, + indexPattern, + isRefreshPaused, + savedQueryId, + setFilters, + setKqlFilterQueryDraft, + setSavedQueryId, + refreshInterval, + timelineId, + to, + toStr, + updateReduxTime, + }) => { + const [dateRangeFrom, setDateRangeFrom] = useState( + fromStr != null ? fromStr : new Date(from).toISOString() + ); + const [dateRangeTo, setDateRangTo] = useState( + toStr != null ? toStr : new Date(to).toISOString() + ); + + const [savedQuery, setSavedQuery] = useState(null); + const [filterQueryConverted, setFilterQueryConverted] = useState({ + query: filterQuery != null ? filterQuery.expression : '', + language: filterQuery != null ? filterQuery.kind : 'kuery', + }); + const [queryBarFilters, setQueryBarFilters] = useState([]); + const [dataProvidersDsl, setDataProvidersDsl] = useState( + convertKueryToElasticSearchQuery(buildGlobalQuery(dataProviders, browserFields), indexPattern) + ); + const savedQueryServices = useSavedQueryServices(); + + useEffect(() => { + let isSubscribed = true; + const subscriptions = new Subscription(); + filterManager.setFilters(filters); + + subscriptions.add( + filterManager.getUpdates$().subscribe({ + next: () => { + if (isSubscribed) { + const filterWithoutDropArea = filterManager + .getFilters() + .filter((f: Filter) => f.meta.controlledBy !== timelineFilterDropArea); + setFilters(filterWithoutDropArea); + setQueryBarFilters(filterWithoutDropArea); + } + }, + }) + ); + + return () => { + isSubscribed = false; + subscriptions.unsubscribe(); + }; + }, []); + + useEffect(() => { + const filterWithoutDropArea = filterManager + .getFilters() + .filter((f: Filter) => f.meta.controlledBy !== timelineFilterDropArea); + if (!deepEqual(filters, filterWithoutDropArea)) { + filterManager.setFilters(filters); + } + }, [filters]); + + useEffect(() => { + setFilterQueryConverted({ + query: filterQuery != null ? filterQuery.expression : '', + language: filterQuery != null ? filterQuery.kind : 'kuery', + }); + }, [filterQuery]); + + useEffect(() => { + setDataProvidersDsl( + convertKueryToElasticSearchQuery( + buildGlobalQuery(dataProviders, browserFields), + indexPattern + ) + ); + }, [dataProviders, browserFields, indexPattern]); + + useEffect(() => { + if (fromStr != null && toStr != null) { + setDateRangeFrom(fromStr); + setDateRangTo(toStr); + } else if (from != null && to != null) { + setDateRangeFrom(new Date(from).toISOString()); + setDateRangTo(new Date(to).toISOString()); + } + }, [from, fromStr, to, toStr]); + + useEffect(() => { + let isSubscribed = true; + async function setSavedQueryByServices() { + if (savedQueryId != null && savedQueryServices != null) { + try { + // The getSavedQuery function will throw a promise rejection in + // src/legacy/core_plugins/data/public/search/search_bar/lib/saved_query_service.ts + // if the savedObjectsClient is undefined. This is happening in a test + // so I wrapped this in a try catch to keep the unhandled promise rejection + // warning from appearing in tests. + const mySavedQuery = await savedQueryServices.getSavedQuery(savedQueryId); + if (isSubscribed && mySavedQuery != null) { + setSavedQuery({ + ...mySavedQuery, + attributes: { + ...mySavedQuery.attributes, + filters: filters.filter(f => f.meta.controlledBy !== timelineFilterDropArea), + }, + }); + } + } catch (exc) { + setSavedQuery(null); + } + } else if (isSubscribed) { + setSavedQuery(null); + } + } + setSavedQueryByServices(); + return () => { + isSubscribed = false; + }; + }, [savedQueryId]); + + const onChangedQuery = useCallback( + (newQuery: Query) => { + if ( + filterQueryDraft == null || + (filterQueryDraft != null && filterQueryDraft.expression !== newQuery.query) || + filterQueryDraft.kind !== newQuery.language + ) { + setKqlFilterQueryDraft( + newQuery.query as string, + newQuery.language as KueryFilterQueryKind + ); + } + }, + [filterQueryDraft] + ); + + const onSubmitQuery = useCallback( + (newQuery: Query, timefilter?: SavedQueryTimeFilter) => { + if ( + filterQuery == null || + (filterQuery != null && filterQuery.expression !== newQuery.query) || + filterQuery.kind !== newQuery.language + ) { + setKqlFilterQueryDraft( + newQuery.query as string, + newQuery.language as KueryFilterQueryKind + ); + applyKqlFilterQuery(newQuery.query as string, newQuery.language as KueryFilterQueryKind); + } + if (timefilter != null) { + const isQuickSelection = timefilter.from.includes('now') || timefilter.to.includes('now'); + + updateReduxTime({ + id: 'timeline', + end: timefilter.to, + start: timefilter.from, + isInvalid: false, + isQuickSelection, + timelineId, + }); + } + }, + [filterQuery, timelineId] + ); + + const onSavedQuery = useCallback( + (newSavedQuery: SavedQuery | null) => { + if (newSavedQuery != null) { + if (newSavedQuery.id !== savedQueryId) { + setSavedQueryId(newSavedQuery.id); + } + if (savedQueryServices != null && dataProvidersDsl !== '') { + const dataProviderFilterExists = + newSavedQuery.attributes.filters != null + ? newSavedQuery.attributes.filters.findIndex( + f => f.meta.controlledBy === timelineFilterDropArea + ) + : -1; + savedQueryServices.saveQuery( + { + ...newSavedQuery.attributes, + filters: + newSavedQuery.attributes.filters != null + ? dataProviderFilterExists > -1 + ? [ + ...newSavedQuery.attributes.filters.slice(0, dataProviderFilterExists), + getDataProviderFilter(dataProvidersDsl), + ...newSavedQuery.attributes.filters.slice(dataProviderFilterExists + 1), + ] + : [ + ...newSavedQuery.attributes.filters, + getDataProviderFilter(dataProvidersDsl), + ] + : [], + }, + { + overwrite: true, + } + ); + } + } else { + setSavedQueryId(null); + } + }, + [dataProvidersDsl, savedQueryId, savedQueryServices] + ); + + return ( + + ); + } +); + +export const getDataProviderFilter = (dataProviderDsl: string): Filter => { + const dslObject = JSON.parse(dataProviderDsl); + const key = Object.keys(dslObject); + return { + ...dslObject, + meta: { + alias: timelineFilterDropArea, + controlledBy: timelineFilterDropArea, + negate: false, + disabled: false, + type: 'custom', + key: isEmpty(key) ? 'bool' : key[0], + value: dataProviderDsl, + }, + $state: { + store: esFilters.FilterStateStore.APP_STATE, + }, + }; +}; diff --git a/x-pack/plugins/siem/public/components/timeline/refetch_timeline.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/refetch_timeline.tsx similarity index 83% rename from x-pack/plugins/siem/public/components/timeline/refetch_timeline.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/refetch_timeline.tsx index 73c20d9b9b6b47..aef6e0df604cb8 100644 --- a/x-pack/plugins/siem/public/components/timeline/refetch_timeline.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/refetch_timeline.tsx @@ -7,9 +7,9 @@ import React, { useEffect } from 'react'; import { useDispatch } from 'react-redux'; -import { inputsModel } from '../../store'; -import { inputsActions } from '../../store/actions'; -import { InputsModelId } from '../../store/inputs/constants'; +import { inputsModel } from '../../../common/store'; +import { inputsActions } from '../../../common/store/actions'; +import { InputsModelId } from '../../../common/store/inputs/constants'; export interface TimelineRefetchProps { id: string; diff --git a/x-pack/plugins/siem/public/components/timeline/search_or_filter/helpers.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/search_or_filter/helpers.test.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/search_or_filter/helpers.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/search_or_filter/helpers.test.tsx diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/search_or_filter/helpers.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/search_or_filter/helpers.tsx new file mode 100644 index 00000000000000..77257e367c6f58 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/search_or_filter/helpers.tsx @@ -0,0 +1,90 @@ +/* + * 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 { EuiSpacer, EuiText } from '@elastic/eui'; +import React from 'react'; +import styled from 'styled-components'; + +import { AndOrBadge } from '../and_or_badge'; + +import * as i18n from './translations'; +import { KqlMode } from '../../../../timelines/store/timeline/model'; + +const AndOrContainer = styled.div` + position: relative; + top: -1px; +`; + +AndOrContainer.displayName = 'AndOrContainer'; + +interface ModeProperties { + mode: KqlMode; + description: string; + kqlBarTooltip: string; + placeholder: string; + selectText: string; +} + +export const modes: { [key in KqlMode]: ModeProperties } = { + filter: { + mode: 'filter', + description: i18n.FILTER_DESCRIPTION, + kqlBarTooltip: i18n.FILTER_KQL_TOOLTIP, + placeholder: i18n.FILTER_KQL_PLACEHOLDER, + selectText: i18n.FILTER_KQL_SELECTED_TEXT, + }, + search: { + mode: 'search', + description: i18n.SEARCH_DESCRIPTION, + kqlBarTooltip: i18n.SEARCH_KQL_TOOLTIP, + placeholder: i18n.SEARCH_KQL_PLACEHOLDER, + selectText: i18n.SEARCH_KQL_SELECTED_TEXT, + }, +}; + +export const options = [ + { + value: modes.filter.mode, + inputDisplay: ( + + + {modes.filter.selectText} + + ), + dropdownDisplay: ( + <> + + {modes.filter.selectText} + + +

{modes.filter.description}

+
+ + ), + }, + { + value: modes.search.mode, + inputDisplay: ( + + + {modes.search.selectText} + + ), + dropdownDisplay: ( + <> + + {modes.search.selectText} + + +

{modes.search.description}

+
+ + ), + }, +]; + +export const getPlaceholderText = (kqlMode: KqlMode): string => + kqlMode === 'filter' ? i18n.FILTER_KQL_PLACEHOLDER : i18n.SEARCH_KQL_PLACEHOLDER; diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/search_or_filter/index.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/search_or_filter/index.tsx new file mode 100644 index 00000000000000..22fbaadf2e816e --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/search_or_filter/index.tsx @@ -0,0 +1,242 @@ +/* + * 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 { getOr } from 'lodash/fp'; +import React, { useCallback } from 'react'; +import { connect, ConnectedProps } from 'react-redux'; +import { Dispatch } from 'redux'; +import deepEqual from 'fast-deep-equal'; + +import { + Filter, + FilterManager, + IIndexPattern, +} from '../../../../../../../../src/plugins/data/public'; +import { BrowserFields } from '../../../../common/containers/source'; +import { convertKueryToElasticSearchQuery } from '../../../../common/lib/keury'; +import { + KueryFilterQuery, + SerializedFilterQuery, + State, + inputsModel, + inputsSelectors, +} from '../../../../common/store'; +import { timelineActions, timelineSelectors } from '../../../store/timeline'; +import { KqlMode, TimelineModel, EventType } from '../../../../timelines/store/timeline/model'; +import { timelineDefaults } from '../../../../timelines/store/timeline/defaults'; +import { dispatchUpdateReduxTime } from '../../../../common/components/super_date_picker'; +import { SearchOrFilter } from './search_or_filter'; + +interface OwnProps { + browserFields: BrowserFields; + filterManager: FilterManager; + indexPattern: IIndexPattern; + timelineId: string; +} + +type Props = OwnProps & PropsFromRedux; + +const StatefulSearchOrFilterComponent = React.memo( + ({ + applyKqlFilterQuery, + browserFields, + dataProviders, + eventType, + filters, + filterManager, + filterQuery, + filterQueryDraft, + from, + fromStr, + indexPattern, + isRefreshPaused, + kqlMode, + refreshInterval, + savedQueryId, + setFilters, + setKqlFilterQueryDraft, + setSavedQueryId, + timelineId, + to, + toStr, + updateEventType, + updateKqlMode, + updateReduxTime, + }) => { + const applyFilterQueryFromKueryExpression = useCallback( + (expression: string, kind) => + applyKqlFilterQuery({ + id: timelineId, + filterQuery: { + kuery: { + kind, + expression, + }, + serializedQuery: convertKueryToElasticSearchQuery(expression, indexPattern), + }, + }), + [indexPattern, timelineId] + ); + + const setFilterQueryDraftFromKueryExpression = useCallback( + (expression: string, kind) => + setKqlFilterQueryDraft({ + id: timelineId, + filterQueryDraft: { + kind, + expression, + }, + }), + [timelineId] + ); + + const setFiltersInTimeline = useCallback( + (newFilters: Filter[]) => + setFilters({ + id: timelineId, + filters: newFilters, + }), + [timelineId] + ); + + const setSavedQueryInTimeline = useCallback( + (newSavedQueryId: string | null) => + setSavedQueryId({ + id: timelineId, + savedQueryId: newSavedQueryId, + }), + [timelineId] + ); + + const handleUpdateEventType = useCallback( + (newEventType: EventType) => + updateEventType({ + id: timelineId, + eventType: newEventType, + }), + [timelineId] + ); + + return ( + + ); + }, + (prevProps, nextProps) => { + return ( + prevProps.eventType === nextProps.eventType && + prevProps.filterManager === nextProps.filterManager && + prevProps.from === nextProps.from && + prevProps.fromStr === nextProps.fromStr && + prevProps.to === nextProps.to && + prevProps.toStr === nextProps.toStr && + prevProps.isRefreshPaused === nextProps.isRefreshPaused && + prevProps.refreshInterval === nextProps.refreshInterval && + prevProps.timelineId === nextProps.timelineId && + deepEqual(prevProps.browserFields, nextProps.browserFields) && + deepEqual(prevProps.dataProviders, nextProps.dataProviders) && + deepEqual(prevProps.filters, nextProps.filters) && + deepEqual(prevProps.filterQuery, nextProps.filterQuery) && + deepEqual(prevProps.filterQueryDraft, nextProps.filterQueryDraft) && + deepEqual(prevProps.indexPattern, nextProps.indexPattern) && + deepEqual(prevProps.kqlMode, nextProps.kqlMode) && + deepEqual(prevProps.savedQueryId, nextProps.savedQueryId) && + deepEqual(prevProps.timelineId, nextProps.timelineId) + ); + } +); +StatefulSearchOrFilterComponent.displayName = 'StatefulSearchOrFilterComponent'; + +const makeMapStateToProps = () => { + const getTimeline = timelineSelectors.getTimelineByIdSelector(); + const getKqlFilterQueryDraft = timelineSelectors.getKqlFilterQueryDraftSelector(); + const getKqlFilterQuery = timelineSelectors.getKqlFilterKuerySelector(); + const getInputsTimeline = inputsSelectors.getTimelineSelector(); + const getInputsPolicy = inputsSelectors.getTimelinePolicySelector(); + const mapStateToProps = (state: State, { timelineId }: OwnProps) => { + const timeline: TimelineModel = getTimeline(state, timelineId) ?? timelineDefaults; + const input: inputsModel.InputsRange = getInputsTimeline(state); + const policy: inputsModel.Policy = getInputsPolicy(state); + return { + dataProviders: timeline.dataProviders, + eventType: timeline.eventType ?? 'raw', + filterQuery: getKqlFilterQuery(state, timelineId)!, + filterQueryDraft: getKqlFilterQueryDraft(state, timelineId)!, + filters: timeline.filters!, + from: input.timerange.from, + fromStr: input.timerange.fromStr!, + isRefreshPaused: policy.kind === 'manual', + kqlMode: getOr('filter', 'kqlMode', timeline), + refreshInterval: policy.duration, + savedQueryId: getOr(null, 'savedQueryId', timeline), + to: input.timerange.to, + toStr: input.timerange.toStr!, + }; + }; + return mapStateToProps; +}; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + applyKqlFilterQuery: ({ id, filterQuery }: { id: string; filterQuery: SerializedFilterQuery }) => + dispatch( + timelineActions.applyKqlFilterQuery({ + id, + filterQuery, + }) + ), + updateEventType: ({ id, eventType }: { id: string; eventType: EventType }) => + dispatch(timelineActions.updateEventType({ id, eventType })), + updateKqlMode: ({ id, kqlMode }: { id: string; kqlMode: KqlMode }) => + dispatch(timelineActions.updateKqlMode({ id, kqlMode })), + setKqlFilterQueryDraft: ({ + id, + filterQueryDraft, + }: { + id: string; + filterQueryDraft: KueryFilterQuery; + }) => + dispatch( + timelineActions.setKqlFilterQueryDraft({ + id, + filterQueryDraft, + }) + ), + setSavedQueryId: ({ id, savedQueryId }: { id: string; savedQueryId: string | null }) => + dispatch(timelineActions.setSavedQueryId({ id, savedQueryId })), + setFilters: ({ id, filters }: { id: string; filters: Filter[] }) => + dispatch(timelineActions.setFilters({ id, filters })), + updateReduxTime: dispatchUpdateReduxTime(dispatch), +}); + +export const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const StatefulSearchOrFilter = connector(StatefulSearchOrFilterComponent); diff --git a/x-pack/plugins/siem/public/components/timeline/search_or_filter/pick_events.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/search_or_filter/pick_events.tsx similarity index 96% rename from x-pack/plugins/siem/public/components/timeline/search_or_filter/pick_events.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/search_or_filter/pick_events.tsx index 3117bae7452864..85097f93464b34 100644 --- a/x-pack/plugins/siem/public/components/timeline/search_or_filter/pick_events.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/search_or_filter/pick_events.tsx @@ -8,7 +8,7 @@ import { EuiHealth, EuiSuperSelect } from '@elastic/eui'; import React, { memo } from 'react'; import styled from 'styled-components'; -import { EventType } from '../../../store/timeline/model'; +import { EventType } from '../../../../timelines/store/timeline/model'; import * as i18n from './translations'; interface EventTypeOptionItem { diff --git a/x-pack/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx similarity index 93% rename from x-pack/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx index 0b8ed71135744f..388085d1361f37 100644 --- a/x-pack/plugins/siem/public/components/timeline/search_or_filter/search_or_filter.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/search_or_filter/search_or_filter.tsx @@ -8,11 +8,15 @@ import { EuiFlexGroup, EuiFlexItem, EuiSuperSelect, EuiToolTip } from '@elastic/ import React from 'react'; import styled, { createGlobalStyle } from 'styled-components'; -import { Filter, FilterManager, IIndexPattern } from '../../../../../../../src/plugins/data/public'; -import { BrowserFields } from '../../../containers/source'; -import { KueryFilterQuery, KueryFilterQueryKind } from '../../../store'; -import { KqlMode, EventType } from '../../../store/timeline/model'; -import { DispatchUpdateReduxTime } from '../../super_date_picker'; +import { + Filter, + FilterManager, + IIndexPattern, +} from '../../../../../../../../src/plugins/data/public'; +import { BrowserFields } from '../../../../common/containers/source'; +import { KueryFilterQuery, KueryFilterQueryKind } from '../../../../common/store'; +import { KqlMode, EventType } from '../../../../timelines/store/timeline/model'; +import { DispatchUpdateReduxTime } from '../../../../common/components/super_date_picker'; import { DataProvider } from '../data_providers/data_provider'; import { QueryBarTimeline } from '../query_bar'; diff --git a/x-pack/plugins/siem/public/components/timeline/search_or_filter/translations.ts b/x-pack/plugins/siem/public/timelines/components/timeline/search_or_filter/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/search_or_filter/translations.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/search_or_filter/translations.ts diff --git a/x-pack/plugins/siem/public/components/timeline/search_super_select/index.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/search_super_select/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/search_super_select/index.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/search_super_select/index.tsx diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/selectable_timeline/index.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/selectable_timeline/index.tsx new file mode 100644 index 00000000000000..fb3cb3f177ca04 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/selectable_timeline/index.tsx @@ -0,0 +1,288 @@ +/* + * 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 { + EuiSelectable, + EuiHighlight, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiTextColor, + EuiSelectableOption, + EuiPortal, + EuiFilterGroup, + EuiFilterButton, +} from '@elastic/eui'; +import { isEmpty } from 'lodash/fp'; +import React, { memo, useCallback, useMemo, useState, useEffect } from 'react'; +import { ListProps } from 'react-virtualized'; +import styled from 'styled-components'; + +import { TimelineType, TimelineTypeLiteralWithNull } from '../../../../../common/types/timeline'; + +import { useGetAllTimeline } from '../../../containers/all'; +import { SortFieldTimeline, Direction } from '../../../../graphql/types'; +import { isUntitled } from '../../open_timeline/helpers'; +import * as i18nTimeline from '../../open_timeline/translations'; +import { OpenTimelineResult } from '../../open_timeline/types'; +import { getEmptyTagValue } from '../../../../common/components/empty_value'; + +import * as i18n from '../translations'; + +const MyEuiFlexItem = styled(EuiFlexItem)` + display: inline-block; + max-width: 296px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +`; + +const MyEuiFlexGroup = styled(EuiFlexGroup)` + padding 0px 4px; +`; + +const EuiSelectableContainer = styled.div<{ isLoading: boolean }>` + .euiSelectable { + .euiFormControlLayout__childrenWrapper { + display: flex; + } + ${({ isLoading }) => `${ + isLoading + ? ` + .euiFormControlLayoutIcons { + display: none; + } + .euiFormControlLayoutIcons.euiFormControlLayoutIcons--right { + display: block; + left: 12px; + top: 12px; + }` + : '' + } + `} + } +`; + +const ORIGINAL_PAGE_SIZE = 50; +const POPOVER_HEIGHT = 260; +const TIMELINE_ITEM_HEIGHT = 50; + +export interface GetSelectableOptions { + timelines: OpenTimelineResult[]; + onlyFavorites: boolean; + timelineType?: TimelineTypeLiteralWithNull; + searchTimelineValue: string; +} + +interface SelectableTimelineProps { + hideUntitled?: boolean; + getSelectableOptions: ({ + timelines, + onlyFavorites, + timelineType, + searchTimelineValue, + }: GetSelectableOptions) => EuiSelectableOption[]; + onClosePopover: () => void; + onTimelineChange: (timelineTitle: string, timelineId: string | null) => void; +} + +const SelectableTimelineComponent: React.FC = ({ + hideUntitled = false, + getSelectableOptions, + onClosePopover, + onTimelineChange, +}) => { + const [pageSize, setPageSize] = useState(ORIGINAL_PAGE_SIZE); + const [heightTrigger, setHeightTrigger] = useState(0); + const [searchTimelineValue, setSearchTimelineValue] = useState(''); + const [onlyFavorites, setOnlyFavorites] = useState(false); + const [searchRef, setSearchRef] = useState(null); + const { fetchAllTimeline, timelines, loading, totalCount: timelineCount } = useGetAllTimeline(); + + const onSearchTimeline = useCallback(val => { + setSearchTimelineValue(val); + }, []); + + const handleOnToggleOnlyFavorites = useCallback(() => { + setOnlyFavorites(!onlyFavorites); + }, [onlyFavorites]); + + const handleOnScroll = useCallback( + ( + totalTimelines: number, + totalCount: number, + { + clientHeight, + scrollHeight, + scrollTop, + }: { + clientHeight: number; + scrollHeight: number; + scrollTop: number; + } + ) => { + if (totalTimelines < totalCount) { + const clientHeightTrigger = clientHeight * 1.2; + if ( + scrollTop > 10 && + scrollHeight - scrollTop < clientHeightTrigger && + scrollHeight > heightTrigger + ) { + setHeightTrigger(scrollHeight); + setPageSize(pageSize + ORIGINAL_PAGE_SIZE); + } + } + }, + [heightTrigger, pageSize] + ); + + const renderTimelineOption = useCallback((option, searchValue) => { + return ( + + + + + + + + + {isUntitled(option) ? i18nTimeline.UNTITLED_TIMELINE : option.title} + + + + + + {option.description != null && option.description.trim().length > 0 + ? option.description + : getEmptyTagValue()} + + + + + + + + + + ); + }, []); + + const handleTimelineChange = useCallback( + options => { + const selectedTimeline = options.filter( + (option: { checked: string }) => option.checked === 'on' + ); + if (selectedTimeline != null && selectedTimeline.length > 0) { + onTimelineChange( + isEmpty(selectedTimeline[0].title) + ? i18nTimeline.UNTITLED_TIMELINE + : selectedTimeline[0].title, + selectedTimeline[0].id === '-1' ? null : selectedTimeline[0].id + ); + } + onClosePopover(); + }, + [onClosePopover, onTimelineChange] + ); + + const favoritePortal = useMemo( + () => + searchRef != null ? ( + + + + + + {i18nTimeline.ONLY_FAVORITES} + + + + + + ) : null, + [searchRef, onlyFavorites, handleOnToggleOnlyFavorites] + ); + + useEffect(() => { + fetchAllTimeline({ + pageInfo: { + pageIndex: 1, + pageSize, + }, + search: searchTimelineValue, + sort: { + sortField: SortFieldTimeline.updated, + sortOrder: Direction.desc, + }, + onlyUserFavorite: onlyFavorites, + timelineType: TimelineType.default, + }); + }, [onlyFavorites, pageSize, searchTimelineValue]); + + return ( + + !hideUntitled || t.title !== '').length, + timelineCount + ), + } as unknown) as ListProps, + }} + renderOption={renderTimelineOption} + onChange={handleTimelineChange} + searchable + searchProps={{ + 'data-test-subj': 'timeline-super-select-search-box', + isLoading: loading, + placeholder: i18n.SEARCH_BOX_TIMELINE_PLACEHOLDER, + onSearch: onSearchTimeline, + incremental: false, + inputRef: (ref: HTMLElement) => { + setSearchRef(ref); + }, + }} + singleSelection={true} + options={getSelectableOptions({ + timelines, + onlyFavorites, + searchTimelineValue, + timelineType: TimelineType.default, + })} + > + {(list, search) => ( + <> + {search} + {favoritePortal} + {list} + + )} + + + ); +}; + +export const SelectableTimeline = memo(SelectableTimelineComponent); diff --git a/x-pack/plugins/siem/public/components/skeleton_row/__snapshots__/index.test.tsx.snap b/x-pack/plugins/siem/public/timelines/components/timeline/skeleton_row/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/siem/public/components/skeleton_row/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/siem/public/timelines/components/timeline/skeleton_row/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/siem/public/timelines/components/timeline/skeleton_row/index.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/skeleton_row/index.test.tsx new file mode 100644 index 00000000000000..b63359077bf2c9 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/components/timeline/skeleton_row/index.test.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mount, shallow } from 'enzyme'; +import React from 'react'; + +import { TestProviders } from '../../../../common/mock'; +import { SkeletonRow } from './index'; + +describe('SkeletonRow', () => { + test('it renders', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + test('it renders the correct number of cells if cellCount is specified', () => { + const wrapper = mount( + + + + ); + + expect(wrapper.find('.siemSkeletonRow__cell')).toHaveLength(10); + }); + + test('it applies row and cell styles when cellColor/cellMargin/rowHeight/rowPadding provided', () => { + const wrapper = mount( + + + + ); + const siemSkeletonRow = wrapper.find('.siemSkeletonRow').first(); + const siemSkeletonRowCell = wrapper.find('.siemSkeletonRow__cell').last(); + + expect(siemSkeletonRow).toHaveStyleRule('height', '100px'); + expect(siemSkeletonRow).toHaveStyleRule('padding', '10px'); + expect(siemSkeletonRowCell).toHaveStyleRule('background-color', 'red'); + expect(siemSkeletonRowCell).toHaveStyleRule('margin-left', '10px', { + modifier: '& + &', + }); + }); +}); diff --git a/x-pack/plugins/siem/public/components/skeleton_row/index.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/skeleton_row/index.tsx similarity index 100% rename from x-pack/plugins/siem/public/components/skeleton_row/index.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/skeleton_row/index.tsx diff --git a/x-pack/plugins/siem/public/components/timeline/styles.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/styles.tsx similarity index 98% rename from x-pack/plugins/siem/public/components/timeline/styles.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/styles.tsx index 16fb57714829cc..aad80cbdfe3372 100644 --- a/x-pack/plugins/siem/public/components/timeline/styles.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/styles.tsx @@ -8,8 +8,8 @@ import { EuiLoadingSpinner } from '@elastic/eui'; import { rgba } from 'polished'; import styled, { createGlobalStyle } from 'styled-components'; -import { EventType } from '../../store/timeline/model'; -import { IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME } from '../drag_and_drop/helpers'; +import { EventType } from '../../../timelines/store/timeline/model'; +import { IS_TIMELINE_FIELD_DRAGGING_CLASS_NAME } from '../../../common/components/drag_and_drop/helpers'; /** * TIMELINE BODY diff --git a/x-pack/plugins/siem/public/components/timeline/timeline.test.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/timeline.test.tsx similarity index 96% rename from x-pack/plugins/siem/public/components/timeline/timeline.test.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/timeline.test.tsx index 0d0ce79c77be7f..578f85fe9ddffe 100644 --- a/x-pack/plugins/siem/public/components/timeline/timeline.test.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/timeline.test.tsx @@ -9,11 +9,11 @@ import React from 'react'; import { MockedProvider } from 'react-apollo/test-utils'; import useResizeObserver from 'use-resize-observer/polyfilled'; -import { timelineQuery } from '../../containers/timeline/index.gql_query'; -import { mockBrowserFields } from '../../containers/source/mock'; -import { Direction } from '../../graphql/types'; -import { defaultHeaders, mockTimelineData, mockIndexPattern } from '../../mock'; -import { TestProviders } from '../../mock/test_providers'; +import { timelineQuery } from '../../containers/index.gql_query'; +import { mockBrowserFields } from '../../../common/containers/source/mock'; +import { Direction } from '../../../graphql/types'; +import { defaultHeaders, mockTimelineData, mockIndexPattern } from '../../../common/mock'; +import { TestProviders } from '../../../common/mock/test_providers'; import { DELETE_CLASS_NAME, @@ -23,9 +23,9 @@ import { import { TimelineComponent, Props as TimelineComponentProps } from './timeline'; import { Sort } from './body/sort'; import { mockDataProviders } from './data_providers/mock/mock_data_providers'; -import { useMountAppended } from '../../utils/use_mount_appended'; +import { useMountAppended } from '../../../common/utils/use_mount_appended'; -jest.mock('../../lib/kibana'); +jest.mock('../../../common/lib/kibana'); const mockUseResizeObserver: jest.Mock = useResizeObserver as jest.Mock; jest.mock('use-resize-observer/polyfilled'); diff --git a/x-pack/plugins/siem/public/components/timeline/timeline.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/timeline.tsx similarity index 95% rename from x-pack/plugins/siem/public/components/timeline/timeline.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/timeline.tsx index cc3116235557f7..79d86e5b556d84 100644 --- a/x-pack/plugins/siem/public/components/timeline/timeline.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/timeline.tsx @@ -10,11 +10,11 @@ import React, { useState, useMemo } from 'react'; import styled from 'styled-components'; import { FlyoutHeaderWithCloseButton } from '../flyout/header_with_close_button'; -import { BrowserFields } from '../../containers/source'; -import { TimelineQuery } from '../../containers/timeline'; -import { Direction } from '../../graphql/types'; -import { useKibana } from '../../lib/kibana'; -import { ColumnHeaderOptions, KqlMode, EventType } from '../../store/timeline/model'; +import { BrowserFields } from '../../../common/containers/source'; +import { TimelineQuery } from '../../containers/index'; +import { Direction } from '../../../graphql/types'; +import { useKibana } from '../../../common/lib/kibana'; +import { ColumnHeaderOptions, KqlMode, EventType } from '../../../timelines/store/timeline/model'; import { defaultHeaders } from './body/column_headers/default_headers'; import { Sort } from './body/sort'; import { StatefulBody } from './body/stateful_body'; @@ -37,7 +37,7 @@ import { Filter, FilterManager, IIndexPattern, -} from '../../../../../../src/plugins/data/public'; +} from '../../../../../../../src/plugins/data/public'; const TimelineContainer = styled.div` height: 100%; diff --git a/x-pack/plugins/siem/public/components/timeline/timeline_context.tsx b/x-pack/plugins/siem/public/timelines/components/timeline/timeline_context.tsx similarity index 97% rename from x-pack/plugins/siem/public/components/timeline/timeline_context.tsx rename to x-pack/plugins/siem/public/timelines/components/timeline/timeline_context.tsx index 25a0078b6066a6..7c1eadd8e8beda 100644 --- a/x-pack/plugins/siem/public/components/timeline/timeline_context.tsx +++ b/x-pack/plugins/siem/public/timelines/components/timeline/timeline_context.tsx @@ -6,7 +6,7 @@ import React, { createContext, memo, useContext, useEffect, useState } from 'react'; -import { FilterManager } from '../../../../../../src/plugins/data/public'; +import { FilterManager } from '../../../../../../../src/plugins/data/public'; import { TimelineAction } from './body/actions'; diff --git a/x-pack/plugins/siem/public/components/timeline/translations.ts b/x-pack/plugins/siem/public/timelines/components/timeline/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/components/timeline/translations.ts rename to x-pack/plugins/siem/public/timelines/components/timeline/translations.ts diff --git a/x-pack/plugins/siem/public/containers/timeline/all/index.gql_query.ts b/x-pack/plugins/siem/public/timelines/containers/all/index.gql_query.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/timeline/all/index.gql_query.ts rename to x-pack/plugins/siem/public/timelines/containers/all/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/timelines/containers/all/index.tsx b/x-pack/plugins/siem/public/timelines/containers/all/index.tsx new file mode 100644 index 00000000000000..bdab29953a249f --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/containers/all/index.tsx @@ -0,0 +1,184 @@ +/* + * 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 { getOr, noop } from 'lodash/fp'; +import memoizeOne from 'memoize-one'; +import { useCallback, useState, useEffect } from 'react'; +import { useDispatch } from 'react-redux'; + +import { OpenTimelineResult } from '../../components/open_timeline/types'; +import { errorToToaster, useStateToaster } from '../../../common/components/toasters'; +import { + GetAllTimeline, + PageInfoTimeline, + SortTimeline, + TimelineResult, +} from '../../../graphql/types'; +import { inputsActions } from '../../../common/store/inputs'; +import { useApolloClient } from '../../../common/utils/apollo_context'; + +import { allTimelinesQuery } from './index.gql_query'; +import * as i18n from '../../pages/translations'; +import { TimelineTypeLiteralWithNull } from '../../../../common/types/timeline'; + +export interface AllTimelinesArgs { + fetchAllTimeline: ({ + onlyUserFavorite, + pageInfo, + search, + sort, + timelineType, + }: AllTimelinesVariables) => void; + timelines: OpenTimelineResult[]; + loading: boolean; + totalCount: number; +} + +export interface AllTimelinesVariables { + onlyUserFavorite: boolean; + pageInfo: PageInfoTimeline; + search: string; + sort: SortTimeline; + timelineType: TimelineTypeLiteralWithNull; +} + +export const ALL_TIMELINE_QUERY_ID = 'FETCH_ALL_TIMELINES'; + +export const getAllTimeline = memoizeOne( + (variables: string, timelines: TimelineResult[]): OpenTimelineResult[] => + timelines.map(timeline => ({ + created: timeline.created, + description: timeline.description, + eventIdToNoteIds: + timeline.eventIdToNoteIds != null + ? timeline.eventIdToNoteIds.reduce((acc, note) => { + if (note.eventId != null) { + const notes = getOr([], note.eventId, acc); + return { ...acc, [note.eventId]: [...notes, note.noteId] }; + } + return acc; + }, {}) + : null, + favorite: timeline.favorite, + noteIds: timeline.noteIds, + notes: + timeline.notes != null + ? timeline.notes.map(note => ({ ...note, savedObjectId: note.noteId })) + : null, + pinnedEventIds: + timeline.pinnedEventIds != null + ? timeline.pinnedEventIds.reduce( + (acc, pinnedEventId) => ({ ...acc, [pinnedEventId]: true }), + {} + ) + : null, + savedObjectId: timeline.savedObjectId, + title: timeline.title, + updated: timeline.updated, + updatedBy: timeline.updatedBy, + })) +); + +export const useGetAllTimeline = (): AllTimelinesArgs => { + const dispatch = useDispatch(); + const apolloClient = useApolloClient(); + const [, dispatchToaster] = useStateToaster(); + const [allTimelines, setAllTimelines] = useState({ + fetchAllTimeline: noop, + loading: false, + totalCount: 0, + timelines: [], + }); + + const fetchAllTimeline = useCallback( + async ({ onlyUserFavorite, pageInfo, search, sort, timelineType }: AllTimelinesVariables) => { + let didCancel = false; + const abortCtrl = new AbortController(); + + const fetchData = async () => { + try { + if (apolloClient != null) { + setAllTimelines({ + ...allTimelines, + loading: true, + }); + + const variables: GetAllTimeline.Variables = { + onlyUserFavorite, + pageInfo, + search, + sort, + timelineType, + }; + const response = await apolloClient.query< + GetAllTimeline.Query, + GetAllTimeline.Variables + >({ + query: allTimelinesQuery, + fetchPolicy: 'network-only', + variables, + context: { + fetchOptions: { + abortSignal: abortCtrl.signal, + }, + }, + }); + const totalCount = response?.data?.getAllTimeline?.totalCount ?? 0; + const timelines = response?.data?.getAllTimeline?.timeline ?? []; + if (!didCancel) { + dispatch( + inputsActions.setQuery({ + inputId: 'global', + id: ALL_TIMELINE_QUERY_ID, + loading: false, + refetch: fetchData, + inspect: null, + }) + ); + setAllTimelines({ + fetchAllTimeline, + loading: false, + totalCount, + timelines: getAllTimeline(JSON.stringify(variables), timelines as TimelineResult[]), + }); + } + } + } catch (error) { + if (!didCancel) { + errorToToaster({ + title: i18n.ERROR_FETCHING_TIMELINES_TITLE, + error: error.body && error.body.message ? new Error(error.body.message) : error, + dispatchToaster, + }); + setAllTimelines({ + fetchAllTimeline, + loading: false, + totalCount: 0, + timelines: [], + }); + } + } + }; + fetchData(); + return () => { + didCancel = true; + abortCtrl.abort(); + }; + }, + [apolloClient, allTimelines] + ); + + useEffect(() => { + return () => { + dispatch(inputsActions.deleteOneQuery({ inputId: 'global', id: ALL_TIMELINE_QUERY_ID })); + }; + }, [dispatch]); + + return { + ...allTimelines, + fetchAllTimeline, + }; +}; diff --git a/x-pack/plugins/siem/public/timelines/containers/api.ts b/x-pack/plugins/siem/public/timelines/containers/api.ts new file mode 100644 index 00000000000000..8afbec05938eb8 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/containers/api.ts @@ -0,0 +1,118 @@ +/* + * 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 { fold } from 'fp-ts/lib/Either'; +import { identity } from 'fp-ts/lib/function'; +import { pipe } from 'fp-ts/lib/pipeable'; + +import { throwErrors } from '../../../../case/common/api'; +import { + SavedTimeline, + TimelineResponse, + TimelineResponseType, +} from '../../../common/types/timeline'; +import { TIMELINE_URL, TIMELINE_IMPORT_URL, TIMELINE_EXPORT_URL } from '../../../common/constants'; + +import { KibanaServices } from '../../common/lib/kibana'; +import { ExportSelectedData } from '../../common/components/generic_downloader'; + +import { createToasterPlainError } from '../../cases/containers/utils'; +import { + ImportDataProps, + ImportDataResponse, +} from '../../alerts/containers/detection_engine/rules'; + +interface RequestPostTimeline { + timeline: SavedTimeline; + signal?: AbortSignal; +} + +interface RequestPatchTimeline extends RequestPostTimeline { + timelineId: T; + version: T; +} + +type RequestPersistTimeline = RequestPostTimeline & Partial>; + +const decodeTimelineResponse = (respTimeline?: TimelineResponse) => + pipe( + TimelineResponseType.decode(respTimeline), + fold(throwErrors(createToasterPlainError), identity) + ); + +const postTimeline = async ({ timeline }: RequestPostTimeline): Promise => { + const response = await KibanaServices.get().http.post(TIMELINE_URL, { + method: 'POST', + body: JSON.stringify({ timeline }), + }); + + return decodeTimelineResponse(response); +}; + +const patchTimeline = async ({ + timelineId, + timeline, + version, +}: RequestPatchTimeline): Promise => { + const response = await KibanaServices.get().http.patch(TIMELINE_URL, { + method: 'PATCH', + body: JSON.stringify({ timeline, timelineId, version }), + }); + + return decodeTimelineResponse(response); +}; + +export const persistTimeline = async ({ + timelineId, + timeline, + version, +}: RequestPersistTimeline): Promise => { + if (timelineId == null) { + return postTimeline({ timeline }); + } + return patchTimeline({ + timelineId, + timeline, + version: version ?? '', + }); +}; + +export const importTimelines = async ({ + fileToImport, + overwrite = false, + signal, +}: ImportDataProps): Promise => { + const formData = new FormData(); + formData.append('file', fileToImport); + + return KibanaServices.get().http.fetch(`${TIMELINE_IMPORT_URL}`, { + method: 'POST', + headers: { 'Content-Type': undefined }, + query: { overwrite }, + body: formData, + signal, + }); +}; + +export const exportSelectedTimeline: ExportSelectedData = async ({ + excludeExportDetails = false, + filename = `timelines_export.ndjson`, + ids = [], + signal, +}): Promise => { + const body = ids.length > 0 ? JSON.stringify({ ids }) : undefined; + const response = await KibanaServices.get().http.fetch(`${TIMELINE_EXPORT_URL}`, { + method: 'POST', + body, + query: { + exclude_export_details: excludeExportDetails, + file_name: filename, + }, + signal, + asResponse: true, + }); + + return response.body!; +}; diff --git a/x-pack/plugins/siem/public/containers/timeline/delete/persist.gql_query.ts b/x-pack/plugins/siem/public/timelines/containers/delete/persist.gql_query.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/timeline/delete/persist.gql_query.ts rename to x-pack/plugins/siem/public/timelines/containers/delete/persist.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/timeline/details/index.gql_query.ts b/x-pack/plugins/siem/public/timelines/containers/details/index.gql_query.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/timeline/details/index.gql_query.ts rename to x-pack/plugins/siem/public/timelines/containers/details/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/timelines/containers/details/index.tsx b/x-pack/plugins/siem/public/timelines/containers/details/index.tsx new file mode 100644 index 00000000000000..1b84451b5cba62 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/containers/details/index.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; +import memoizeOne from 'memoize-one'; +import React from 'react'; +import { Query } from 'react-apollo'; + +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { DetailItem, GetTimelineDetailsQuery } from '../../../graphql/types'; +import { useUiSetting } from '../../../common/lib/kibana'; + +import { timelineDetailsQuery } from './index.gql_query'; + +export interface EventsArgs { + detailsData: DetailItem[] | null; + loading: boolean; +} + +export interface TimelineDetailsProps { + children?: (args: EventsArgs) => React.ReactElement; + indexName: string; + eventId: string; + executeQuery: boolean; + sourceId: string; +} + +const getDetailsEvent = memoizeOne( + (variables: string, detail: DetailItem[]): DetailItem[] => detail +); + +const TimelineDetailsQueryComponent: React.FC = ({ + children, + indexName, + eventId, + executeQuery, + sourceId, +}) => { + const variables: GetTimelineDetailsQuery.Variables = { + sourceId, + indexName, + eventId, + defaultIndex: useUiSetting(DEFAULT_INDEX_KEY), + }; + return executeQuery ? ( + + query={timelineDetailsQuery} + fetchPolicy="network-only" + notifyOnNetworkStatusChange + variables={variables} + > + {({ data, loading, refetch }) => + children!({ + loading, + detailsData: getDetailsEvent( + JSON.stringify(variables), + getOr([], 'source.TimelineDetails.data', data) + ), + }) + } +
+ ) : ( + children!({ loading: false, detailsData: null }) + ); +}; + +export const TimelineDetailsQuery = React.memo(TimelineDetailsQueryComponent); diff --git a/x-pack/plugins/siem/public/containers/timeline/favorite/persist.gql_query.ts b/x-pack/plugins/siem/public/timelines/containers/favorite/persist.gql_query.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/timeline/favorite/persist.gql_query.ts rename to x-pack/plugins/siem/public/timelines/containers/favorite/persist.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/timeline/index.gql_query.ts b/x-pack/plugins/siem/public/timelines/containers/index.gql_query.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/timeline/index.gql_query.ts rename to x-pack/plugins/siem/public/timelines/containers/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/timelines/containers/index.tsx b/x-pack/plugins/siem/public/timelines/containers/index.tsx new file mode 100644 index 00000000000000..76f6bdd36ecec0 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/containers/index.tsx @@ -0,0 +1,199 @@ +/* + * 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 { getOr, uniqBy } from 'lodash/fp'; +import memoizeOne from 'memoize-one'; +import React from 'react'; +import { Query } from 'react-apollo'; +import { compose, Dispatch } from 'redux'; +import { connect, ConnectedProps } from 'react-redux'; + +import { DEFAULT_INDEX_KEY } from '../../../common/constants'; +import { IIndexPattern } from '../../../../../../src/plugins/data/common/index_patterns'; +import { + GetTimelineQuery, + PageInfo, + SortField, + TimelineEdges, + TimelineItem, +} from '../../graphql/types'; +import { inputsModel, inputsSelectors, State } from '../../common/store'; +import { withKibana, WithKibanaProps } from '../../common/lib/kibana'; +import { createFilter } from '../../common/containers/helpers'; +import { QueryTemplate, QueryTemplateProps } from '../../common/containers/query_template'; +import { EventType } from '../../timelines/store/timeline/model'; +import { timelineQuery } from './index.gql_query'; +import { timelineActions } from '../../timelines/store/timeline'; +import { SIGNALS_PAGE_TIMELINE_ID } from '../../alerts/components/signals'; + +export interface TimelineArgs { + events: TimelineItem[]; + id: string; + inspect: inputsModel.InspectQuery; + loading: boolean; + loadMore: (cursor: string, tieBreaker: string) => void; + pageInfo: PageInfo; + refetch: inputsModel.Refetch; + totalCount: number; + getUpdatedAt: () => number; +} + +export interface CustomReduxProps { + clearSignalsState: ({ id }: { id?: string }) => void; +} + +export interface OwnProps extends QueryTemplateProps { + children?: (args: TimelineArgs) => React.ReactNode; + eventType?: EventType; + id: string; + indexPattern?: IIndexPattern; + indexToAdd?: string[]; + limit: number; + sortField: SortField; + fields: string[]; +} + +type TimelineQueryProps = OwnProps & PropsFromRedux & WithKibanaProps & CustomReduxProps; + +class TimelineQueryComponent extends QueryTemplate< + TimelineQueryProps, + GetTimelineQuery.Query, + GetTimelineQuery.Variables +> { + private updatedDate: number = Date.now(); + private memoizedTimelineEvents: (variables: string, events: TimelineEdges[]) => TimelineItem[]; + + constructor(props: TimelineQueryProps) { + super(props); + this.memoizedTimelineEvents = memoizeOne(this.getTimelineEvents); + } + + public render() { + const { + children, + clearSignalsState, + eventType = 'raw', + id, + indexPattern, + indexToAdd = [], + isInspected, + kibana, + limit, + fields, + filterQuery, + sourceId, + sortField, + } = this.props; + const defaultKibanaIndex = kibana.services.uiSettings.get(DEFAULT_INDEX_KEY); + const defaultIndex = + indexPattern == null || (indexPattern != null && indexPattern.title === '') + ? [ + ...(['all', 'raw'].includes(eventType) ? defaultKibanaIndex : []), + ...(['all', 'signal'].includes(eventType) ? indexToAdd : []), + ] + : indexPattern?.title.split(',') ?? []; + const variables: GetTimelineQuery.Variables = { + fieldRequested: fields, + filterQuery: createFilter(filterQuery), + sourceId, + pagination: { limit, cursor: null, tiebreaker: null }, + sortField, + defaultIndex, + inspect: isInspected, + }; + + return ( + + query={timelineQuery} + fetchPolicy="network-only" + notifyOnNetworkStatusChange + variables={variables} + > + {({ data, loading, fetchMore, refetch }) => { + this.setRefetch(refetch); + this.setExecuteBeforeRefetch(clearSignalsState); + this.setExecuteBeforeFetchMore(clearSignalsState); + + const timelineEdges = getOr([], 'source.Timeline.edges', data); + this.setFetchMore(fetchMore); + this.setFetchMoreOptions((newCursor: string, tiebreaker?: string) => ({ + variables: { + pagination: { + cursor: newCursor, + tiebreaker, + limit, + }, + }, + updateQuery: (prev, { fetchMoreResult }) => { + if (!fetchMoreResult) { + return prev; + } + return { + ...fetchMoreResult, + source: { + ...fetchMoreResult.source, + Timeline: { + ...fetchMoreResult.source.Timeline, + edges: uniqBy('node._id', [ + ...prev.source.Timeline.edges, + ...fetchMoreResult.source.Timeline.edges, + ]), + }, + }, + }; + }, + })); + this.updatedDate = Date.now(); + return children!({ + id, + inspect: getOr(null, 'source.Timeline.inspect', data), + refetch: this.wrappedRefetch, + loading, + totalCount: getOr(0, 'source.Timeline.totalCount', data), + pageInfo: getOr({}, 'source.Timeline.pageInfo', data), + events: this.memoizedTimelineEvents(JSON.stringify(variables), timelineEdges), + loadMore: this.wrappedLoadMore, + getUpdatedAt: this.getUpdatedAt, + }); + }} + + ); + } + + private getUpdatedAt = () => this.updatedDate; + + private getTimelineEvents = (variables: string, timelineEdges: TimelineEdges[]): TimelineItem[] => + timelineEdges.map((e: TimelineEdges) => e.node); +} + +const makeMapStateToProps = () => { + const getQuery = inputsSelectors.timelineQueryByIdSelector(); + const mapStateToProps = (state: State, { id }: OwnProps) => { + const { isInspected } = getQuery(state, id); + return { + isInspected, + }; + }; + return mapStateToProps; +}; + +const mapDispatchToProps = (dispatch: Dispatch) => ({ + clearSignalsState: ({ id }: { id?: string }) => { + if (id != null && id === SIGNALS_PAGE_TIMELINE_ID) { + dispatch(timelineActions.clearEventsLoading({ id })); + dispatch(timelineActions.clearEventsDeleted({ id })); + } + }, +}); + +const connector = connect(makeMapStateToProps, mapDispatchToProps); + +type PropsFromRedux = ConnectedProps; + +export const TimelineQuery = compose>( + connector, + withKibana +)(TimelineQueryComponent); diff --git a/x-pack/plugins/siem/public/containers/timeline/notes/persist.gql_query.ts b/x-pack/plugins/siem/public/timelines/containers/notes/persist.gql_query.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/timeline/notes/persist.gql_query.ts rename to x-pack/plugins/siem/public/timelines/containers/notes/persist.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/timeline/one/index.gql_query.ts b/x-pack/plugins/siem/public/timelines/containers/one/index.gql_query.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/timeline/one/index.gql_query.ts rename to x-pack/plugins/siem/public/timelines/containers/one/index.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/timeline/persist.gql_query.ts b/x-pack/plugins/siem/public/timelines/containers/persist.gql_query.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/timeline/persist.gql_query.ts rename to x-pack/plugins/siem/public/timelines/containers/persist.gql_query.ts diff --git a/x-pack/plugins/siem/public/containers/timeline/pinned_event/persist.gql_query.ts b/x-pack/plugins/siem/public/timelines/containers/pinned_event/persist.gql_query.ts similarity index 100% rename from x-pack/plugins/siem/public/containers/timeline/pinned_event/persist.gql_query.ts rename to x-pack/plugins/siem/public/timelines/containers/pinned_event/persist.gql_query.ts diff --git a/x-pack/plugins/siem/public/timelines/index.ts b/x-pack/plugins/siem/public/timelines/index.ts new file mode 100644 index 00000000000000..5cce258b10d16e --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SecuritySubPluginWithStore } from '../app/types'; +import { getTimelinesRoutes } from './routes'; +import { initialTimelineState, timelineReducer } from './store/timeline/reducer'; +import { TimelineState } from './store/timeline/types'; + +export class Timelines { + public setup() {} + + public start(): SecuritySubPluginWithStore<'timeline', TimelineState> { + return { + routes: getTimelinesRoutes(), + store: { + initialState: { timeline: initialTimelineState }, + reducer: { timeline: timelineReducer }, + }, + }; + } +} diff --git a/x-pack/plugins/siem/public/timelines/pages/index.tsx b/x-pack/plugins/siem/public/timelines/pages/index.tsx new file mode 100644 index 00000000000000..55b4dc16c28410 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/pages/index.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { ApolloConsumer } from 'react-apollo'; +import { Switch, Route, Redirect } from 'react-router-dom'; + +import { ChromeBreadcrumb } from '../../../../../../src/core/public'; + +import { TimelineType } from '../../../common/types/timeline'; +import { TAB_TIMELINES, TAB_TEMPLATES } from '../components/open_timeline/translations'; +import { getTimelinesUrl } from '../../common/components/link_to'; +import { TimelineRouteSpyState } from '../../common/utils/route/types'; + +import { SiemPageName } from '../../app/types'; + +import { TimelinesPage } from './timelines_page'; +import { PAGE_TITLE } from './translations'; +import { appendSearch } from '../../common/components/link_to/helpers'; +const timelinesPagePath = `/:pageName(${SiemPageName.timelines})/:tabName(${TimelineType.default}|${TimelineType.template})`; +const timelinesDefaultPath = `/${SiemPageName.timelines}/${TimelineType.default}`; + +const TabNameMappedToI18nKey: Record = { + [TimelineType.default]: TAB_TIMELINES, + [TimelineType.template]: TAB_TEMPLATES, +}; + +export const getBreadcrumbs = ( + params: TimelineRouteSpyState, + search: string[] +): ChromeBreadcrumb[] => { + let breadcrumb = [ + { + text: PAGE_TITLE, + href: `${getTimelinesUrl(appendSearch(search[1]))}`, + }, + ]; + + const tabName = params?.tabName; + if (!tabName) return breadcrumb; + + breadcrumb = [ + ...breadcrumb, + { + text: TabNameMappedToI18nKey[tabName], + href: '', + }, + ]; + return breadcrumb; +}; + +export const Timelines = React.memo(() => { + return ( + + + {client => } + + ( + + )} + /> + + ); +}); + +Timelines.displayName = 'Timelines'; diff --git a/x-pack/plugins/siem/public/pages/timelines/timelines_page.test.tsx b/x-pack/plugins/siem/public/timelines/pages/timelines_page.test.tsx similarity index 92% rename from x-pack/plugins/siem/public/pages/timelines/timelines_page.test.tsx rename to x-pack/plugins/siem/public/timelines/pages/timelines_page.test.tsx index ae95a1316a6002..0338163d8b79fb 100644 --- a/x-pack/plugins/siem/public/pages/timelines/timelines_page.test.tsx +++ b/x-pack/plugins/siem/public/timelines/pages/timelines_page.test.tsx @@ -4,15 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TimelinesPageComponent } from './timelines_page'; -import { useKibana } from '../../lib/kibana'; +import ApolloClient from 'apollo-client'; import { shallow, ShallowWrapper } from 'enzyme'; import React from 'react'; -import ApolloClient from 'apollo-client'; -jest.mock('../../pages/overview/events_by_dataset'); +import { useKibana } from '../../common/lib/kibana'; +import { TimelinesPageComponent } from './timelines_page'; + +jest.mock('../../overview/components/events_by_dataset'); -jest.mock('../../lib/kibana', () => { +jest.mock('../../common/lib/kibana', () => { return { useKibana: jest.fn(), }; @@ -21,7 +22,7 @@ describe('TimelinesPageComponent', () => { const mockAppollloClient = {} as ApolloClient; let wrapper: ShallowWrapper; - describe('If the user is authorised', () => { + describe('If the user is authorized', () => { beforeAll(() => { ((useKibana as unknown) as jest.Mock).mockReturnValue({ services: { diff --git a/x-pack/plugins/siem/public/pages/timelines/timelines_page.tsx b/x-pack/plugins/siem/public/timelines/pages/timelines_page.tsx similarity index 85% rename from x-pack/plugins/siem/public/pages/timelines/timelines_page.tsx rename to x-pack/plugins/siem/public/timelines/pages/timelines_page.tsx index 73070d2b94aace..d00aef64204516 100644 --- a/x-pack/plugins/siem/public/pages/timelines/timelines_page.tsx +++ b/x-pack/plugins/siem/public/timelines/pages/timelines_page.tsx @@ -4,17 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EuiButton } from '@elastic/eui'; import ApolloClient from 'apollo-client'; -import React, { useState, useCallback } from 'react'; +import React, { useCallback, useState } from 'react'; import styled from 'styled-components'; - -import { EuiButton } from '@elastic/eui'; -import { HeaderPage } from '../../components/header_page'; -import { StatefulOpenTimeline } from '../../components/open_timeline'; -import { WrapperPage } from '../../components/wrapper_page'; -import { SpyRoute } from '../../utils/route/spy_routes'; +import { HeaderPage } from '../../common/components/header_page'; +import { WrapperPage } from '../../common/components/wrapper_page'; +import { useKibana } from '../../common/lib/kibana'; +import { SpyRoute } from '../../common/utils/route/spy_routes'; +import { StatefulOpenTimeline } from '../components/open_timeline'; import * as i18n from './translations'; -import { useKibana } from '../../lib/kibana'; const TimelinesContainer = styled.div` width: 100%; diff --git a/x-pack/plugins/siem/public/pages/timelines/translations.ts b/x-pack/plugins/siem/public/timelines/pages/translations.ts similarity index 100% rename from x-pack/plugins/siem/public/pages/timelines/translations.ts rename to x-pack/plugins/siem/public/timelines/pages/translations.ts diff --git a/x-pack/plugins/siem/public/timelines/routes.tsx b/x-pack/plugins/siem/public/timelines/routes.tsx new file mode 100644 index 00000000000000..50b8e1b8a71189 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/routes.tsx @@ -0,0 +1,15 @@ +/* + * 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 { Route } from 'react-router-dom'; + +import { Timelines } from './pages'; +import { SiemPageName } from '../app/types'; + +export const getTimelinesRoutes = () => [ + } />, +]; diff --git a/x-pack/plugins/siem/public/timelines/store/timeline/actions.ts b/x-pack/plugins/siem/public/timelines/store/timeline/actions.ts new file mode 100644 index 00000000000000..ba62c5b93012d9 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/store/timeline/actions.ts @@ -0,0 +1,249 @@ +/* + * 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 actionCreatorFactory from 'typescript-fsa'; + +import { Filter } from '../../../../../../../src/plugins/data/public'; +import { Sort } from '../../../timelines/components/timeline/body/sort'; +import { + DataProvider, + QueryOperator, +} from '../../../timelines/components/timeline/data_providers/data_provider'; +import { KueryFilterQuery, SerializedFilterQuery } from '../../../common/store/types'; + +import { EventType, KqlMode, TimelineModel, ColumnHeaderOptions } from './model'; +import { TimelineNonEcsData } from '../../../graphql/types'; + +const actionCreator = actionCreatorFactory('x-pack/siem/local/timeline'); + +export const addHistory = actionCreator<{ id: string; historyId: string }>('ADD_HISTORY'); + +export const addNote = actionCreator<{ id: string; noteId: string }>('ADD_NOTE'); + +export const addNoteToEvent = actionCreator<{ id: string; noteId: string; eventId: string }>( + 'ADD_NOTE_TO_EVENT' +); + +export const upsertColumn = actionCreator<{ + column: ColumnHeaderOptions; + id: string; + index: number; +}>('UPSERT_COLUMN'); + +export const addProvider = actionCreator<{ id: string; provider: DataProvider }>('ADD_PROVIDER'); + +export const applyDeltaToWidth = actionCreator<{ + id: string; + delta: number; + bodyClientWidthPixels: number; + minWidthPixels: number; + maxWidthPercent: number; +}>('APPLY_DELTA_TO_WIDTH'); + +export const applyDeltaToColumnWidth = actionCreator<{ + id: string; + columnId: string; + delta: number; +}>('APPLY_DELTA_TO_COLUMN_WIDTH'); + +export const createTimeline = actionCreator<{ + id: string; + dataProviders?: DataProvider[]; + dateRange?: { + start: number; + end: number; + }; + filters?: Filter[]; + columns: ColumnHeaderOptions[]; + itemsPerPage?: number; + kqlQuery?: { + filterQuery: SerializedFilterQuery | null; + filterQueryDraft: KueryFilterQuery | null; + }; + show?: boolean; + sort?: Sort; + showCheckboxes?: boolean; + showRowRenderers?: boolean; +}>('CREATE_TIMELINE'); + +export const pinEvent = actionCreator<{ id: string; eventId: string }>('PIN_EVENT'); + +export const removeColumn = actionCreator<{ + id: string; + columnId: string; +}>('REMOVE_COLUMN'); + +export const removeProvider = actionCreator<{ + id: string; + providerId: string; + andProviderId?: string; +}>('REMOVE_PROVIDER'); + +export const showTimeline = actionCreator<{ id: string; show: boolean }>('SHOW_TIMELINE'); + +export const unPinEvent = actionCreator<{ id: string; eventId: string }>('UN_PIN_EVENT'); + +export const updateTimeline = actionCreator<{ + id: string; + timeline: TimelineModel; +}>('UPDATE_TIMELINE'); + +export const addTimeline = actionCreator<{ + id: string; + timeline: TimelineModel; +}>('ADD_TIMELINE'); + +export const startTimelineSaving = actionCreator<{ + id: string; +}>('START_TIMELINE_SAVING'); + +export const endTimelineSaving = actionCreator<{ + id: string; +}>('END_TIMELINE_SAVING'); + +export const updateIsLoading = actionCreator<{ + id: string; + isLoading: boolean; +}>('UPDATE_LOADING'); + +export const updateColumns = actionCreator<{ + id: string; + columns: ColumnHeaderOptions[]; +}>('UPDATE_COLUMNS'); + +export const updateDataProviderEnabled = actionCreator<{ + id: string; + enabled: boolean; + providerId: string; + andProviderId?: string; +}>('TOGGLE_PROVIDER_ENABLED'); + +export const updateDataProviderExcluded = actionCreator<{ + id: string; + excluded: boolean; + providerId: string; + andProviderId?: string; +}>('TOGGLE_PROVIDER_EXCLUDED'); + +export const dataProviderEdited = actionCreator<{ + andProviderId?: string; + excluded: boolean; + field: string; + id: string; + operator: QueryOperator; + providerId: string; + value: string | number; +}>('DATA_PROVIDER_EDITED'); + +export const updateDataProviderKqlQuery = actionCreator<{ + id: string; + kqlQuery: string; + providerId: string; +}>('PROVIDER_EDIT_KQL_QUERY'); + +export const updateHighlightedDropAndProviderId = actionCreator<{ + id: string; + providerId: string; +}>('UPDATE_DROP_AND_PROVIDER'); + +export const updateDescription = actionCreator<{ id: string; description: string }>( + 'UPDATE_DESCRIPTION' +); + +export const updateKqlMode = actionCreator<{ id: string; kqlMode: KqlMode }>('UPDATE_KQL_MODE'); + +export const setKqlFilterQueryDraft = actionCreator<{ + id: string; + filterQueryDraft: KueryFilterQuery; +}>('SET_KQL_FILTER_QUERY_DRAFT'); + +export const applyKqlFilterQuery = actionCreator<{ + id: string; + filterQuery: SerializedFilterQuery; +}>('APPLY_KQL_FILTER_QUERY'); + +export const updateIsFavorite = actionCreator<{ id: string; isFavorite: boolean }>( + 'UPDATE_IS_FAVORITE' +); + +export const updateIsLive = actionCreator<{ id: string; isLive: boolean }>('UPDATE_IS_LIVE'); + +export const updateItemsPerPage = actionCreator<{ id: string; itemsPerPage: number }>( + 'UPDATE_ITEMS_PER_PAGE' +); + +export const updateItemsPerPageOptions = actionCreator<{ + id: string; + itemsPerPageOptions: number[]; +}>('UPDATE_ITEMS_PER_PAGE_OPTIONS'); + +export const updateTitle = actionCreator<{ id: string; title: string }>('UPDATE_TITLE'); + +export const updatePageIndex = actionCreator<{ id: string; activePage: number }>( + 'UPDATE_PAGE_INDEX' +); + +export const updateProviders = actionCreator<{ id: string; providers: DataProvider[] }>( + 'UPDATE_PROVIDERS' +); + +export const updateRange = actionCreator<{ id: string; start: number; end: number }>( + 'UPDATE_RANGE' +); + +export const updateSort = actionCreator<{ id: string; sort: Sort }>('UPDATE_SORT'); + +export const updateAutoSaveMsg = actionCreator<{ + timelineId: string | null; + newTimelineModel: TimelineModel | null; +}>('UPDATE_AUTO_SAVE'); + +export const showCallOutUnauthorizedMsg = actionCreator('SHOW_CALL_OUT_UNAUTHORIZED_MSG'); + +export const setSavedQueryId = actionCreator<{ + id: string; + savedQueryId: string | null; +}>('SET_TIMELINE_SAVED_QUERY'); + +export const setFilters = actionCreator<{ + id: string; + filters: Filter[]; +}>('SET_TIMELINE_FILTERS'); + +export const setSelected = actionCreator<{ + id: string; + eventIds: Readonly>; + isSelected: boolean; + isSelectAllChecked: boolean; +}>('SET_TIMELINE_SELECTED'); + +export const clearSelected = actionCreator<{ + id: string; +}>('CLEAR_TIMELINE_SELECTED'); + +export const setEventsLoading = actionCreator<{ + id: string; + eventIds: string[]; + isLoading: boolean; +}>('SET_TIMELINE_EVENTS_LOADING'); + +export const clearEventsLoading = actionCreator<{ + id: string; +}>('CLEAR_TIMELINE_EVENTS_LOADING'); + +export const setEventsDeleted = actionCreator<{ + id: string; + eventIds: string[]; + isDeleted: boolean; +}>('SET_TIMELINE_EVENTS_DELETED'); + +export const clearEventsDeleted = actionCreator<{ + id: string; +}>('CLEAR_TIMELINE_EVENTS_DELETED'); + +export const updateEventType = actionCreator<{ id: string; eventType: EventType }>( + 'UPDATE_EVENT_TYPE' +); diff --git a/x-pack/plugins/siem/public/store/timeline/defaults.ts b/x-pack/plugins/siem/public/timelines/store/timeline/defaults.ts similarity index 80% rename from x-pack/plugins/siem/public/store/timeline/defaults.ts rename to x-pack/plugins/siem/public/timelines/store/timeline/defaults.ts index 9203720e2e28ce..e0f142bd61d03f 100644 --- a/x-pack/plugins/siem/public/store/timeline/defaults.ts +++ b/x-pack/plugins/siem/public/timelines/store/timeline/defaults.ts @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TimelineType } from '../../../common/types/timeline'; - -import { Direction } from '../../graphql/types'; -import { DEFAULT_TIMELINE_WIDTH } from '../../components/timeline/body/constants'; -import { defaultHeaders } from '../../components/timeline/body/column_headers/default_headers'; +import { TimelineType } from '../../../../common/types/timeline'; +import { Direction } from '../../../graphql/types'; +import { DEFAULT_TIMELINE_WIDTH } from '../../../timelines/components/timeline/body/constants'; +import { defaultHeaders } from '../../../timelines/components/timeline/body/column_headers/default_headers'; import { SubsetTimelineModel, TimelineModel } from './model'; export const timelineDefaults: SubsetTimelineModel & Pick = { diff --git a/x-pack/plugins/siem/public/store/timeline/epic.test.ts b/x-pack/plugins/siem/public/timelines/store/timeline/epic.test.ts similarity index 97% rename from x-pack/plugins/siem/public/store/timeline/epic.test.ts rename to x-pack/plugins/siem/public/timelines/store/timeline/epic.test.ts index 00aa20e0786009..6bee579206de45 100644 --- a/x-pack/plugins/siem/public/store/timeline/epic.test.ts +++ b/x-pack/plugins/siem/public/timelines/store/timeline/epic.test.ts @@ -4,14 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Filter, esFilters } from '../../../../../../src/plugins/data/public'; - -import { TimelineType } from '../../../common/types/timeline'; - -import { Direction } from '../../graphql/types'; - -import { TimelineModel } from './model'; +import { Filter, esFilters } from '../../../../../../../src/plugins/data/public'; +import { TimelineType } from '../../../../common/types/timeline'; +import { Direction } from '../../../graphql/types'; import { convertTimelineAsInput } from './epic'; +import { TimelineModel } from './model'; describe('Epic Timeline', () => { describe('#convertTimelineAsInput ', () => { diff --git a/x-pack/plugins/siem/public/timelines/store/timeline/epic.ts b/x-pack/plugins/siem/public/timelines/store/timeline/epic.ts new file mode 100644 index 00000000000000..7bb890292adf47 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/store/timeline/epic.ts @@ -0,0 +1,394 @@ +/* + * 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 { + get, + has, + merge as mergeObject, + set, + omit, + isObject, + toString as fpToString, +} from 'lodash/fp'; +import { Action } from 'redux'; +import { Epic } from 'redux-observable'; +import { from, Observable, empty, merge } from 'rxjs'; +import { + filter, + map, + startWith, + withLatestFrom, + debounceTime, + mergeMap, + concatMap, + delay, + takeUntil, +} from 'rxjs/operators'; + +import { esFilters, Filter, MatchAllFilter } from '../../../../../../../src/plugins/data/public'; +import { TimelineType } from '../../../../common/types/timeline'; +import { TimelineInput, ResponseTimeline, TimelineResult } from '../../../graphql/types'; +import { AppApolloClient } from '../../../common/lib/lib'; +import { addError } from '../../../common/store/app/actions'; +import { NotesById } from '../../../common/store/app/model'; +import { inputsModel } from '../../../common/store/inputs'; + +import { + applyKqlFilterQuery, + addProvider, + dataProviderEdited, + removeColumn, + removeProvider, + updateColumns, + updateEventType, + updateDataProviderEnabled, + updateDataProviderExcluded, + updateDataProviderKqlQuery, + updateDescription, + updateKqlMode, + updateProviders, + updateRange, + updateSort, + upsertColumn, + updateTimeline, + updateTitle, + updateAutoSaveMsg, + setFilters, + setSavedQueryId, + startTimelineSaving, + endTimelineSaving, + createTimeline, + addTimeline, + showCallOutUnauthorizedMsg, +} from './actions'; +import { ColumnHeaderOptions, TimelineModel } from './model'; +import { epicPersistNote, timelineNoteActionsType } from './epic_note'; +import { epicPersistPinnedEvent, timelinePinnedEventActionsType } from './epic_pinned_event'; +import { epicPersistTimelineFavorite, timelineFavoriteActionsType } from './epic_favorite'; +import { isNotNull } from './helpers'; +import { dispatcherTimelinePersistQueue } from './epic_dispatcher_timeline_persistence_queue'; +import { myEpicTimelineId } from './my_epic_timeline_id'; +import { ActionTimeline, TimelineById } from './types'; +import { persistTimeline } from '../../containers/api'; +import { ALL_TIMELINE_QUERY_ID } from '../../containers/all'; + +interface TimelineEpicDependencies { + timelineByIdSelector: (state: State) => TimelineById; + timelineTimeRangeSelector: (state: State) => inputsModel.TimeRange; + selectAllTimelineQuery: () => (state: State, id: string) => inputsModel.GlobalQuery; + selectNotesByIdSelector: (state: State) => NotesById; + apolloClient$: Observable; +} + +const timelineActionsType = [ + applyKqlFilterQuery.type, + addProvider.type, + dataProviderEdited.type, + removeColumn.type, + removeProvider.type, + setFilters.type, + setSavedQueryId.type, + updateColumns.type, + updateDataProviderEnabled.type, + updateDataProviderExcluded.type, + updateDataProviderKqlQuery.type, + updateDescription.type, + updateEventType.type, + updateKqlMode.type, + updateProviders.type, + updateSort.type, + updateTitle.type, + updateRange.type, + upsertColumn.type, +]; + +const isItAtimelineAction = (timelineId: string | undefined) => + timelineId && timelineId.toLowerCase().startsWith('timeline'); + +export const createTimelineEpic = (): Epic< + Action, + Action, + State, + TimelineEpicDependencies +> => ( + action$, + state$, + { + selectAllTimelineQuery, + selectNotesByIdSelector, + timelineByIdSelector, + timelineTimeRangeSelector, + apolloClient$, + } +) => { + const timeline$ = state$.pipe(map(timelineByIdSelector), filter(isNotNull)); + + const allTimelineQuery$ = state$.pipe( + map(state => { + const getQuery = selectAllTimelineQuery(); + return getQuery(state, ALL_TIMELINE_QUERY_ID); + }), + filter(isNotNull) + ); + + const notes$ = state$.pipe(map(selectNotesByIdSelector), filter(isNotNull)); + + const timelineTimeRange$ = state$.pipe(map(timelineTimeRangeSelector), filter(isNotNull)); + + return merge( + action$.pipe( + withLatestFrom(timeline$), + filter(([action, timeline]) => { + const timelineId: string = get('payload.id', action); + const timelineObj: TimelineModel = timeline[timelineId]; + if (action.type === addError.type) { + return true; + } + if (action.type === createTimeline.type && isItAtimelineAction(timelineId)) { + myEpicTimelineId.setTimelineId(null); + myEpicTimelineId.setTimelineVersion(null); + } else if (action.type === addTimeline.type && isItAtimelineAction(timelineId)) { + const addNewTimeline: TimelineModel = get('payload.timeline', action); + myEpicTimelineId.setTimelineId(addNewTimeline.savedObjectId); + myEpicTimelineId.setTimelineVersion(addNewTimeline.version); + return true; + } else if ( + timelineActionsType.includes(action.type) && + !timelineObj.isLoading && + isItAtimelineAction(timelineId) + ) { + return true; + } + return false; + }), + debounceTime(500), + mergeMap(([action]) => { + dispatcherTimelinePersistQueue.next({ action }); + return empty(); + }) + ), + dispatcherTimelinePersistQueue.pipe( + delay(500), + withLatestFrom(timeline$, apolloClient$, notes$, timelineTimeRange$), + concatMap(([objAction, timeline, apolloClient, notes, timelineTimeRange]) => { + const action: ActionTimeline = get('action', objAction); + const timelineId = myEpicTimelineId.getTimelineId(); + const version = myEpicTimelineId.getTimelineVersion(); + + if (timelineNoteActionsType.includes(action.type)) { + return epicPersistNote( + apolloClient, + action, + timeline, + notes, + action$, + timeline$, + notes$, + allTimelineQuery$ + ); + } else if (timelinePinnedEventActionsType.includes(action.type)) { + return epicPersistPinnedEvent( + apolloClient, + action, + timeline, + action$, + timeline$, + allTimelineQuery$ + ); + } else if (timelineFavoriteActionsType.includes(action.type)) { + return epicPersistTimelineFavorite( + apolloClient, + action, + timeline, + action$, + timeline$, + allTimelineQuery$ + ); + } else if (timelineActionsType.includes(action.type)) { + return from( + persistTimeline({ + timelineId, + version, + timeline: convertTimelineAsInput(timeline[action.payload.id], timelineTimeRange), + }) + ).pipe( + withLatestFrom(timeline$, allTimelineQuery$), + mergeMap(([result, recentTimeline, allTimelineQuery]) => { + const savedTimeline = recentTimeline[action.payload.id]; + const response: ResponseTimeline = get('data.persistTimeline', result); + const callOutMsg = response.code === 403 ? [showCallOutUnauthorizedMsg()] : []; + + if (allTimelineQuery.refetch != null) { + (allTimelineQuery.refetch as inputsModel.Refetch)(); + } + + return [ + response.code === 409 + ? updateAutoSaveMsg({ + timelineId: action.payload.id, + newTimelineModel: omitTypenameInTimeline(savedTimeline, response.timeline), + }) + : updateTimeline({ + id: action.payload.id, + timeline: { + ...savedTimeline, + savedObjectId: response.timeline.savedObjectId, + version: response.timeline.version, + timelineType: response.timeline.timelineType ?? TimelineType.default, + templateTimelineId: response.timeline.templateTimelineId ?? null, + templateTimelineVersion: response.timeline.templateTimelineVersion ?? null, + isSaving: false, + }, + }), + ...callOutMsg, + endTimelineSaving({ + id: action.payload.id, + }), + ]; + }), + startWith(startTimelineSaving({ id: action.payload.id })), + takeUntil( + action$.pipe( + withLatestFrom(timeline$), + filter(([checkAction, updatedTimeline]) => { + if ( + checkAction.type === endTimelineSaving.type && + updatedTimeline[get('payload.id', checkAction)].savedObjectId != null + ) { + myEpicTimelineId.setTimelineId( + updatedTimeline[get('payload.id', checkAction)].savedObjectId + ); + myEpicTimelineId.setTimelineVersion( + updatedTimeline[get('payload.id', checkAction)].version + ); + return true; + } + return false; + }) + ) + ) + ); + } + return empty(); + }) + ) + ); +}; + +const timelineInput: TimelineInput = { + columns: null, + dataProviders: null, + description: null, + eventType: null, + filters: null, + kqlMode: null, + kqlQuery: null, + title: null, + timelineType: TimelineType.default, + templateTimelineVersion: null, + templateTimelineId: null, + dateRange: null, + savedQueryId: null, + sort: null, +}; + +export const convertTimelineAsInput = ( + timeline: TimelineModel, + timelineTimeRange: inputsModel.TimeRange +): TimelineInput => + Object.keys(timelineInput).reduce((acc, key) => { + if (has(key, timeline)) { + if (key === 'kqlQuery') { + return set(`${key}.filterQuery`, get(`${key}.filterQuery`, timeline), acc); + } else if (key === 'dateRange') { + return set(`${key}`, { start: timelineTimeRange.from, end: timelineTimeRange.to }, acc); + } else if (key === 'columns' && get(key, timeline) != null) { + return set( + key, + get(key, timeline).map((col: ColumnHeaderOptions) => omit(['width', '__typename'], col)), + acc + ); + } else if (key === 'filters' && get(key, timeline) != null) { + const filters = get(key, timeline); + return set( + key, + filters != null + ? filters.map((myFilter: Filter) => { + const basicFilter = omit(['$state'], myFilter); + return { + ...basicFilter, + meta: { + ...basicFilter.meta, + field: + (esFilters.isMatchAllFilter(basicFilter) || + esFilters.isPhraseFilter(basicFilter) || + esFilters.isPhrasesFilter(basicFilter) || + esFilters.isRangeFilter(basicFilter)) && + basicFilter.meta.field != null + ? convertToString(basicFilter.meta.field) + : null, + value: + basicFilter.meta.value != null + ? convertToString(basicFilter.meta.value) + : null, + params: + basicFilter.meta.params != null + ? convertToString(basicFilter.meta.params) + : null, + }, + ...(esFilters.isMatchAllFilter(basicFilter) + ? { + match_all: convertToString((basicFilter as MatchAllFilter).match_all), + } + : { match_all: null }), + ...(esFilters.isMissingFilter(basicFilter) && basicFilter.missing != null + ? { missing: convertToString(basicFilter.missing) } + : { missing: null }), + ...(esFilters.isExistsFilter(basicFilter) && basicFilter.exists != null + ? { exists: convertToString(basicFilter.exists) } + : { exists: null }), + ...((esFilters.isQueryStringFilter(basicFilter) || + get('query', basicFilter) != null) && + basicFilter.query != null + ? { query: convertToString(basicFilter.query) } + : { query: null }), + ...(esFilters.isRangeFilter(basicFilter) && basicFilter.range != null + ? { range: convertToString(basicFilter.range) } + : { range: null }), + ...(esFilters.isRangeFilter(basicFilter) && + basicFilter.script != + null /* TODO remove it when PR50713 is merged || esFilters.isPhraseFilter(basicFilter) */ + ? { script: convertToString(basicFilter.script) } + : { script: null }), + }; + }) + : [], + acc + ); + } + return set(key, get(key, timeline), acc); + } + return acc; + }, timelineInput); + +const omitTypename = (key: string, value: keyof TimelineModel) => + key === '__typename' ? undefined : value; + +const omitTypenameInTimeline = ( + oldTimeline: TimelineModel, + newTimeline: TimelineResult +): TimelineModel => JSON.parse(JSON.stringify(mergeObject(oldTimeline, newTimeline)), omitTypename); + +const convertToString = (obj: unknown) => { + try { + if (isObject(obj)) { + return JSON.stringify(obj); + } + return fpToString(obj); + } catch { + return ''; + } +}; diff --git a/x-pack/plugins/siem/public/store/timeline/epic_dispatcher_timeline_persistence_queue.ts b/x-pack/plugins/siem/public/timelines/store/timeline/epic_dispatcher_timeline_persistence_queue.ts similarity index 100% rename from x-pack/plugins/siem/public/store/timeline/epic_dispatcher_timeline_persistence_queue.ts rename to x-pack/plugins/siem/public/timelines/store/timeline/epic_dispatcher_timeline_persistence_queue.ts diff --git a/x-pack/plugins/siem/public/store/timeline/epic_favorite.ts b/x-pack/plugins/siem/public/timelines/store/timeline/epic_favorite.ts similarity index 95% rename from x-pack/plugins/siem/public/store/timeline/epic_favorite.ts rename to x-pack/plugins/siem/public/timelines/store/timeline/epic_favorite.ts index 6a1dadb8a59f59..0fd8e983085f7f 100644 --- a/x-pack/plugins/siem/public/store/timeline/epic_favorite.ts +++ b/x-pack/plugins/siem/public/timelines/store/timeline/epic_favorite.ts @@ -12,9 +12,9 @@ import { Epic } from 'redux-observable'; import { from, Observable, empty } from 'rxjs'; import { filter, mergeMap, withLatestFrom, startWith, takeUntil } from 'rxjs/operators'; -import { persistTimelineFavoriteMutation } from '../../containers/timeline/favorite/persist.gql_query'; -import { PersistTimelineFavoriteMutation, ResponseFavoriteTimeline } from '../../graphql/types'; -import { addError } from '../app/actions'; +import { persistTimelineFavoriteMutation } from '../../containers/favorite/persist.gql_query'; +import { PersistTimelineFavoriteMutation, ResponseFavoriteTimeline } from '../../../graphql/types'; +import { addError } from '../../../common/store/app/actions'; import { endTimelineSaving, updateIsFavorite, @@ -26,7 +26,7 @@ import { dispatcherTimelinePersistQueue } from './epic_dispatcher_timeline_persi import { refetchQueries } from './refetch_queries'; import { myEpicTimelineId } from './my_epic_timeline_id'; import { ActionTimeline, TimelineById } from './types'; -import { inputsModel } from '../inputs'; +import { inputsModel } from '../../../common/store/inputs'; export const timelineFavoriteActionsType = [updateIsFavorite.type]; diff --git a/x-pack/plugins/siem/public/store/timeline/epic_note.ts b/x-pack/plugins/siem/public/timelines/store/timeline/epic_note.ts similarity index 93% rename from x-pack/plugins/siem/public/store/timeline/epic_note.ts rename to x-pack/plugins/siem/public/timelines/store/timeline/epic_note.ts index 3722a6ad8036ca..30b2566de1468f 100644 --- a/x-pack/plugins/siem/public/store/timeline/epic_note.ts +++ b/x-pack/plugins/siem/public/timelines/store/timeline/epic_note.ts @@ -12,11 +12,11 @@ import { Epic } from 'redux-observable'; import { from, empty, Observable } from 'rxjs'; import { filter, mergeMap, switchMap, withLatestFrom, startWith, takeUntil } from 'rxjs/operators'; -import { persistTimelineNoteMutation } from '../../containers/timeline/notes/persist.gql_query'; -import { PersistTimelineNoteMutation, ResponseNote } from '../../graphql/types'; -import { updateNote, addError } from '../app/actions'; -import { NotesById } from '../app/model'; -import { inputsModel } from '../inputs'; +import { persistTimelineNoteMutation } from '../../../timelines/containers/notes/persist.gql_query'; +import { PersistTimelineNoteMutation, ResponseNote } from '../../../graphql/types'; +import { updateNote, addError } from '../../../common/store/app/actions'; +import { NotesById } from '../../../common/store/app/model'; +import { inputsModel } from '../../../common/store/inputs'; import { addNote, diff --git a/x-pack/plugins/siem/public/store/timeline/epic_pinned_event.ts b/x-pack/plugins/siem/public/timelines/store/timeline/epic_pinned_event.ts similarity index 95% rename from x-pack/plugins/siem/public/store/timeline/epic_pinned_event.ts rename to x-pack/plugins/siem/public/timelines/store/timeline/epic_pinned_event.ts index a1281250ba72af..88c080bb78ccab 100644 --- a/x-pack/plugins/siem/public/store/timeline/epic_pinned_event.ts +++ b/x-pack/plugins/siem/public/timelines/store/timeline/epic_pinned_event.ts @@ -12,10 +12,10 @@ import { Epic } from 'redux-observable'; import { from, Observable, empty } from 'rxjs'; import { filter, mergeMap, startWith, withLatestFrom, takeUntil } from 'rxjs/operators'; -import { persistTimelinePinnedEventMutation } from '../../containers/timeline/pinned_event/persist.gql_query'; -import { PersistTimelinePinnedEventMutation, PinnedEvent } from '../../graphql/types'; -import { addError } from '../app/actions'; -import { inputsModel } from '../inputs'; +import { persistTimelinePinnedEventMutation } from '../../../timelines/containers/pinned_event/persist.gql_query'; +import { PersistTimelinePinnedEventMutation, PinnedEvent } from '../../../graphql/types'; +import { addError } from '../../../common/store/app/actions'; +import { inputsModel } from '../../../common/store/inputs'; import { pinEvent, diff --git a/x-pack/plugins/siem/public/timelines/store/timeline/helpers.ts b/x-pack/plugins/siem/public/timelines/store/timeline/helpers.ts new file mode 100644 index 00000000000000..a8821779169c70 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/store/timeline/helpers.ts @@ -0,0 +1,1324 @@ +/* + * 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 { getOr, omit, uniq, isEmpty, isEqualWith, union } from 'lodash/fp'; + +import { Filter } from '../../../../../../../src/plugins/data/public'; +import { getColumnWidthFromType } from '../../../timelines/components/timeline/body/column_headers/helpers'; +import { Sort } from '../../../timelines/components/timeline/body/sort'; +import { + DataProvider, + QueryOperator, + QueryMatch, +} from '../../../timelines/components/timeline/data_providers/data_provider'; +import { KueryFilterQuery, SerializedFilterQuery } from '../../../common/store/model'; + +import { timelineDefaults } from './defaults'; +import { ColumnHeaderOptions, KqlMode, TimelineModel, EventType } from './model'; +import { TimelineById, TimelineState } from './types'; +import { TimelineNonEcsData } from '../../../graphql/types'; + +const EMPTY_TIMELINE_BY_ID: TimelineById = {}; // stable reference + +export const isNotNull = (value: T | null): value is T => value !== null; + +export const initialTimelineState: TimelineState = { + timelineById: EMPTY_TIMELINE_BY_ID, + autoSavedWarningMsg: { + timelineId: null, + newTimelineModel: null, + }, + showCallOutUnauthorizedMsg: false, +}; + +interface AddTimelineHistoryParams { + id: string; + historyId: string; + timelineById: TimelineById; +} + +export const addTimelineHistory = ({ + id, + historyId, + timelineById, +}: AddTimelineHistoryParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + historyIds: uniq([...timeline.historyIds, historyId]), + }, + }; +}; + +interface AddTimelineNoteParams { + id: string; + noteId: string; + timelineById: TimelineById; +} + +export const addTimelineNote = ({ + id, + noteId, + timelineById, +}: AddTimelineNoteParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + noteIds: [...timeline.noteIds, noteId], + }, + }; +}; + +interface AddTimelineNoteToEventParams { + id: string; + noteId: string; + eventId: string; + timelineById: TimelineById; +} + +export const addTimelineNoteToEvent = ({ + id, + noteId, + eventId, + timelineById, +}: AddTimelineNoteToEventParams): TimelineById => { + const timeline = timelineById[id]; + const existingNoteIds = getOr([], `eventIdToNoteIds.${eventId}`, timeline); + + return { + ...timelineById, + [id]: { + ...timeline, + eventIdToNoteIds: { + ...timeline.eventIdToNoteIds, + ...{ [eventId]: uniq([...existingNoteIds, noteId]) }, + }, + }, + }; +}; + +interface AddTimelineParams { + id: string; + timeline: TimelineModel; + timelineById: TimelineById; +} + +/** + * Add a saved object timeline to the store + * and default the value to what need to be if values are null + */ +export const addTimelineToStore = ({ + id, + timeline, + timelineById, +}: AddTimelineParams): TimelineById => ({ + ...timelineById, + [id]: { + ...timeline, + isLoading: timelineById[id].isLoading, + }, +}); + +interface AddNewTimelineParams { + columns: ColumnHeaderOptions[]; + dataProviders?: DataProvider[]; + dateRange?: { + start: number; + end: number; + }; + filters?: Filter[]; + id: string; + itemsPerPage?: number; + kqlQuery?: { + filterQuery: SerializedFilterQuery | null; + filterQueryDraft: KueryFilterQuery | null; + }; + show?: boolean; + sort?: Sort; + showCheckboxes?: boolean; + showRowRenderers?: boolean; + timelineById: TimelineById; +} + +/** Adds a new `Timeline` to the provided collection of `TimelineById` */ +export const addNewTimeline = ({ + columns, + dataProviders = [], + dateRange = { start: 0, end: 0 }, + filters = timelineDefaults.filters, + id, + itemsPerPage = timelineDefaults.itemsPerPage, + kqlQuery = { filterQuery: null, filterQueryDraft: null }, + sort = timelineDefaults.sort, + show = false, + showCheckboxes = false, + showRowRenderers = true, + timelineById, +}: AddNewTimelineParams): TimelineById => ({ + ...timelineById, + [id]: { + id, + ...timelineDefaults, + columns, + dataProviders, + dateRange, + filters, + itemsPerPage, + kqlQuery, + sort, + show, + savedObjectId: null, + version: null, + isSaving: false, + isLoading: false, + showCheckboxes, + showRowRenderers, + }, +}); + +interface PinTimelineEventParams { + id: string; + eventId: string; + timelineById: TimelineById; +} + +export const pinTimelineEvent = ({ + id, + eventId, + timelineById, +}: PinTimelineEventParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + pinnedEventIds: { + ...timeline.pinnedEventIds, + ...{ [eventId]: true }, + }, + }, + }; +}; + +interface UpdateShowTimelineProps { + id: string; + show: boolean; + timelineById: TimelineById; +} + +export const updateTimelineShowTimeline = ({ + id, + show, + timelineById, +}: UpdateShowTimelineProps): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + show, + }, + }; +}; + +interface ApplyDeltaToCurrentWidthParams { + id: string; + delta: number; + bodyClientWidthPixels: number; + minWidthPixels: number; + maxWidthPercent: number; + timelineById: TimelineById; +} + +export const applyDeltaToCurrentWidth = ({ + id, + delta, + bodyClientWidthPixels, + minWidthPixels, + maxWidthPercent, + timelineById, +}: ApplyDeltaToCurrentWidthParams): TimelineById => { + const timeline = timelineById[id]; + + const requestedWidth = timeline.width + delta * -1; // raw change in width + const maxWidthPixels = (maxWidthPercent / 100) * bodyClientWidthPixels; + const clampedWidth = Math.min(requestedWidth, maxWidthPixels); + const width = Math.max(minWidthPixels, clampedWidth); // if the clamped width is smaller than the min, use the min + + return { + ...timelineById, + [id]: { + ...timeline, + width, + }, + }; +}; + +const queryMatchCustomizer = (dp1: QueryMatch, dp2: QueryMatch) => { + if (dp1.field === dp2.field && dp1.value === dp2.value && dp1.operator === dp2.operator) { + return true; + } + return false; +}; + +const addAndToProviderInTimeline = ( + id: string, + provider: DataProvider, + timeline: TimelineModel, + timelineById: TimelineById +): TimelineById => { + const alreadyExistsProviderIndex = timeline.dataProviders.findIndex( + p => p.id === timeline.highlightedDropAndProviderId + ); + const newProvider = timeline.dataProviders[alreadyExistsProviderIndex]; + const alreadyExistsAndProviderIndex = newProvider.and.findIndex(p => p.id === provider.id); + const { and, ...andProvider } = provider; + + if ( + isEqualWith(queryMatchCustomizer, newProvider.queryMatch, andProvider.queryMatch) || + (alreadyExistsAndProviderIndex === -1 && + newProvider.and.filter(itemAndProvider => + isEqualWith(queryMatchCustomizer, itemAndProvider.queryMatch, andProvider.queryMatch) + ).length > 0) + ) { + return timelineById; + } + + const dataProviders = [ + ...timeline.dataProviders.slice(0, alreadyExistsProviderIndex), + { + ...timeline.dataProviders[alreadyExistsProviderIndex], + and: + alreadyExistsAndProviderIndex > -1 + ? [ + ...newProvider.and.slice(0, alreadyExistsAndProviderIndex), + andProvider, + ...newProvider.and.slice(alreadyExistsAndProviderIndex + 1), + ] + : [...newProvider.and, andProvider], + }, + ...timeline.dataProviders.slice(alreadyExistsProviderIndex + 1), + ]; + + return { + ...timelineById, + [id]: { + ...timeline, + dataProviders, + }, + }; +}; + +const addProviderToTimeline = ( + id: string, + provider: DataProvider, + timeline: TimelineModel, + timelineById: TimelineById +): TimelineById => { + const alreadyExistsAtIndex = timeline.dataProviders.findIndex(p => p.id === provider.id); + + if (alreadyExistsAtIndex > -1 && !isEmpty(timeline.dataProviders[alreadyExistsAtIndex].and)) { + provider.id = `${provider.id}-${ + timeline.dataProviders.filter(p => p.id === provider.id).length + }`; + } + + const dataProviders = + alreadyExistsAtIndex > -1 && isEmpty(timeline.dataProviders[alreadyExistsAtIndex].and) + ? [ + ...timeline.dataProviders.slice(0, alreadyExistsAtIndex), + provider, + ...timeline.dataProviders.slice(alreadyExistsAtIndex + 1), + ] + : [...timeline.dataProviders, provider]; + + return { + ...timelineById, + [id]: { + ...timeline, + dataProviders, + }, + }; +}; + +interface AddTimelineColumnParams { + column: ColumnHeaderOptions; + id: string; + index: number; + timelineById: TimelineById; +} + +/** + * Adds or updates a column. When updating a column, it will be moved to the + * new index + */ +export const upsertTimelineColumn = ({ + column, + id, + index, + timelineById, +}: AddTimelineColumnParams): TimelineById => { + const timeline = timelineById[id]; + const alreadyExistsAtIndex = timeline.columns.findIndex(c => c.id === column.id); + + if (alreadyExistsAtIndex !== -1) { + // remove the existing entry and add the new one at the specified index + const reordered = timeline.columns.filter(c => c.id !== column.id); + reordered.splice(index, 0, column); // ⚠️ mutation + + return { + ...timelineById, + [id]: { + ...timeline, + columns: reordered, + }, + }; + } + + // add the new entry at the specified index + const columns = [...timeline.columns]; + columns.splice(index, 0, column); // ⚠️ mutation + + return { + ...timelineById, + [id]: { + ...timeline, + columns, + }, + }; +}; + +interface RemoveTimelineColumnParams { + id: string; + columnId: string; + timelineById: TimelineById; +} + +export const removeTimelineColumn = ({ + id, + columnId, + timelineById, +}: RemoveTimelineColumnParams): TimelineById => { + const timeline = timelineById[id]; + + const columns = timeline.columns.filter(c => c.id !== columnId); + + return { + ...timelineById, + [id]: { + ...timeline, + columns, + }, + }; +}; + +interface ApplyDeltaToTimelineColumnWidth { + id: string; + columnId: string; + delta: number; + timelineById: TimelineById; +} + +export const applyDeltaToTimelineColumnWidth = ({ + id, + columnId, + delta, + timelineById, +}: ApplyDeltaToTimelineColumnWidth): TimelineById => { + const timeline = timelineById[id]; + + const columnIndex = timeline.columns.findIndex(c => c.id === columnId); + if (columnIndex === -1) { + // the column was not found + return { + ...timelineById, + [id]: { + ...timeline, + }, + }; + } + const minWidthPixels = getColumnWidthFromType(timeline.columns[columnIndex].type!); + const requestedWidth = timeline.columns[columnIndex].width + delta; // raw change in width + const width = Math.max(minWidthPixels, requestedWidth); // if the requested width is smaller than the min, use the min + + const columnWithNewWidth = { + ...timeline.columns[columnIndex], + width, + }; + + const columns = [ + ...timeline.columns.slice(0, columnIndex), + columnWithNewWidth, + ...timeline.columns.slice(columnIndex + 1), + ]; + + return { + ...timelineById, + [id]: { + ...timeline, + columns, + }, + }; +}; + +interface AddTimelineProviderParams { + id: string; + provider: DataProvider; + timelineById: TimelineById; +} + +export const addTimelineProvider = ({ + id, + provider, + timelineById, +}: AddTimelineProviderParams): TimelineById => { + const timeline = timelineById[id]; + + if (timeline.highlightedDropAndProviderId !== '') { + return addAndToProviderInTimeline(id, provider, timeline, timelineById); + } else { + return addProviderToTimeline(id, provider, timeline, timelineById); + } +}; + +interface ApplyKqlFilterQueryDraftParams { + id: string; + filterQuery: SerializedFilterQuery; + timelineById: TimelineById; +} + +export const applyKqlFilterQueryDraft = ({ + id, + filterQuery, + timelineById, +}: ApplyKqlFilterQueryDraftParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + kqlQuery: { + ...timeline.kqlQuery, + filterQuery, + }, + }, + }; +}; + +interface UpdateTimelineKqlModeParams { + id: string; + kqlMode: KqlMode; + timelineById: TimelineById; +} + +export const updateTimelineKqlMode = ({ + id, + kqlMode, + timelineById, +}: UpdateTimelineKqlModeParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + kqlMode, + }, + }; +}; + +interface UpdateKqlFilterQueryDraftParams { + id: string; + filterQueryDraft: KueryFilterQuery; + timelineById: TimelineById; +} + +export const updateKqlFilterQueryDraft = ({ + id, + filterQueryDraft, + timelineById, +}: UpdateKqlFilterQueryDraftParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + kqlQuery: { + ...timeline.kqlQuery, + filterQueryDraft, + }, + }, + }; +}; + +interface UpdateTimelineColumnsParams { + id: string; + columns: ColumnHeaderOptions[]; + timelineById: TimelineById; +} + +export const updateTimelineColumns = ({ + id, + columns, + timelineById, +}: UpdateTimelineColumnsParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + columns, + }, + }; +}; + +interface UpdateTimelineDescriptionParams { + id: string; + description: string; + timelineById: TimelineById; +} + +export const updateTimelineDescription = ({ + id, + description, + timelineById, +}: UpdateTimelineDescriptionParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + description: description.endsWith(' ') ? `${description.trim()} ` : description.trim(), + }, + }; +}; + +interface UpdateTimelineTitleParams { + id: string; + title: string; + timelineById: TimelineById; +} + +export const updateTimelineTitle = ({ + id, + title, + timelineById, +}: UpdateTimelineTitleParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + title: title.endsWith(' ') ? `${title.trim()} ` : title.trim(), + }, + }; +}; + +interface UpdateTimelineEventTypeParams { + id: string; + eventType: EventType; + timelineById: TimelineById; +} + +export const updateTimelineEventType = ({ + id, + eventType, + timelineById, +}: UpdateTimelineEventTypeParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + eventType, + }, + }; +}; + +interface UpdateTimelineIsFavoriteParams { + id: string; + isFavorite: boolean; + timelineById: TimelineById; +} + +export const updateTimelineIsFavorite = ({ + id, + isFavorite, + timelineById, +}: UpdateTimelineIsFavoriteParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + isFavorite, + }, + }; +}; + +interface UpdateTimelineIsLiveParams { + id: string; + isLive: boolean; + timelineById: TimelineById; +} + +export const updateTimelineIsLive = ({ + id, + isLive, + timelineById, +}: UpdateTimelineIsLiveParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + isLive, + }, + }; +}; + +interface UpdateTimelineProvidersParams { + id: string; + providers: DataProvider[]; + timelineById: TimelineById; +} + +export const updateTimelineProviders = ({ + id, + providers, + timelineById, +}: UpdateTimelineProvidersParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + dataProviders: providers, + }, + }; +}; + +interface UpdateTimelineRangeParams { + id: string; + start: number; + end: number; + timelineById: TimelineById; +} + +export const updateTimelineRange = ({ + id, + start, + end, + timelineById, +}: UpdateTimelineRangeParams): TimelineById => { + const timeline = timelineById[id]; + return { + ...timelineById, + [id]: { + ...timeline, + dateRange: { + start, + end, + }, + }, + }; +}; + +interface UpdateTimelineSortParams { + id: string; + sort: Sort; + timelineById: TimelineById; +} + +export const updateTimelineSort = ({ + id, + sort, + timelineById, +}: UpdateTimelineSortParams): TimelineById => { + const timeline = timelineById[id]; + return { + ...timelineById, + [id]: { + ...timeline, + sort, + }, + }; +}; + +const updateEnabledAndProvider = ( + andProviderId: string, + enabled: boolean, + providerId: string, + timeline: TimelineModel +) => + timeline.dataProviders.map(provider => + provider.id === providerId + ? { + ...provider, + and: provider.and.map(andProvider => + andProvider.id === andProviderId ? { ...andProvider, enabled } : andProvider + ), + } + : provider + ); + +const updateEnabledProvider = (enabled: boolean, providerId: string, timeline: TimelineModel) => + timeline.dataProviders.map(provider => + provider.id === providerId + ? { + ...provider, + enabled, + } + : provider + ); + +interface UpdateTimelineProviderEnabledParams { + id: string; + providerId: string; + enabled: boolean; + timelineById: TimelineById; + andProviderId?: string; +} + +export const updateTimelineProviderEnabled = ({ + id, + providerId, + enabled, + timelineById, + andProviderId, +}: UpdateTimelineProviderEnabledParams): TimelineById => { + const timeline = timelineById[id]; + return { + ...timelineById, + [id]: { + ...timeline, + dataProviders: andProviderId + ? updateEnabledAndProvider(andProviderId, enabled, providerId, timeline) + : updateEnabledProvider(enabled, providerId, timeline), + }, + }; +}; + +const updateExcludedAndProvider = ( + andProviderId: string, + excluded: boolean, + providerId: string, + timeline: TimelineModel +) => + timeline.dataProviders.map(provider => + provider.id === providerId + ? { + ...provider, + and: provider.and.map(andProvider => + andProvider.id === andProviderId ? { ...andProvider, excluded } : andProvider + ), + } + : provider + ); + +const updateExcludedProvider = (excluded: boolean, providerId: string, timeline: TimelineModel) => + timeline.dataProviders.map(provider => + provider.id === providerId + ? { + ...provider, + excluded, + } + : provider + ); + +interface UpdateTimelineProviderExcludedParams { + id: string; + providerId: string; + excluded: boolean; + timelineById: TimelineById; + andProviderId?: string; +} + +export const updateTimelineProviderExcluded = ({ + id, + providerId, + excluded, + timelineById, + andProviderId, +}: UpdateTimelineProviderExcludedParams): TimelineById => { + const timeline = timelineById[id]; + return { + ...timelineById, + [id]: { + ...timeline, + dataProviders: andProviderId + ? updateExcludedAndProvider(andProviderId, excluded, providerId, timeline) + : updateExcludedProvider(excluded, providerId, timeline), + }, + }; +}; + +const updateProviderProperties = ({ + excluded, + field, + operator, + providerId, + timeline, + value, +}: { + excluded: boolean; + field: string; + operator: QueryOperator; + providerId: string; + timeline: TimelineModel; + value: string | number; +}) => + timeline.dataProviders.map(provider => + provider.id === providerId + ? { + ...provider, + excluded, + queryMatch: { + ...provider.queryMatch, + field, + displayField: field, + value, + displayValue: value, + operator, + }, + } + : provider + ); + +const updateAndProviderProperties = ({ + andProviderId, + excluded, + field, + operator, + providerId, + timeline, + value, +}: { + andProviderId: string; + excluded: boolean; + field: string; + operator: QueryOperator; + providerId: string; + timeline: TimelineModel; + value: string | number; +}) => + timeline.dataProviders.map(provider => + provider.id === providerId + ? { + ...provider, + and: provider.and.map(andProvider => + andProvider.id === andProviderId + ? { + ...andProvider, + excluded, + queryMatch: { + ...andProvider.queryMatch, + field, + displayField: field, + value, + displayValue: value, + operator, + }, + } + : andProvider + ), + } + : provider + ); + +interface UpdateTimelineProviderEditPropertiesParams { + andProviderId?: string; + excluded: boolean; + field: string; + id: string; + operator: QueryOperator; + providerId: string; + timelineById: TimelineById; + value: string | number; +} + +export const updateTimelineProviderProperties = ({ + andProviderId, + excluded, + field, + id, + operator, + providerId, + timelineById, + value, +}: UpdateTimelineProviderEditPropertiesParams): TimelineById => { + const timeline = timelineById[id]; + return { + ...timelineById, + [id]: { + ...timeline, + dataProviders: andProviderId + ? updateAndProviderProperties({ + andProviderId, + excluded, + field, + operator, + providerId, + timeline, + value, + }) + : updateProviderProperties({ + excluded, + field, + operator, + providerId, + timeline, + value, + }), + }, + }; +}; + +interface UpdateTimelineProviderKqlQueryParams { + id: string; + providerId: string; + kqlQuery: string; + timelineById: TimelineById; +} + +export const updateTimelineProviderKqlQuery = ({ + id, + providerId, + kqlQuery, + timelineById, +}: UpdateTimelineProviderKqlQueryParams): TimelineById => { + const timeline = timelineById[id]; + return { + ...timelineById, + [id]: { + ...timeline, + dataProviders: timeline.dataProviders.map(provider => + provider.id === providerId ? { ...provider, ...{ kqlQuery } } : provider + ), + }, + }; +}; + +interface UpdateTimelineItemsPerPageParams { + id: string; + itemsPerPage: number; + timelineById: TimelineById; +} + +export const updateTimelineItemsPerPage = ({ + id, + itemsPerPage, + timelineById, +}: UpdateTimelineItemsPerPageParams) => { + const timeline = timelineById[id]; + return { + ...timelineById, + [id]: { + ...timeline, + itemsPerPage, + }, + }; +}; + +interface UpdateTimelinePageIndexParams { + id: string; + activePage: number; + timelineById: TimelineById; +} + +export const updateTimelinePageIndex = ({ + id, + activePage, + timelineById, +}: UpdateTimelinePageIndexParams) => { + const timeline = timelineById[id]; + return { + ...timelineById, + [id]: { + ...timeline, + activePage, + }, + }; +}; + +interface UpdateTimelinePerPageOptionsParams { + id: string; + itemsPerPageOptions: number[]; + timelineById: TimelineById; +} + +export const updateTimelinePerPageOptions = ({ + id, + itemsPerPageOptions, + timelineById, +}: UpdateTimelinePerPageOptionsParams) => { + const timeline = timelineById[id]; + return { + ...timelineById, + [id]: { + ...timeline, + itemsPerPageOptions, + }, + }; +}; + +const removeAndProvider = (andProviderId: string, providerId: string, timeline: TimelineModel) => { + const providerIndex = timeline.dataProviders.findIndex(p => p.id === providerId); + const providerAndIndex = timeline.dataProviders[providerIndex].and.findIndex( + p => p.id === andProviderId + ); + return [ + ...timeline.dataProviders.slice(0, providerIndex), + { + ...timeline.dataProviders[providerIndex], + and: [ + ...timeline.dataProviders[providerIndex].and.slice(0, providerAndIndex), + ...timeline.dataProviders[providerIndex].and.slice(providerAndIndex + 1), + ], + }, + ...timeline.dataProviders.slice(providerIndex + 1), + ]; +}; + +const removeProvider = (providerId: string, timeline: TimelineModel) => { + const providerIndex = timeline.dataProviders.findIndex(p => p.id === providerId); + return [ + ...timeline.dataProviders.slice(0, providerIndex), + ...(timeline.dataProviders[providerIndex].and.length + ? [ + { + ...timeline.dataProviders[providerIndex].and.slice(0, 1)[0], + and: [...timeline.dataProviders[providerIndex].and.slice(1)], + }, + ] + : []), + ...timeline.dataProviders.slice(providerIndex + 1), + ]; +}; + +interface RemoveTimelineProviderParams { + id: string; + providerId: string; + timelineById: TimelineById; + andProviderId?: string; +} + +export const removeTimelineProvider = ({ + id, + providerId, + timelineById, + andProviderId, +}: RemoveTimelineProviderParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + dataProviders: andProviderId + ? removeAndProvider(andProviderId, providerId, timeline) + : removeProvider(providerId, timeline), + }, + }; +}; + +interface SetDeletedTimelineEventsParams { + id: string; + eventIds: string[]; + isDeleted: boolean; + timelineById: TimelineById; +} + +export const setDeletedTimelineEvents = ({ + id, + eventIds, + isDeleted, + timelineById, +}: SetDeletedTimelineEventsParams): TimelineById => { + const timeline = timelineById[id]; + + const deletedEventIds = isDeleted + ? union(timeline.deletedEventIds, eventIds) + : timeline.deletedEventIds.filter(currentEventId => !eventIds.includes(currentEventId)); + + const selectedEventIds = Object.fromEntries( + Object.entries(timeline.selectedEventIds).filter( + ([selectedEventId]) => !deletedEventIds.includes(selectedEventId) + ) + ); + + const isSelectAllChecked = + Object.keys(selectedEventIds).length > 0 ? timeline.isSelectAllChecked : false; + + return { + ...timelineById, + [id]: { + ...timeline, + deletedEventIds, + selectedEventIds, + isSelectAllChecked, + }, + }; +}; + +interface SetLoadingTimelineEventsParams { + id: string; + eventIds: string[]; + isLoading: boolean; + timelineById: TimelineById; +} + +export const setLoadingTimelineEvents = ({ + id, + eventIds, + isLoading, + timelineById, +}: SetLoadingTimelineEventsParams): TimelineById => { + const timeline = timelineById[id]; + + const loadingEventIds = isLoading + ? union(timeline.loadingEventIds, eventIds) + : timeline.loadingEventIds.filter(currentEventId => !eventIds.includes(currentEventId)); + + return { + ...timelineById, + [id]: { + ...timeline, + loadingEventIds, + }, + }; +}; + +interface SetSelectedTimelineEventsParams { + id: string; + eventIds: Record; + isSelectAllChecked: boolean; + isSelected: boolean; + timelineById: TimelineById; +} + +export const setSelectedTimelineEvents = ({ + id, + eventIds, + isSelectAllChecked = false, + isSelected, + timelineById, +}: SetSelectedTimelineEventsParams): TimelineById => { + const timeline = timelineById[id]; + + const selectedEventIds = isSelected + ? { ...timeline.selectedEventIds, ...eventIds } + : omit(Object.keys(eventIds), timeline.selectedEventIds); + + return { + ...timelineById, + [id]: { + ...timeline, + selectedEventIds, + isSelectAllChecked, + }, + }; +}; + +interface UnPinTimelineEventParams { + id: string; + eventId: string; + timelineById: TimelineById; +} + +export const unPinTimelineEvent = ({ + id, + eventId, + timelineById, +}: UnPinTimelineEventParams): TimelineById => { + const timeline = timelineById[id]; + return { + ...timelineById, + [id]: { + ...timeline, + pinnedEventIds: omit(eventId, timeline.pinnedEventIds), + }, + }; +}; + +interface UpdateHighlightedDropAndProviderIdParams { + id: string; + providerId: string; + timelineById: TimelineById; +} + +export const updateHighlightedDropAndProvider = ({ + id, + providerId, + timelineById, +}: UpdateHighlightedDropAndProviderIdParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + highlightedDropAndProviderId: providerId, + }, + }; +}; + +interface UpdateSavedQueryParams { + id: string; + savedQueryId: string | null; + timelineById: TimelineById; +} + +export const updateSavedQuery = ({ + id, + savedQueryId, + timelineById, +}: UpdateSavedQueryParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + savedQueryId, + }, + }; +}; + +interface UpdateFiltersParams { + id: string; + filters: Filter[]; + timelineById: TimelineById; +} + +export const updateFilters = ({ id, filters, timelineById }: UpdateFiltersParams): TimelineById => { + const timeline = timelineById[id]; + + return { + ...timelineById, + [id]: { + ...timeline, + filters, + }, + }; +}; diff --git a/x-pack/plugins/siem/public/timelines/store/timeline/index.ts b/x-pack/plugins/siem/public/timelines/store/timeline/index.ts new file mode 100644 index 00000000000000..48042ddf899108 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/store/timeline/index.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 { AnyAction, Reducer } from 'redux'; +import * as timelineActions from './actions'; +import * as timelineSelectors from './selectors'; +import { TimelineState } from './types'; + +export { timelineActions, timelineSelectors }; + +export interface TimelinePluginState { + timeline: TimelineState; +} + +export interface TimelinePluginReducer { + timeline: Reducer; +} diff --git a/x-pack/plugins/siem/public/store/timeline/manage_timeline_id.tsx b/x-pack/plugins/siem/public/timelines/store/timeline/manage_timeline_id.tsx similarity index 100% rename from x-pack/plugins/siem/public/store/timeline/manage_timeline_id.tsx rename to x-pack/plugins/siem/public/timelines/store/timeline/manage_timeline_id.tsx diff --git a/x-pack/plugins/siem/public/timelines/store/timeline/model.ts b/x-pack/plugins/siem/public/timelines/store/timeline/model.ts new file mode 100644 index 00000000000000..1957abafbcc716 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/store/timeline/model.ts @@ -0,0 +1,160 @@ +/* + * 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 { Filter } from '../../../../../../../src/plugins/data/public'; + +import { TimelineTypeLiteralWithNull } from '../../../../common/types/timeline'; +import { DataProvider } from '../../../timelines/components/timeline/data_providers/data_provider'; +import { Sort } from '../../../timelines/components/timeline/body/sort'; +import { PinnedEvent, TimelineNonEcsData } from '../../../graphql/types'; +import { KueryFilterQuery, SerializedFilterQuery } from '../../../common/store/model'; + +export const DEFAULT_PAGE_COUNT = 2; // Eui Pager will not render unless this is a minimum of 2 pages +export type KqlMode = 'filter' | 'search'; +export type EventType = 'all' | 'raw' | 'signal'; + +export type ColumnHeaderType = 'not-filtered' | 'text-filter'; + +/** Uniquely identifies a column */ +export type ColumnId = string; + +/** The specification of a column header */ +export interface ColumnHeaderOptions { + aggregatable?: boolean; + category?: string; + columnHeaderType: ColumnHeaderType; + description?: string; + example?: string; + format?: string; + id: ColumnId; + label?: string; + linkField?: string; + placeholder?: string; + type?: string; + width: number; +} + +export interface TimelineModel { + /** The columns displayed in the timeline */ + columns: ColumnHeaderOptions[]; + /** The sources of the event data shown in the timeline */ + dataProviders: DataProvider[]; + /** Events to not be rendered **/ + deletedEventIds: string[]; + /** A summary of the events and notes in this timeline */ + description: string; + /** Typoe of event you want to see in this timeline */ + eventType?: EventType; + /** A map of events in this timeline to the chronologically ordered notes (in this timeline) associated with the event */ + eventIdToNoteIds: Record; + filters?: Filter[]; + /** The chronological history of actions related to this timeline */ + historyIds: string[]; + /** The chronological history of actions related to this timeline */ + highlightedDropAndProviderId: string; + /** Uniquely identifies the timeline */ + id: string; + /** If selectAll checkbox in header is checked **/ + isSelectAllChecked: boolean; + /** Events to be rendered as loading **/ + loadingEventIds: string[]; + savedObjectId: string | null; + /** When true, this timeline was marked as "favorite" by the user */ + isFavorite: boolean; + /** When true, the timeline will update as new data arrives */ + isLive: boolean; + /** The number of items to show in a single page of results */ + itemsPerPage: number; + /** Displays a series of choices that when selected, become the value of `itemsPerPage` */ + itemsPerPageOptions: number[]; + /** determines the behavior of the KQL bar */ + kqlMode: KqlMode; + /** the KQL query in the KQL bar */ + kqlQuery: { + filterQuery: SerializedFilterQuery | null; + filterQueryDraft: KueryFilterQuery | null; + }; + /** Title */ + title: string; + /** timelineType: default | template */ + timelineType: TimelineTypeLiteralWithNull; + /** an unique id for template timeline */ + templateTimelineId: string | null; + /** null for default timeline, number for template timeline */ + templateTimelineVersion: number | null; + /** Notes added to the timeline itself. Notes added to events are stored (separately) in `eventIdToNote` */ + noteIds: string[]; + /** Events pinned to this timeline */ + pinnedEventIds: Record; + pinnedEventsSaveObject: Record; + /** Specifies the granularity of the date range (e.g. 1 Day / Week / Month) applicable to the mini-map */ + dateRange: { + start: number; + end: number; + }; + savedQueryId?: string | null; + /** Events selected on this timeline -- eventId to TimelineNonEcsData[] mapping of data required for batch actions **/ + selectedEventIds: Record; + /** When true, show the timeline flyover */ + show: boolean; + /** When true, shows checkboxes enabling selection. Selected events store in selectedEventIds **/ + showCheckboxes: boolean; + /** When true, shows additional rowRenderers below the PlainRowRenderer **/ + showRowRenderers: boolean; + /** Specifies which column the timeline is sorted on, and the direction (ascending / descending) */ + sort: Sort; + /** Persists the UI state (width) of the timeline flyover */ + width: number; + /** timeline is saving */ + isSaving: boolean; + isLoading: boolean; + version: string | null; +} + +export type SubsetTimelineModel = Readonly< + Pick< + TimelineModel, + | 'columns' + | 'dataProviders' + | 'deletedEventIds' + | 'description' + | 'eventType' + | 'eventIdToNoteIds' + | 'highlightedDropAndProviderId' + | 'historyIds' + | 'isFavorite' + | 'isLive' + | 'isSelectAllChecked' + | 'itemsPerPage' + | 'itemsPerPageOptions' + | 'kqlMode' + | 'kqlQuery' + | 'title' + | 'timelineType' + | 'templateTimelineId' + | 'templateTimelineVersion' + | 'loadingEventIds' + | 'noteIds' + | 'pinnedEventIds' + | 'pinnedEventsSaveObject' + | 'dateRange' + | 'selectedEventIds' + | 'show' + | 'showCheckboxes' + | 'showRowRenderers' + | 'sort' + | 'width' + | 'isSaving' + | 'isLoading' + | 'savedObjectId' + | 'version' + > +>; + +export interface TimelineUrl { + id: string; + isOpen: boolean; +} diff --git a/x-pack/plugins/siem/public/store/timeline/my_epic_timeline_id.ts b/x-pack/plugins/siem/public/timelines/store/timeline/my_epic_timeline_id.ts similarity index 100% rename from x-pack/plugins/siem/public/store/timeline/my_epic_timeline_id.ts rename to x-pack/plugins/siem/public/timelines/store/timeline/my_epic_timeline_id.ts diff --git a/x-pack/plugins/siem/public/timelines/store/timeline/reducer.test.ts b/x-pack/plugins/siem/public/timelines/store/timeline/reducer.test.ts new file mode 100644 index 00000000000000..65c78ca8efdb2a --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/store/timeline/reducer.test.ts @@ -0,0 +1,2254 @@ +/* + * 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 { cloneDeep, set } from 'lodash/fp'; + +import { TimelineType } from '../../../../common/types/timeline'; + +import { + IS_OPERATOR, + DataProvider, + DataProvidersAnd, +} from '../../../timelines/components/timeline/data_providers/data_provider'; +import { defaultColumnHeaderType } from '../../../timelines/components/timeline/body/column_headers/default_headers'; +import { + DEFAULT_COLUMN_MIN_WIDTH, + DEFAULT_TIMELINE_WIDTH, +} from '../../../timelines/components/timeline/body/constants'; +import { getColumnWidthFromType } from '../../../timelines/components/timeline/body/column_headers/helpers'; +import { Direction } from '../../../graphql/types'; +import { defaultHeaders } from '../../../common/mock'; + +import { + addNewTimeline, + addTimelineProvider, + addTimelineToStore, + applyDeltaToTimelineColumnWidth, + removeTimelineColumn, + removeTimelineProvider, + updateTimelineColumns, + updateTimelineDescription, + updateTimelineItemsPerPage, + updateTimelinePerPageOptions, + updateTimelineProviderEnabled, + updateTimelineProviderExcluded, + updateTimelineProviders, + updateTimelineRange, + updateTimelineShowTimeline, + updateTimelineSort, + updateTimelineTitle, + upsertTimelineColumn, +} from './helpers'; +import { ColumnHeaderOptions } from './model'; +import { timelineDefaults } from './defaults'; +import { TimelineById } from './types'; + +const timelineByIdMock: TimelineById = { + foo: { + dataProviders: [ + { + and: [], + id: '123', + name: 'data provider 1', + enabled: true, + queryMatch: { + field: '', + value: '', + operator: IS_OPERATOR, + }, + + excluded: false, + kqlQuery: '', + }, + ], + columns: [], + description: '', + deletedEventIds: [], + eventIdToNoteIds: {}, + highlightedDropAndProviderId: '', + historyIds: [], + id: 'foo', + savedObjectId: null, + isFavorite: false, + isLive: false, + isSelectAllChecked: false, + isLoading: false, + itemsPerPage: 25, + itemsPerPageOptions: [10, 25, 50], + kqlMode: 'filter', + kqlQuery: { filterQuery: null, filterQueryDraft: null }, + loadingEventIds: [], + title: '', + timelineType: TimelineType.default, + templateTimelineVersion: null, + templateTimelineId: null, + noteIds: [], + pinnedEventIds: {}, + pinnedEventsSaveObject: {}, + dateRange: { + start: 0, + end: 0, + }, + selectedEventIds: {}, + show: true, + showCheckboxes: false, + showRowRenderers: true, + sort: { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + width: DEFAULT_TIMELINE_WIDTH, + isSaving: false, + version: null, + }, +}; + +const columnsMock: ColumnHeaderOptions[] = [ + defaultHeaders[0], + defaultHeaders[1], + defaultHeaders[2], +]; + +describe('Timeline', () => { + describe('#add saved object Timeline to store ', () => { + test('should return a timelineModel with default value and not just a timelineResult ', () => { + const update = addTimelineToStore({ + id: 'foo', + timeline: { + ...timelineByIdMock.foo, + }, + timelineById: timelineByIdMock, + }); + + expect(update).toEqual({ + foo: { + ...timelineByIdMock.foo, + show: true, + }, + }); + }); + }); + + describe('#addNewTimeline', () => { + test('should return a new reference and not the same reference', () => { + const update = addNewTimeline({ + id: 'bar', + columns: defaultHeaders, + timelineById: timelineByIdMock, + }); + expect(update).not.toBe(timelineByIdMock); + }); + + test('should add a new timeline', () => { + const update = addNewTimeline({ + id: 'bar', + columns: timelineDefaults.columns, + timelineById: timelineByIdMock, + }); + expect(update).toEqual({ + foo: timelineByIdMock.foo, + bar: set('id', 'bar', timelineDefaults), + }); + }); + + test('should add the specified columns to the timeline', () => { + const barWithEmptyColumns = set('id', 'bar', timelineDefaults); + const barWithPopulatedColumns = set('columns', defaultHeaders, barWithEmptyColumns); + + const update = addNewTimeline({ + id: 'bar', + columns: defaultHeaders, + timelineById: timelineByIdMock, + }); + expect(update).toEqual({ + foo: timelineByIdMock.foo, + bar: barWithPopulatedColumns, + }); + }); + }); + + describe('#updateTimelineShowTimeline', () => { + test('should return a new reference and not the same reference', () => { + const update = updateTimelineShowTimeline({ + id: 'foo', + show: false, + timelineById: timelineByIdMock, + }); + expect(update).not.toBe(timelineByIdMock); + }); + + test('should change show from true to false', () => { + const update = updateTimelineShowTimeline({ + id: 'foo', + show: false, // value we are changing from true to false + timelineById: timelineByIdMock, + }); + expect(update).toEqual(set('foo.show', false, timelineByIdMock)); + }); + }); + + describe('#upsertTimelineColumn', () => { + let timelineById: TimelineById = {}; + let columns: ColumnHeaderOptions[] = []; + let columnToAdd: ColumnHeaderOptions; + + beforeEach(() => { + timelineById = cloneDeep(timelineByIdMock); + columns = cloneDeep(columnsMock); + columnToAdd = { + category: 'event', + columnHeaderType: defaultColumnHeaderType, + description: + 'The action captured by the event.\nThis describes the information in the event. It is more specific than `event.category`. Examples are `group-add`, `process-started`, `file-created`. The value is normally defined by the implementer.', + example: 'user-password-change', + id: 'event.action', + type: 'keyword', + aggregatable: true, + width: DEFAULT_COLUMN_MIN_WIDTH, + }; + }); + + test('should return a new reference and not the same reference', () => { + const update = upsertTimelineColumn({ + column: columnToAdd, + id: 'foo', + index: 0, + timelineById, + }); + + expect(update).not.toBe(timelineById); + }); + + test('should add a new column to an empty collection of columns', () => { + const expectedColumns = [columnToAdd]; + const update = upsertTimelineColumn({ + column: columnToAdd, + id: 'foo', + index: 0, + timelineById, + }); + + expect(update).toEqual(set('foo.columns', expectedColumns, timelineById)); + }); + + test('should add a new column to an existing collection of columns at the beginning of the collection', () => { + const expectedColumns = [columnToAdd, ...columns]; + const mockWithExistingColumns = set('foo.columns', columns, timelineById); + + const update = upsertTimelineColumn({ + column: columnToAdd, + id: 'foo', + index: 0, + timelineById: mockWithExistingColumns, + }); + + expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); + }); + + test('should add a new column to an existing collection of columns in the middle of the collection', () => { + const expectedColumns = [columns[0], columnToAdd, columns[1], columns[2]]; + const mockWithExistingColumns = set('foo.columns', columns, timelineById); + + const update = upsertTimelineColumn({ + column: columnToAdd, + id: 'foo', + index: 1, + timelineById: mockWithExistingColumns, + }); + + expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); + }); + + test('should add a new column to an existing collection of columns at the end of the collection', () => { + const expectedColumns = [...columns, columnToAdd]; + const mockWithExistingColumns = set('foo.columns', columns, timelineById); + + const update = upsertTimelineColumn({ + column: columnToAdd, + id: 'foo', + index: expectedColumns.length - 1, + timelineById: mockWithExistingColumns, + }); + + expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); + }); + + columns.forEach((column, i) => { + test(`should upsert (NOT add a new column) a column when already exists at the same index (${i})`, () => { + const mockWithExistingColumns = set('foo.columns', columns, timelineById); + + const update = upsertTimelineColumn({ + column, + id: 'foo', + index: i, + timelineById: mockWithExistingColumns, + }); + + expect(update).toEqual(set('foo.columns', columns, mockWithExistingColumns)); + }); + }); + + test('should allow the 1st column to be moved to the 2nd column', () => { + const expectedColumns = [columns[1], columns[0], columns[2]]; + const mockWithExistingColumns = set('foo.columns', columns, timelineById); + + const update = upsertTimelineColumn({ + column: columns[0], + id: 'foo', + index: 1, + timelineById: mockWithExistingColumns, + }); + + expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); + }); + + test('should allow the 1st column to be moved to the 3rd column', () => { + const expectedColumns = [columns[1], columns[2], columns[0]]; + const mockWithExistingColumns = set('foo.columns', columns, timelineById); + + const update = upsertTimelineColumn({ + column: columns[0], + id: 'foo', + index: 2, + timelineById: mockWithExistingColumns, + }); + + expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); + }); + + test('should allow the 2nd column to be moved to the 1st column', () => { + const expectedColumns = [columns[1], columns[0], columns[2]]; + const mockWithExistingColumns = set('foo.columns', columns, timelineById); + + const update = upsertTimelineColumn({ + column: columns[1], + id: 'foo', + index: 0, + timelineById: mockWithExistingColumns, + }); + + expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); + }); + + test('should allow the 2nd column to be moved to the 3rd column', () => { + const expectedColumns = [columns[0], columns[2], columns[1]]; + const mockWithExistingColumns = set('foo.columns', columns, timelineById); + + const update = upsertTimelineColumn({ + column: columns[1], + id: 'foo', + index: 2, + timelineById: mockWithExistingColumns, + }); + + expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); + }); + + test('should allow the 3rd column to be moved to the 1st column', () => { + const expectedColumns = [columns[2], columns[0], columns[1]]; + const mockWithExistingColumns = set('foo.columns', columns, timelineById); + + const update = upsertTimelineColumn({ + column: columns[2], + id: 'foo', + index: 0, + timelineById: mockWithExistingColumns, + }); + + expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); + }); + + test('should allow the 3rd column to be moved to the 2nd column', () => { + const expectedColumns = [columns[0], columns[2], columns[1]]; + const mockWithExistingColumns = set('foo.columns', columns, timelineById); + + const update = upsertTimelineColumn({ + column: columns[2], + id: 'foo', + index: 1, + timelineById: mockWithExistingColumns, + }); + + expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); + }); + }); + + describe('#addTimelineProvider', () => { + test('should return a new reference and not the same reference', () => { + const update = addTimelineProvider({ + id: 'foo', + provider: { + and: [], + id: '567', + name: 'data provider 2', + enabled: true, + queryMatch: { + field: '', + value: '', + operator: IS_OPERATOR, + }, + + excluded: false, + kqlQuery: '', + }, + timelineById: timelineByIdMock, + }); + expect(update).not.toBe(timelineByIdMock); + }); + + test('should add a new timeline provider', () => { + const providerToAdd: DataProvider = { + and: [], + id: '567', + name: 'data provider 2', + enabled: true, + queryMatch: { + field: '', + value: '', + operator: IS_OPERATOR, + }, + + excluded: false, + kqlQuery: '', + }; + const update = addTimelineProvider({ + id: 'foo', + provider: providerToAdd, + timelineById: timelineByIdMock, + }); + const addedDataProvider = timelineByIdMock.foo.dataProviders.concat(providerToAdd); + expect(update).toEqual(set('foo.dataProviders', addedDataProvider, timelineByIdMock)); + }); + + test('should NOT add a new timeline provider if it already exists and the attributes "and" is empty', () => { + const providerToAdd: DataProvider = { + and: [], + id: '123', + name: 'data provider 1', + enabled: true, + queryMatch: { + field: '', + value: '', + operator: IS_OPERATOR, + }, + + excluded: false, + kqlQuery: '', + }; + const update = addTimelineProvider({ + id: 'foo', + provider: providerToAdd, + timelineById: timelineByIdMock, + }); + expect(update).toEqual(timelineByIdMock); + }); + + test('should add a new timeline provider if it already exists and the attributes "and" is NOT empty', () => { + const myMockTimelineByIdMock = cloneDeep(timelineByIdMock); + myMockTimelineByIdMock.foo.dataProviders[0].and = [ + { + id: '456', + name: 'and data provider 1', + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: '', + value: '', + operator: IS_OPERATOR, + }, + }, + ]; + const providerToAdd: DataProvider = { + and: [], + id: '123', + name: 'data provider 1', + enabled: true, + queryMatch: { + field: '', + value: '', + operator: IS_OPERATOR, + }, + + excluded: false, + kqlQuery: '', + }; + const update = addTimelineProvider({ + id: 'foo', + provider: providerToAdd, + timelineById: myMockTimelineByIdMock, + }); + expect(update).toEqual(set('foo.dataProviders[1]', providerToAdd, myMockTimelineByIdMock)); + }); + + test('should UPSERT an existing timeline provider if it already exists', () => { + const providerToAdd: DataProvider = { + and: [], + id: '123', + name: 'my name changed', + enabled: true, + queryMatch: { + field: '', + value: '', + operator: IS_OPERATOR, + }, + excluded: false, + kqlQuery: '', + }; + const update = addTimelineProvider({ + id: 'foo', + provider: providerToAdd, + timelineById: timelineByIdMock, + }); + expect(update).toEqual(set('foo.dataProviders[0].name', 'my name changed', timelineByIdMock)); + }); + }); + + describe('#removeTimelineColumn', () => { + test('should return a new reference and not the same reference', () => { + // pre-populate a new mock with existing columns: + const mockWithExistingColumns = set('foo.columns', columnsMock, timelineByIdMock); + + const update = removeTimelineColumn({ + id: 'foo', + columnId: columnsMock[0].id, + timelineById: mockWithExistingColumns, + }); + + expect(update).not.toBe(timelineByIdMock); + }); + + test('should remove just the first column when the id matches', () => { + const expectedColumns = [columnsMock[1], columnsMock[2]]; + + // pre-populate a new mock with existing columns: + const mockWithExistingColumns = set('foo.columns', columnsMock, timelineByIdMock); + + const update = removeTimelineColumn({ + id: 'foo', + columnId: columnsMock[0].id, + timelineById: mockWithExistingColumns, + }); + + expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); + }); + + test('should remove just the last column when the id matches', () => { + const expectedColumns = [columnsMock[0], columnsMock[1]]; + + // pre-populate a new mock with existing columns: + const mockWithExistingColumns = set('foo.columns', columnsMock, timelineByIdMock); + + const update = removeTimelineColumn({ + id: 'foo', + columnId: columnsMock[2].id, + timelineById: mockWithExistingColumns, + }); + + expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); + }); + + test('should remove just the middle column when the id matches', () => { + const expectedColumns = [columnsMock[0], columnsMock[2]]; + + // pre-populate a new mock with existing columns: + const mockWithExistingColumns = set('foo.columns', columnsMock, timelineByIdMock); + + const update = removeTimelineColumn({ + id: 'foo', + columnId: columnsMock[1].id, + timelineById: mockWithExistingColumns, + }); + + expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); + }); + + test('should not modify the columns if the id to remove was not found', () => { + const expectedColumns = cloneDeep(columnsMock); + + // pre-populate a new mock with existing columns: + const mockWithExistingColumns = set('foo.columns', columnsMock, timelineByIdMock); + + const update = removeTimelineColumn({ + id: 'foo', + columnId: 'does.not.exist', + timelineById: mockWithExistingColumns, + }); + + expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); + }); + }); + + describe('#applyDeltaToColumnWidth', () => { + test('should return a new reference and not the same reference', () => { + const delta = 50; + // pre-populate a new mock with existing columns: + const mockWithExistingColumns = set('foo.columns', columnsMock, timelineByIdMock); + + const update = applyDeltaToTimelineColumnWidth({ + id: 'foo', + columnId: columnsMock[0].id, + delta, + timelineById: mockWithExistingColumns, + }); + + expect(update).not.toBe(timelineByIdMock); + }); + + test('should update (just) the specified column of type `date` when the id matches, and the result of applying the delta is greater than the min width for a date column', () => { + const aDateColumn = columnsMock[0]; + const delta = 50; + const expectedToHaveNewWidth = { + ...aDateColumn, + width: getColumnWidthFromType(aDateColumn.type!) + delta, + }; + const expectedColumns = [expectedToHaveNewWidth, columnsMock[1], columnsMock[2]]; + + // pre-populate a new mock with existing columns: + const mockWithExistingColumns = set('foo.columns', columnsMock, timelineByIdMock); + + const update = applyDeltaToTimelineColumnWidth({ + id: 'foo', + columnId: aDateColumn.id, + delta, + timelineById: mockWithExistingColumns, + }); + + expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); + }); + + test('should NOT update (just) the specified column of type `date` when the id matches, because the result of applying the delta is less than the min width for a date column', () => { + const aDateColumn = columnsMock[0]; + const delta = -50; // this will be less than the min + const expectedToHaveNewWidth = { + ...aDateColumn, + width: getColumnWidthFromType(aDateColumn.type!), // we expect the minimum + }; + const expectedColumns = [expectedToHaveNewWidth, columnsMock[1], columnsMock[2]]; + + // pre-populate a new mock with existing columns: + const mockWithExistingColumns = set('foo.columns', columnsMock, timelineByIdMock); + + const update = applyDeltaToTimelineColumnWidth({ + id: 'foo', + columnId: aDateColumn.id, + delta, + timelineById: mockWithExistingColumns, + }); + + expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); + }); + + test('should update (just) the specified non-date column when the id matches, and the result of applying the delta is greater than the min width for the column', () => { + const aNonDateColumn = columnsMock[1]; + const delta = 50; + const expectedToHaveNewWidth = { + ...aNonDateColumn, + width: getColumnWidthFromType(aNonDateColumn.type!) + delta, + }; + const expectedColumns = [columnsMock[0], expectedToHaveNewWidth, columnsMock[2]]; + + // pre-populate a new mock with existing columns: + const mockWithExistingColumns = set('foo.columns', columnsMock, timelineByIdMock); + + const update = applyDeltaToTimelineColumnWidth({ + id: 'foo', + columnId: aNonDateColumn.id, + delta, + timelineById: mockWithExistingColumns, + }); + + expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); + }); + + test('should NOT update the specified non-date column when the id matches, because the result of applying the delta is less than the min width for the column', () => { + const aNonDateColumn = columnsMock[1]; + const delta = -50; + const expectedToHaveNewWidth = { + ...aNonDateColumn, + width: getColumnWidthFromType(aNonDateColumn.type!), + }; + const expectedColumns = [columnsMock[0], expectedToHaveNewWidth, columnsMock[2]]; + + // pre-populate a new mock with existing columns: + const mockWithExistingColumns = set('foo.columns', columnsMock, timelineByIdMock); + + const update = applyDeltaToTimelineColumnWidth({ + id: 'foo', + columnId: aNonDateColumn.id, + delta, + timelineById: mockWithExistingColumns, + }); + + expect(update).toEqual(set('foo.columns', expectedColumns, mockWithExistingColumns)); + }); + }); + + describe('#addAndProviderToTimelineProvider', () => { + test('should add a new and provider to an existing timeline provider', () => { + const providerToAdd: DataProvider = { + and: [], + id: '567', + name: 'data provider 2', + enabled: true, + queryMatch: { + field: 'handsome', + value: 'garrett', + operator: IS_OPERATOR, + }, + excluded: false, + kqlQuery: '', + }; + + const newTimeline = addTimelineProvider({ + id: 'foo', + provider: providerToAdd, + timelineById: timelineByIdMock, + }); + + newTimeline.foo.highlightedDropAndProviderId = '567'; + + const andProviderToAdd: DataProvider = { + and: [], + id: '568', + name: 'And Data Provider', + enabled: true, + queryMatch: { + field: 'smart', + value: 'frank', + operator: IS_OPERATOR, + }, + + excluded: false, + kqlQuery: '', + }; + + const update = addTimelineProvider({ + id: 'foo', + provider: andProviderToAdd, + timelineById: newTimeline, + }); + const indexProvider = update.foo.dataProviders.findIndex(i => i.id === '567'); + const addedAndDataProvider = update.foo.dataProviders[indexProvider].and[0]; + const { and, ...expectedResult } = andProviderToAdd; + expect(addedAndDataProvider).toEqual(expectedResult); + newTimeline.foo.highlightedDropAndProviderId = ''; + }); + + test('should add another and provider because it is not a duplicate', () => { + const providerToAdd: DataProvider = { + and: [ + { + id: '568', + name: 'And Data Provider', + enabled: true, + queryMatch: { + field: 'smart', + value: 'garrett', + operator: IS_OPERATOR, + }, + excluded: false, + kqlQuery: '', + }, + ], + id: '567', + name: 'data provider 1', + enabled: true, + queryMatch: { + field: 'handsome', + value: 'frank', + operator: IS_OPERATOR, + }, + excluded: false, + kqlQuery: '', + }; + + const newTimeline = addTimelineProvider({ + id: 'foo', + provider: providerToAdd, + timelineById: timelineByIdMock, + }); + + newTimeline.foo.highlightedDropAndProviderId = '567'; + + const andProviderToAdd: DataProvider = { + and: [], + id: '569', + name: 'And Data Provider', + enabled: true, + queryMatch: { + field: 'happy', + value: 'andrewG', + operator: IS_OPERATOR, + }, + excluded: false, + kqlQuery: '', + }; + // temporary, we will have to decouple DataProvider & DataProvidersAnd + // that's bigger a refactor than just fixing a bug + delete andProviderToAdd.and; + const update = addTimelineProvider({ + id: 'foo', + provider: andProviderToAdd, + timelineById: newTimeline, + }); + + expect(update).toEqual(set('foo.dataProviders[1].and[1]', andProviderToAdd, newTimeline)); + newTimeline.foo.highlightedDropAndProviderId = ''; + }); + + test('should NOT add another and provider because it is a duplicate', () => { + const providerToAdd: DataProvider = { + and: [ + { + id: '568', + name: 'And Data Provider', + enabled: true, + queryMatch: { + field: 'smart', + value: 'garrett', + operator: IS_OPERATOR, + }, + excluded: false, + kqlQuery: '', + }, + ], + id: '567', + name: 'data provider 1', + enabled: true, + queryMatch: { + field: 'handsome', + value: 'frank', + operator: IS_OPERATOR, + }, + excluded: false, + kqlQuery: '', + }; + + const newTimeline = addTimelineProvider({ + id: 'foo', + provider: providerToAdd, + timelineById: timelineByIdMock, + }); + + newTimeline.foo.highlightedDropAndProviderId = '567'; + + const andProviderToAdd: DataProvider = { + and: [], + id: '569', + name: 'And Data Provider', + enabled: true, + queryMatch: { + field: 'smart', + value: 'garrett', + operator: IS_OPERATOR, + }, + excluded: false, + kqlQuery: '', + }; + const update = addTimelineProvider({ + id: 'foo', + provider: andProviderToAdd, + timelineById: newTimeline, + }); + + expect(update).toEqual(newTimeline); + newTimeline.foo.highlightedDropAndProviderId = ''; + }); + }); + + describe('#updateTimelineColumns', () => { + test('should return a new reference and not the same reference', () => { + const update = updateTimelineColumns({ + id: 'foo', + columns: columnsMock, + timelineById: timelineByIdMock, + }); + expect(update).not.toBe(timelineByIdMock); + }); + + test('should update a timeline with new columns', () => { + const update = updateTimelineColumns({ + id: 'foo', + columns: columnsMock, + timelineById: timelineByIdMock, + }); + expect(update).toEqual(set('foo.columns', [...columnsMock], timelineByIdMock)); + }); + }); + + describe('#updateTimelineDescription', () => { + const newDescription = 'a new description'; + + test('should return a new reference and not the same reference', () => { + const update = updateTimelineDescription({ + id: 'foo', + description: newDescription, + timelineById: timelineByIdMock, + }); + expect(update).not.toBe(timelineByIdMock); + }); + + test('should update the timeline description', () => { + const update = updateTimelineDescription({ + id: 'foo', + description: newDescription, + timelineById: timelineByIdMock, + }); + expect(update).toEqual(set('foo.description', newDescription, timelineByIdMock)); + }); + + test('should always trim all leading whitespace and allow only one trailing space', () => { + const update = updateTimelineDescription({ + id: 'foo', + description: ' breathing room ', + timelineById: timelineByIdMock, + }); + expect(update).toEqual(set('foo.description', 'breathing room ', timelineByIdMock)); + }); + }); + + describe('#updateTimelineTitle', () => { + const newTitle = 'a new title'; + + test('should return a new reference and not the same reference', () => { + const update = updateTimelineTitle({ + id: 'foo', + title: newTitle, + timelineById: timelineByIdMock, + }); + expect(update).not.toBe(timelineByIdMock); + }); + + test('should update the timeline title', () => { + const update = updateTimelineTitle({ + id: 'foo', + title: newTitle, + timelineById: timelineByIdMock, + }); + expect(update).toEqual(set('foo.title', newTitle, timelineByIdMock)); + }); + + test('should always trim all leading whitespace and allow only one trailing space', () => { + const update = updateTimelineTitle({ + id: 'foo', + title: ' room at the back ', + timelineById: timelineByIdMock, + }); + expect(update).toEqual(set('foo.title', 'room at the back ', timelineByIdMock)); + }); + }); + + describe('#updateTimelineProviders', () => { + test('should return a new reference and not the same reference', () => { + const update = updateTimelineProviders({ + id: 'foo', + providers: [ + { + and: [], + id: '567', + name: 'data provider 2', + enabled: true, + queryMatch: { + field: '', + value: '', + operator: IS_OPERATOR, + }, + + excluded: false, + kqlQuery: '', + }, + ], + timelineById: timelineByIdMock, + }); + expect(update).not.toBe(timelineByIdMock); + }); + + test('should add update a timeline with new providers', () => { + const providerToAdd: DataProvider = { + and: [], + id: '567', + name: 'data provider 2', + enabled: true, + queryMatch: { + field: '', + value: '', + operator: IS_OPERATOR, + }, + + excluded: false, + kqlQuery: '', + }; + const update = updateTimelineProviders({ + id: 'foo', + providers: [providerToAdd], + timelineById: timelineByIdMock, + }); + expect(update).toEqual(set('foo.dataProviders', [providerToAdd], timelineByIdMock)); + }); + }); + + describe('#updateTimelineRange', () => { + test('should return a new reference and not the same reference', () => { + const update = updateTimelineRange({ + id: 'foo', + start: 23, + end: 33, + timelineById: timelineByIdMock, + }); + expect(update).not.toBe(timelineByIdMock); + }); + + test('should update the timeline range', () => { + const update = updateTimelineRange({ + id: 'foo', + start: 23, + end: 33, + timelineById: timelineByIdMock, + }); + expect(update).toEqual( + set( + 'foo.dateRange', + { + start: 23, + end: 33, + }, + timelineByIdMock + ) + ); + }); + }); + + describe('#updateTimelineSort', () => { + test('should return a new reference and not the same reference', () => { + const update = updateTimelineSort({ + id: 'foo', + sort: { + columnId: 'some column', + sortDirection: Direction.desc, + }, + timelineById: timelineByIdMock, + }); + expect(update).not.toBe(timelineByIdMock); + }); + + test('should update the timeline range', () => { + const update = updateTimelineSort({ + id: 'foo', + sort: { + columnId: 'some column', + sortDirection: Direction.desc, + }, + timelineById: timelineByIdMock, + }); + expect(update).toEqual( + set( + 'foo.sort', + { columnId: 'some column', sortDirection: Direction.desc }, + timelineByIdMock + ) + ); + }); + }); + + describe('#updateTimelineProviderEnabled', () => { + test('should return a new reference and not the same reference', () => { + const update = updateTimelineProviderEnabled({ + id: 'foo', + providerId: '123', + enabled: false, // value we are updating from true to false + timelineById: timelineByIdMock, + }); + expect(update).not.toBe(timelineByIdMock); + }); + + test('should return a new reference for data provider and not the same reference of data provider', () => { + const update = updateTimelineProviderEnabled({ + id: 'foo', + providerId: '123', + enabled: false, // value we are updating from true to false + timelineById: timelineByIdMock, + }); + expect(update.foo.dataProviders).not.toBe(timelineByIdMock.foo.dataProviders); + }); + + test('should update the timeline provider enabled from true to false', () => { + const update = updateTimelineProviderEnabled({ + id: 'foo', + providerId: '123', + enabled: false, // value we are updating from true to false + timelineById: timelineByIdMock, + }); + const expected: TimelineById = { + foo: { + id: 'foo', + savedObjectId: null, + columns: [], + dataProviders: [ + { + and: [], + id: '123', + name: 'data provider 1', + enabled: false, // This value changed from true to false + excluded: false, + kqlQuery: '', + queryMatch: { + field: '', + value: '', + operator: IS_OPERATOR, + }, + }, + ], + deletedEventIds: [], + description: '', + eventIdToNoteIds: {}, + highlightedDropAndProviderId: '', + historyIds: [], + isFavorite: false, + isLive: false, + isSelectAllChecked: false, + isLoading: false, + kqlMode: 'filter', + kqlQuery: { filterQuery: null, filterQueryDraft: null }, + loadingEventIds: [], + title: '', + timelineType: TimelineType.default, + templateTimelineVersion: null, + templateTimelineId: null, + noteIds: [], + dateRange: { + start: 0, + end: 0, + }, + selectedEventIds: {}, + show: true, + showRowRenderers: true, + showCheckboxes: false, + sort: { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + pinnedEventIds: {}, + pinnedEventsSaveObject: {}, + itemsPerPage: 25, + itemsPerPageOptions: [10, 25, 50], + width: DEFAULT_TIMELINE_WIDTH, + isSaving: false, + version: null, + }, + }; + expect(update).toEqual(expected); + }); + + test('should update only one data provider and not two data providers', () => { + const multiDataProvider = timelineByIdMock.foo.dataProviders.concat({ + and: [], + id: '456', + name: 'data provider 1', + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: '', + value: '', + operator: IS_OPERATOR, + }, + }); + const multiDataProviderMock = set('foo.dataProviders', multiDataProvider, timelineByIdMock); + const update = updateTimelineProviderEnabled({ + id: 'foo', + providerId: '123', + enabled: false, // value we are updating from true to false + timelineById: multiDataProviderMock, + }); + const expected: TimelineById = { + foo: { + id: 'foo', + savedObjectId: null, + columns: [], + dataProviders: [ + { + and: [], + id: '123', + name: 'data provider 1', + enabled: false, // value we are updating from true to false + excluded: false, + kqlQuery: '', + queryMatch: { + field: '', + value: '', + operator: IS_OPERATOR, + }, + }, + { + and: [], + id: '456', + name: 'data provider 1', + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: '', + value: '', + operator: IS_OPERATOR, + }, + }, + ], + description: '', + deletedEventIds: [], + eventIdToNoteIds: {}, + highlightedDropAndProviderId: '', + historyIds: [], + isFavorite: false, + isLive: false, + isSelectAllChecked: false, + isLoading: false, + kqlMode: 'filter', + kqlQuery: { filterQuery: null, filterQueryDraft: null }, + loadingEventIds: [], + title: '', + timelineType: TimelineType.default, + templateTimelineVersion: null, + templateTimelineId: null, + noteIds: [], + dateRange: { + start: 0, + end: 0, + }, + selectedEventIds: {}, + show: true, + showRowRenderers: true, + showCheckboxes: false, + sort: { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + pinnedEventIds: {}, + pinnedEventsSaveObject: {}, + itemsPerPage: 25, + itemsPerPageOptions: [10, 25, 50], + width: DEFAULT_TIMELINE_WIDTH, + isSaving: false, + version: null, + }, + }; + expect(update).toEqual(expected); + }); + }); + + describe('#updateTimelineAndProviderEnabled', () => { + let timelineByIdwithAndMock: TimelineById = timelineByIdMock; + beforeEach(() => { + const providerToAdd: DataProvider = { + and: [ + { + id: '568', + name: 'And Data Provider', + enabled: true, + queryMatch: { + field: '', + value: '', + operator: IS_OPERATOR, + }, + + excluded: false, + kqlQuery: '', + }, + ], + id: '567', + name: 'data provider 1', + enabled: true, + queryMatch: { + field: '', + value: '', + operator: IS_OPERATOR, + }, + + excluded: false, + kqlQuery: '', + }; + + timelineByIdwithAndMock = addTimelineProvider({ + id: 'foo', + provider: providerToAdd, + timelineById: timelineByIdMock, + }); + }); + + test('should return a new reference and not the same reference', () => { + const update = updateTimelineProviderEnabled({ + id: 'foo', + providerId: '567', + enabled: false, // value we are updating from true to false + timelineById: timelineByIdwithAndMock, + andProviderId: '568', + }); + expect(update).not.toBe(timelineByIdwithAndMock); + }); + + test('should return a new reference for and data provider and not the same reference of data and provider', () => { + const update = updateTimelineProviderEnabled({ + id: 'foo', + providerId: '567', + enabled: false, // value we are updating from true to false + timelineById: timelineByIdwithAndMock, + andProviderId: '568', + }); + expect(update.foo.dataProviders).not.toBe(timelineByIdMock.foo.dataProviders); + }); + + test('should update the timeline and provider enabled from true to false', () => { + const update = updateTimelineProviderEnabled({ + id: 'foo', + providerId: '567', + enabled: false, // value we are updating from true to false + timelineById: timelineByIdwithAndMock, + andProviderId: '568', + }); + const indexProvider = update.foo.dataProviders.findIndex(i => i.id === '567'); + expect(update.foo.dataProviders[indexProvider].and[0].enabled).toEqual(false); + }); + + test('should update only one and data provider and not two and data providers', () => { + const indexProvider = timelineByIdwithAndMock.foo.dataProviders.findIndex( + i => i.id === '567' + ); + const multiAndDataProvider = timelineByIdwithAndMock.foo.dataProviders[ + indexProvider + ].and.concat({ + id: '456', + name: 'new and data provider', + enabled: true, + queryMatch: { + field: '', + value: '', + operator: IS_OPERATOR, + }, + + excluded: false, + kqlQuery: '', + }); + const multiAndDataProviderMock = set( + `foo.dataProviders[${indexProvider}].and`, + multiAndDataProvider, + timelineByIdwithAndMock + ); + const update = updateTimelineProviderEnabled({ + id: 'foo', + providerId: '567', + enabled: false, // value we are updating from true to false + timelineById: multiAndDataProviderMock, + andProviderId: '568', + }); + const oldAndProvider = update.foo.dataProviders[indexProvider].and.find(i => i.id === '568'); + const newAndProvider = update.foo.dataProviders[indexProvider].and.find(i => i.id === '456'); + expect(oldAndProvider!.enabled).toEqual(false); + expect(newAndProvider!.enabled).toEqual(true); + }); + }); + + describe('#updateTimelineProviderExcluded', () => { + test('should return a new reference and not the same reference', () => { + const update = updateTimelineProviderExcluded({ + id: 'foo', + providerId: '123', + excluded: true, // value we are updating from false to true + timelineById: timelineByIdMock, + }); + expect(update).not.toBe(timelineByIdMock); + }); + + test('should return a new reference for data provider and not the same reference of data provider', () => { + const update = updateTimelineProviderExcluded({ + id: 'foo', + providerId: '123', + excluded: true, // value we are updating from false to true + timelineById: timelineByIdMock, + }); + expect(update.foo.dataProviders).not.toBe(timelineByIdMock.foo.dataProviders); + }); + + test('should update the timeline provider excluded from true to false', () => { + const update = updateTimelineProviderExcluded({ + id: 'foo', + providerId: '123', + excluded: true, // value we are updating from false to true + timelineById: timelineByIdMock, + }); + const expected: TimelineById = { + foo: { + id: 'foo', + savedObjectId: null, + columns: [], + dataProviders: [ + { + and: [], + id: '123', + name: 'data provider 1', + enabled: true, + excluded: true, // This value changed from true to false + kqlQuery: '', + queryMatch: { + field: '', + value: '', + operator: IS_OPERATOR, + }, + }, + ], + description: '', + deletedEventIds: [], + eventIdToNoteIds: {}, + highlightedDropAndProviderId: '', + historyIds: [], + isFavorite: false, + isLive: false, + isSelectAllChecked: false, + isLoading: false, + kqlMode: 'filter', + kqlQuery: { filterQuery: null, filterQueryDraft: null }, + loadingEventIds: [], + title: '', + timelineType: TimelineType.default, + templateTimelineVersion: null, + templateTimelineId: null, + noteIds: [], + dateRange: { + start: 0, + end: 0, + }, + selectedEventIds: {}, + show: true, + showRowRenderers: true, + showCheckboxes: false, + sort: { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + pinnedEventIds: {}, + pinnedEventsSaveObject: {}, + itemsPerPage: 25, + itemsPerPageOptions: [10, 25, 50], + width: DEFAULT_TIMELINE_WIDTH, + isSaving: false, + version: null, + }, + }; + expect(update).toEqual(expected); + }); + + test('should update only one data provider and not two data providers', () => { + const multiDataProvider = timelineByIdMock.foo.dataProviders.concat({ + and: [], + id: '456', + name: 'data provider 1', + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: '', + value: '', + operator: IS_OPERATOR, + }, + }); + const multiDataProviderMock = set('foo.dataProviders', multiDataProvider, timelineByIdMock); + const update = updateTimelineProviderExcluded({ + id: 'foo', + providerId: '123', + excluded: true, // value we are updating from false to true + timelineById: multiDataProviderMock, + }); + const expected: TimelineById = { + foo: { + id: 'foo', + savedObjectId: null, + columns: [], + dataProviders: [ + { + and: [], + id: '123', + name: 'data provider 1', + enabled: true, + excluded: true, // value we are updating from false to true + kqlQuery: '', + queryMatch: { + field: '', + value: '', + operator: IS_OPERATOR, + }, + }, + { + and: [], + id: '456', + name: 'data provider 1', + enabled: true, + excluded: false, + kqlQuery: '', + queryMatch: { + field: '', + value: '', + operator: IS_OPERATOR, + }, + }, + ], + description: '', + deletedEventIds: [], + eventIdToNoteIds: {}, + highlightedDropAndProviderId: '', + historyIds: [], + isFavorite: false, + isLive: false, + isSelectAllChecked: false, + isLoading: false, + kqlMode: 'filter', + kqlQuery: { filterQuery: null, filterQueryDraft: null }, + loadingEventIds: [], + title: '', + timelineType: TimelineType.default, + templateTimelineId: null, + templateTimelineVersion: null, + noteIds: [], + dateRange: { + start: 0, + end: 0, + }, + selectedEventIds: {}, + show: true, + showRowRenderers: true, + showCheckboxes: false, + sort: { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + pinnedEventIds: {}, + pinnedEventsSaveObject: {}, + itemsPerPage: 25, + itemsPerPageOptions: [10, 25, 50], + width: DEFAULT_TIMELINE_WIDTH, + isSaving: false, + version: null, + }, + }; + expect(update).toEqual(expected); + }); + }); + + describe('#updateTimelineAndProviderExcluded', () => { + let timelineByIdwithAndMock: TimelineById = timelineByIdMock; + beforeEach(() => { + const providerToAdd: DataProvider = { + and: [ + { + id: '568', + name: 'And Data Provider', + enabled: true, + queryMatch: { + field: '', + value: '', + operator: IS_OPERATOR, + }, + + excluded: false, + kqlQuery: '', + }, + ], + id: '567', + name: 'data provider 1', + enabled: true, + queryMatch: { + field: '', + value: '', + operator: IS_OPERATOR, + }, + + excluded: false, + kqlQuery: '', + }; + + timelineByIdwithAndMock = addTimelineProvider({ + id: 'foo', + provider: providerToAdd, + timelineById: timelineByIdMock, + }); + }); + + test('should return a new reference and not the same reference', () => { + const update = updateTimelineProviderExcluded({ + id: 'foo', + providerId: '567', + excluded: true, // value we are updating from true to false + timelineById: timelineByIdwithAndMock, + andProviderId: '568', + }); + expect(update).not.toBe(timelineByIdwithAndMock); + }); + + test('should return a new reference for and data provider and not the same reference of data and provider', () => { + const update = updateTimelineProviderExcluded({ + id: 'foo', + providerId: '567', + excluded: true, // value we are updating from false to true + timelineById: timelineByIdwithAndMock, + andProviderId: '568', + }); + expect(update.foo.dataProviders).not.toBe(timelineByIdMock.foo.dataProviders); + }); + + test('should update the timeline and provider excluded from true to false', () => { + const update = updateTimelineProviderExcluded({ + id: 'foo', + providerId: '567', + excluded: true, // value we are updating from true to false + timelineById: timelineByIdwithAndMock, + andProviderId: '568', + }); + const indexProvider = update.foo.dataProviders.findIndex(i => i.id === '567'); + expect(update.foo.dataProviders[indexProvider].and[0].enabled).toEqual(true); + }); + + test('should update only one and data provider and not two and data providers', () => { + const indexProvider = timelineByIdwithAndMock.foo.dataProviders.findIndex( + i => i.id === '567' + ); + const multiAndDataProvider = timelineByIdwithAndMock.foo.dataProviders[ + indexProvider + ].and.concat({ + id: '456', + name: 'new and data provider', + enabled: true, + queryMatch: { + field: '', + value: '', + operator: IS_OPERATOR, + }, + + excluded: false, + kqlQuery: '', + }); + const multiAndDataProviderMock = set( + `foo.dataProviders[${indexProvider}].and`, + multiAndDataProvider, + timelineByIdwithAndMock + ); + const update = updateTimelineProviderExcluded({ + id: 'foo', + providerId: '567', + excluded: true, // value we are updating from true to false + timelineById: multiAndDataProviderMock, + andProviderId: '568', + }); + const oldAndProvider = update.foo.dataProviders[indexProvider].and.find(i => i.id === '568'); + const newAndProvider = update.foo.dataProviders[indexProvider].and.find(i => i.id === '456'); + expect(oldAndProvider!.excluded).toEqual(true); + expect(newAndProvider!.excluded).toEqual(false); + }); + }); + + describe('#updateTimelineItemsPerPage', () => { + test('should return a new reference and not the same reference', () => { + const update = updateTimelineItemsPerPage({ + id: 'foo', + itemsPerPage: 10, // value we are updating from 5 to 10 + timelineById: timelineByIdMock, + }); + expect(update).not.toBe(timelineByIdMock); + }); + + test('should update the items per page from 25 to 50', () => { + const update = updateTimelineItemsPerPage({ + id: 'foo', + itemsPerPage: 50, // value we are updating from 25 to 50 + timelineById: timelineByIdMock, + }); + const expected: TimelineById = { + foo: { + id: 'foo', + savedObjectId: null, + columns: [], + dataProviders: [ + { + and: [], + id: '123', + name: 'data provider 1', + enabled: true, + queryMatch: { + field: '', + value: '', + operator: IS_OPERATOR, + }, + + excluded: false, + kqlQuery: '', + }, + ], + description: '', + deletedEventIds: [], + eventIdToNoteIds: {}, + highlightedDropAndProviderId: '', + historyIds: [], + isFavorite: false, + isLive: false, + isSelectAllChecked: false, + isLoading: false, + kqlMode: 'filter', + kqlQuery: { filterQuery: null, filterQueryDraft: null }, + loadingEventIds: [], + title: '', + timelineType: TimelineType.default, + templateTimelineVersion: null, + templateTimelineId: null, + noteIds: [], + dateRange: { + start: 0, + end: 0, + }, + selectedEventIds: {}, + show: true, + showRowRenderers: true, + showCheckboxes: false, + sort: { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + pinnedEventIds: {}, + pinnedEventsSaveObject: {}, + itemsPerPage: 50, + itemsPerPageOptions: [10, 25, 50], + width: DEFAULT_TIMELINE_WIDTH, + isSaving: false, + version: null, + }, + }; + expect(update).toEqual(expected); + }); + }); + + describe('#updateTimelinePerPageOptions', () => { + test('should return a new reference and not the same reference', () => { + const update = updateTimelinePerPageOptions({ + id: 'foo', + itemsPerPageOptions: [100, 200, 300], // value we are updating from [5, 10, 20] + timelineById: timelineByIdMock, + }); + expect(update).not.toBe(timelineByIdMock); + }); + + test('should update the items per page options from [10, 25, 50] to [100, 200, 300]', () => { + const update = updateTimelinePerPageOptions({ + id: 'foo', + itemsPerPageOptions: [100, 200, 300], // value we are updating from [10, 25, 50] + timelineById: timelineByIdMock, + }); + const expected: TimelineById = { + foo: { + columns: [], + dataProviders: [ + { + and: [], + id: '123', + name: 'data provider 1', + enabled: true, + queryMatch: { + field: '', + value: '', + operator: IS_OPERATOR, + }, + + excluded: false, + kqlQuery: '', + }, + ], + description: '', + deletedEventIds: [], + eventIdToNoteIds: {}, + highlightedDropAndProviderId: '', + historyIds: [], + isFavorite: false, + isLive: false, + isSelectAllChecked: false, + isLoading: false, + id: 'foo', + savedObjectId: null, + kqlMode: 'filter', + kqlQuery: { filterQuery: null, filterQueryDraft: null }, + loadingEventIds: [], + title: '', + timelineType: TimelineType.default, + templateTimelineVersion: null, + templateTimelineId: null, + noteIds: [], + dateRange: { + start: 0, + end: 0, + }, + selectedEventIds: {}, + show: true, + showRowRenderers: true, + showCheckboxes: false, + sort: { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + pinnedEventIds: {}, + pinnedEventsSaveObject: {}, + itemsPerPage: 25, + itemsPerPageOptions: [100, 200, 300], // updated + width: DEFAULT_TIMELINE_WIDTH, + isSaving: false, + version: null, + }, + }; + expect(update).toEqual(expected); + }); + }); + + describe('#removeTimelineProvider', () => { + test('should return a new reference and not the same reference', () => { + const update = removeTimelineProvider({ + id: 'foo', + providerId: '123', + timelineById: timelineByIdMock, + }); + expect(update).not.toBe(timelineByIdMock); + }); + + test('should remove a timeline provider', () => { + const update = removeTimelineProvider({ + id: 'foo', + providerId: '123', + timelineById: timelineByIdMock, + }); + expect(update).toEqual(set('foo.dataProviders', [], timelineByIdMock)); + }); + + test('should remove only one data provider and not two data providers', () => { + const multiDataProvider = timelineByIdMock.foo.dataProviders.concat({ + and: [], + id: '456', + name: 'data provider 2', + enabled: true, + queryMatch: { + field: '', + value: '', + operator: IS_OPERATOR, + }, + + excluded: false, + kqlQuery: '', + }); + const multiDataProviderMock = set('foo.dataProviders', multiDataProvider, timelineByIdMock); + const update = removeTimelineProvider({ + id: 'foo', + providerId: '123', + timelineById: multiDataProviderMock, + }); + const expected: TimelineById = { + foo: { + columns: [], + dataProviders: [ + { + and: [], + id: '456', + name: 'data provider 2', + enabled: true, + queryMatch: { + field: '', + value: '', + operator: IS_OPERATOR, + }, + + excluded: false, + kqlQuery: '', + }, + ], + description: '', + deletedEventIds: [], + eventIdToNoteIds: {}, + highlightedDropAndProviderId: '', + historyIds: [], + id: 'foo', + savedObjectId: null, + isFavorite: false, + isLive: false, + isSelectAllChecked: false, + isLoading: false, + kqlMode: 'filter', + kqlQuery: { filterQuery: null, filterQueryDraft: null }, + loadingEventIds: [], + title: '', + timelineType: TimelineType.default, + templateTimelineVersion: null, + templateTimelineId: null, + noteIds: [], + dateRange: { + start: 0, + end: 0, + }, + selectedEventIds: {}, + show: true, + showRowRenderers: true, + showCheckboxes: false, + sort: { + columnId: '@timestamp', + sortDirection: Direction.desc, + }, + pinnedEventIds: {}, + pinnedEventsSaveObject: {}, + itemsPerPage: 25, + itemsPerPageOptions: [10, 25, 50], + width: DEFAULT_TIMELINE_WIDTH, + isSaving: false, + version: null, + }, + }; + expect(update).toEqual(expected); + }); + + test('should remove only first provider and not nested andProvider', () => { + const dataProviders: DataProvider[] = [ + { + and: [], + id: '111', + name: 'data provider 1', + enabled: true, + queryMatch: { + field: '', + value: '', + operator: IS_OPERATOR, + }, + + excluded: false, + kqlQuery: '', + }, + { + and: [], + id: '222', + name: 'data provider 2', + enabled: true, + queryMatch: { + field: '', + value: '', + operator: IS_OPERATOR, + }, + + excluded: false, + kqlQuery: '', + }, + { + and: [], + id: '333', + name: 'data provider 3', + enabled: true, + queryMatch: { + field: '', + value: '', + operator: IS_OPERATOR, + }, + + excluded: false, + kqlQuery: '', + }, + ]; + + const multiDataProviderMock = set('foo.dataProviders', dataProviders, timelineByIdMock); + + const andDataProvider: DataProvidersAnd = { + id: '211', + name: 'And Data Provider', + enabled: true, + queryMatch: { + field: '', + value: '', + operator: IS_OPERATOR, + }, + + excluded: false, + kqlQuery: '', + }; + + const nestedMultiAndDataProviderMock = set( + 'foo.dataProviders[1].and', + [andDataProvider], + multiDataProviderMock + ); + + const update = removeTimelineProvider({ + id: 'foo', + providerId: '222', + timelineById: nestedMultiAndDataProviderMock, + }); + expect(update).toEqual( + set( + 'foo.dataProviders', + [ + nestedMultiAndDataProviderMock.foo.dataProviders[0], + { ...andDataProvider, and: [] }, + nestedMultiAndDataProviderMock.foo.dataProviders[2], + ], + timelineByIdMock + ) + ); + }); + + test('should remove only the first provider and keep multiple nested andProviders', () => { + const multiDataProvider: DataProvider[] = [ + { + and: [ + { + enabled: true, + id: 'socket_closed-MSoH7GoB9v5HJNSHRYj1-user_name-root', + name: 'root', + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'user.name', + value: 'root', + operator: ':', + }, + }, + { + enabled: true, + id: 'executed-yioH7GoB9v5HJNSHKnp5-auditd_result-success', + name: 'success', + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'auditd.result', + value: 'success', + operator: ':', + }, + }, + ], + enabled: true, + excluded: false, + id: 'hosts-table-hostName-suricata-iowa', + name: 'suricata-iowa', + kqlQuery: '', + queryMatch: { + field: 'host.name', + value: 'suricata-iowa', + operator: ':', + }, + }, + ]; + + const multiDataProviderMock = set('foo.dataProviders', multiDataProvider, timelineByIdMock); + + const update = removeTimelineProvider({ + id: 'foo', + providerId: 'hosts-table-hostName-suricata-iowa', + timelineById: multiDataProviderMock, + }); + + expect(update).toEqual( + set( + 'foo.dataProviders', + [ + { + enabled: true, + id: 'socket_closed-MSoH7GoB9v5HJNSHRYj1-user_name-root', + name: 'root', + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'user.name', + value: 'root', + operator: ':', + }, + and: [ + { + enabled: true, + id: 'executed-yioH7GoB9v5HJNSHKnp5-auditd_result-success', + name: 'success', + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'auditd.result', + value: 'success', + operator: ':', + }, + }, + ], + }, + ], + timelineByIdMock + ) + ); + }); + test('should remove only the first AND provider when the first AND is deleted, and there are multiple andProviders', () => { + const multiDataProvider: DataProvider[] = [ + { + and: [ + { + enabled: true, + id: 'socket_closed-MSoH7GoB9v5HJNSHRYj1-user_name-root', + name: 'root', + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'user.name', + value: 'root', + operator: ':', + }, + }, + { + enabled: true, + id: 'executed-yioH7GoB9v5HJNSHKnp5-auditd_result-success', + name: 'success', + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'auditd.result', + value: 'success', + operator: ':', + }, + }, + ], + enabled: true, + excluded: false, + id: 'hosts-table-hostName-suricata-iowa', + name: 'suricata-iowa', + kqlQuery: '', + queryMatch: { + field: 'host.name', + value: 'suricata-iowa', + operator: ':', + }, + }, + ]; + + const multiDataProviderMock = set('foo.dataProviders', multiDataProvider, timelineByIdMock); + + const update = removeTimelineProvider({ + andProviderId: 'socket_closed-MSoH7GoB9v5HJNSHRYj1-user_name-root', + id: 'foo', + providerId: 'hosts-table-hostName-suricata-iowa', + timelineById: multiDataProviderMock, + }); + + expect(update).toEqual( + set( + 'foo.dataProviders', + [ + { + and: [ + { + enabled: true, + id: 'executed-yioH7GoB9v5HJNSHKnp5-auditd_result-success', + name: 'success', + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'auditd.result', + value: 'success', + operator: ':', + }, + }, + ], + enabled: true, + excluded: false, + id: 'hosts-table-hostName-suricata-iowa', + name: 'suricata-iowa', + kqlQuery: '', + queryMatch: { + field: 'host.name', + value: 'suricata-iowa', + operator: ':', + }, + }, + ], + timelineByIdMock + ) + ); + }); + + test('should remove only the second AND provider when the second AND is deleted, and there are multiple andProviders', () => { + const multiDataProvider: DataProvider[] = [ + { + and: [ + { + enabled: true, + id: 'socket_closed-MSoH7GoB9v5HJNSHRYj1-user_name-root', + name: 'root', + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'user.name', + value: 'root', + operator: ':', + }, + }, + { + enabled: true, + id: 'executed-yioH7GoB9v5HJNSHKnp5-auditd_result-success', + name: 'success', + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'auditd.result', + value: 'success', + operator: ':', + }, + }, + ], + enabled: true, + excluded: false, + id: 'hosts-table-hostName-suricata-iowa', + name: 'suricata-iowa', + kqlQuery: '', + queryMatch: { + field: 'host.name', + value: 'suricata-iowa', + operator: ':', + }, + }, + ]; + + const multiDataProviderMock = set('foo.dataProviders', multiDataProvider, timelineByIdMock); + + const update = removeTimelineProvider({ + andProviderId: 'executed-yioH7GoB9v5HJNSHKnp5-auditd_result-success', + id: 'foo', + providerId: 'hosts-table-hostName-suricata-iowa', + timelineById: multiDataProviderMock, + }); + + expect(update).toEqual( + set( + 'foo.dataProviders', + [ + { + and: [ + { + enabled: true, + id: 'socket_closed-MSoH7GoB9v5HJNSHRYj1-user_name-root', + name: 'root', + excluded: false, + kqlQuery: '', + queryMatch: { + field: 'user.name', + value: 'root', + operator: ':', + }, + }, + ], + enabled: true, + excluded: false, + id: 'hosts-table-hostName-suricata-iowa', + name: 'suricata-iowa', + kqlQuery: '', + queryMatch: { + field: 'host.name', + value: 'suricata-iowa', + operator: ':', + }, + }, + ], + timelineByIdMock + ) + ); + }); + }); +}); diff --git a/x-pack/plugins/siem/public/store/timeline/reducer.ts b/x-pack/plugins/siem/public/timelines/store/timeline/reducer.ts similarity index 100% rename from x-pack/plugins/siem/public/store/timeline/reducer.ts rename to x-pack/plugins/siem/public/timelines/store/timeline/reducer.ts diff --git a/x-pack/plugins/siem/public/timelines/store/timeline/refetch_queries.ts b/x-pack/plugins/siem/public/timelines/store/timeline/refetch_queries.ts new file mode 100644 index 00000000000000..f5a30ed831bd68 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/store/timeline/refetch_queries.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { allTimelinesQuery } from '../../../timelines/containers/all/index.gql_query'; +import { Direction } from '../../../graphql/types'; +import { DEFAULT_SORT_FIELD } from '../../../timelines/components/open_timeline/constants'; + +export const refetchQueries = [ + { + query: allTimelinesQuery, + variables: { + search: '', + pageInfo: { + pageIndex: 1, + pageSize: 10, + }, + sort: { sortField: DEFAULT_SORT_FIELD, sortOrder: Direction.desc }, + onlyUserFavorite: false, + }, + }, +]; diff --git a/x-pack/plugins/siem/public/timelines/store/timeline/selectors.ts b/x-pack/plugins/siem/public/timelines/store/timeline/selectors.ts new file mode 100644 index 00000000000000..03e9d722ac93e6 --- /dev/null +++ b/x-pack/plugins/siem/public/timelines/store/timeline/selectors.ts @@ -0,0 +1,76 @@ +/* + * 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 { createSelector } from 'reselect'; + +import { isFromKueryExpressionValid } from '../../../common/lib/keury'; +import { State } from '../../../common/store/reducer'; + +import { TimelineModel } from './model'; +import { AutoSavedWarningMsg, TimelineById } from './types'; + +const selectTimelineById = (state: State): TimelineById => state.timeline.timelineById; + +const selectAutoSaveMsg = (state: State): AutoSavedWarningMsg => state.timeline.autoSavedWarningMsg; + +const selectCallOutUnauthorizedMsg = (state: State): boolean => + state.timeline.showCallOutUnauthorizedMsg; + +export const selectTimeline = (state: State, timelineId: string): TimelineModel => + state.timeline.timelineById[timelineId]; + +export const autoSaveMsgSelector = createSelector(selectAutoSaveMsg, autoSaveMsg => autoSaveMsg); + +export const timelineByIdSelector = createSelector( + selectTimelineById, + timelineById => timelineById +); + +export const getShowCallOutUnauthorizedMsg = () => + createSelector( + selectCallOutUnauthorizedMsg, + showCallOutUnauthorizedMsg => showCallOutUnauthorizedMsg + ); + +export const getTimelines = () => timelineByIdSelector; + +export const getTimelineByIdSelector = () => createSelector(selectTimeline, timeline => timeline); + +export const getEventsByIdSelector = () => createSelector(selectTimeline, timeline => timeline); + +export const getKqlFilterQuerySelector = () => + createSelector(selectTimeline, timeline => + timeline && + timeline.kqlQuery && + timeline.kqlQuery.filterQuery && + timeline.kqlQuery.filterQuery.kuery + ? timeline.kqlQuery.filterQuery.kuery.expression + : null + ); + +export const getKqlFilterQueryDraftSelector = () => + createSelector(selectTimeline, timeline => + timeline && timeline.kqlQuery ? timeline.kqlQuery.filterQueryDraft : null + ); + +export const getKqlFilterKuerySelector = () => + createSelector(selectTimeline, timeline => + timeline && + timeline.kqlQuery && + timeline.kqlQuery.filterQuery && + timeline.kqlQuery.filterQuery.kuery + ? timeline.kqlQuery.filterQuery.kuery + : null + ); + +export const isFilterQueryDraftValidSelector = () => + createSelector( + selectTimeline, + timeline => + timeline && + timeline.kqlQuery && + isFromKueryExpressionValid(timeline.kqlQuery.filterQueryDraft) + ); diff --git a/x-pack/plugins/siem/public/store/timeline/types.ts b/x-pack/plugins/siem/public/timelines/store/timeline/types.ts similarity index 100% rename from x-pack/plugins/siem/public/store/timeline/types.ts rename to x-pack/plugins/siem/public/timelines/store/timeline/types.ts diff --git a/x-pack/plugins/siem/public/utils/route/index.test.tsx b/x-pack/plugins/siem/public/utils/route/index.test.tsx deleted file mode 100644 index e777d281ed51ab..00000000000000 --- a/x-pack/plugins/siem/public/utils/route/index.test.tsx +++ /dev/null @@ -1,205 +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 { mount } from 'enzyme'; -import React from 'react'; - -import { HostsTableType } from '../../store/hosts/model'; -import { RouteSpyState } from './types'; -import { ManageRoutesSpy } from './manage_spy_routes'; -import { SpyRouteComponent } from './spy_routes'; -import { useRouteSpy } from './use_route_spy'; - -type Action = 'PUSH' | 'POP' | 'REPLACE'; -const pop: Action = 'POP'; - -const defaultLocation = { - hash: '', - pathname: '/hosts', - search: '', - state: '', -}; - -export const mockHistory = { - action: pop, - block: jest.fn(), - createHref: jest.fn(), - go: jest.fn(), - goBack: jest.fn(), - goForward: jest.fn(), - length: 2, - listen: jest.fn(), - location: defaultLocation, - push: jest.fn(), - replace: jest.fn(), -}; - -const dispatchMock = jest.fn(); -const mockRoutes: RouteSpyState = { - pageName: '', - detailName: undefined, - tabName: undefined, - search: '', - pathName: '/', - history: mockHistory, -}; - -const mockUseRouteSpy: jest.Mock = useRouteSpy as jest.Mock; -jest.mock('./use_route_spy', () => ({ - useRouteSpy: jest.fn(), -})); - -describe('Spy Routes', () => { - describe('At Initialization of the app', () => { - beforeEach(() => { - dispatchMock.mockReset(); - dispatchMock.mockClear(); - }); - test('Make sure we update search state first', () => { - const pathname = '/'; - mockUseRouteSpy.mockImplementation(() => [mockRoutes, dispatchMock]); - mount( - - - - ); - - expect(dispatchMock.mock.calls[0]).toEqual([ - { - type: 'updateSearch', - search: '?importantQueryString="really"', - }, - ]); - }); - - test('Make sure we update search state first and then update the route but keeping the initial search', () => { - const pathname = '/hosts/allHosts'; - mockUseRouteSpy.mockImplementation(() => [mockRoutes, dispatchMock]); - mount( - - - - ); - - expect(dispatchMock.mock.calls[0]).toEqual([ - { - type: 'updateSearch', - search: '?importantQueryString="really"', - }, - ]); - - expect(dispatchMock.mock.calls[1]).toEqual([ - { - route: { - detailName: undefined, - history: mockHistory, - pageName: 'hosts', - pathName: pathname, - tabName: HostsTableType.hosts, - }, - type: 'updateRouteWithOutSearch', - }, - ]); - }); - }); - - describe('When app is running', () => { - beforeEach(() => { - dispatchMock.mockReset(); - dispatchMock.mockClear(); - }); - test('Update route should be updated when there is changed detected', () => { - const pathname = '/hosts/allHosts'; - const newPathname = `hosts/${HostsTableType.authentications}`; - mockUseRouteSpy.mockImplementation(() => [mockRoutes, dispatchMock]); - const wrapper = mount( - - ); - - dispatchMock.mockReset(); - dispatchMock.mockClear(); - - wrapper.setProps({ - location: { - hash: '', - pathname: newPathname, - search: '?updated="true"', - state: '', - }, - match: { - isExact: false, - path: newPathname, - url: newPathname, - params: { - pageName: 'hosts', - detailName: undefined, - tabName: HostsTableType.authentications, - search: '', - }, - }, - }); - wrapper.update(); - expect(dispatchMock.mock.calls[0]).toEqual([ - { - route: { - detailName: undefined, - history: mockHistory, - pageName: 'hosts', - pathName: newPathname, - tabName: HostsTableType.authentications, - search: '?updated="true"', - }, - type: 'updateRoute', - }, - ]); - }); - }); -}); diff --git a/x-pack/plugins/siem/public/utils/route/types.ts b/x-pack/plugins/siem/public/utils/route/types.ts deleted file mode 100644 index 17b312a427c435..00000000000000 --- a/x-pack/plugins/siem/public/utils/route/types.ts +++ /dev/null @@ -1,70 +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 H from 'history'; -import React from 'react'; -import { RouteComponentProps } from 'react-router-dom'; - -import { TimelineType } from '../../../common/types/timeline'; - -import { HostsTableType } from '../../store/hosts/model'; -import { NetworkRouteType } from '../../pages/network/navigation/types'; -import { FlowTarget } from '../../graphql/types'; - -export type SiemRouteType = HostsTableType | NetworkRouteType | TimelineType; -export interface RouteSpyState { - pageName: string; - detailName: string | undefined; - tabName: SiemRouteType | undefined; - search: string; - pathName: string; - history?: H.History; - flowTarget?: FlowTarget; - state?: Record; -} - -export interface HostRouteSpyState extends RouteSpyState { - tabName: HostsTableType | undefined; -} - -export interface NetworkRouteSpyState extends RouteSpyState { - tabName: NetworkRouteType | undefined; -} - -export interface TimelineRouteSpyState extends RouteSpyState { - tabName: TimelineType | undefined; -} - -export type RouteSpyAction = - | { - type: 'updateSearch'; - search: string; - } - | { - type: 'updateRouteWithOutSearch'; - route: Pick< - RouteSpyState, - 'pageName' & 'detailName' & 'tabName' & 'pathName' & 'history' & 'state' - >; - } - | { - type: 'updateRoute'; - route: RouteSpyState; - }; - -export interface ManageRoutesSpyProps { - children: React.ReactNode; -} - -export type SpyRouteProps = RouteComponentProps<{ - pageName: string | undefined; - detailName: string | undefined; - tabName: HostsTableType | undefined; - search: string; - flowTarget: FlowTarget | undefined; -}> & { - state?: Record; -}; diff --git a/x-pack/plugins/siem/public/utils/saved_query_services/index.tsx b/x-pack/plugins/siem/public/utils/saved_query_services/index.tsx deleted file mode 100644 index 335398177f0f43..00000000000000 --- a/x-pack/plugins/siem/public/utils/saved_query_services/index.tsx +++ /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 { useState, useEffect } from 'react'; -import { - SavedQueryService, - createSavedQueryService, -} from '../../../../../../src/plugins/data/public'; - -import { useKibana } from '../../lib/kibana'; - -export const useSavedQueryServices = () => { - const kibana = useKibana(); - const client = kibana.services.savedObjects.client; - - const [savedQueryService, setSavedQueryService] = useState( - createSavedQueryService(client) - ); - - useEffect(() => { - setSavedQueryService(createSavedQueryService(client)); - }, [client]); - return savedQueryService; -}; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts index e6facf6f3b7a8b..473d183c8a8f26 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.test.ts @@ -5,6 +5,8 @@ */ import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { mlServicesMock, mlAuthzMock as mockMlAuthzFactory } from '../../../machine_learning/mocks'; +import { buildMlAuthz } from '../../../machine_learning/authz'; import { typicalPayload, getReadBulkRequest, @@ -19,9 +21,12 @@ import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { createRulesBulkRoute } from './create_rules_bulk_route'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; +jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); + describe('create_rules_bulk', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); + let ml: ReturnType; beforeAll(() => { setFeatureFlagsForTestsOnly(); @@ -34,12 +39,13 @@ describe('create_rules_bulk', () => { beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); + ml = mlServicesMock.create(); clients.clusterClient.callAsCurrentUser.mockResolvedValue(getNonEmptyIndex()); // index exists clients.alertsClient.find.mockResolvedValue(getEmptyFindResult()); // no existing rules clients.alertsClient.create.mockResolvedValue(getResult()); // successful creation - createRulesBulkRoute(server.router); + createRulesBulkRoute(server.router, ml); }); describe('status codes', () => { @@ -64,16 +70,20 @@ describe('create_rules_bulk', () => { }); describe('unhappy paths', () => { - it('returns an error object if creating an ML rule with an insufficient license', async () => { - (context.licensing.license.hasAtLeast as jest.Mock).mockReturnValue(false); + it('returns a 403 error object if ML Authz fails', async () => { + (buildMlAuthz as jest.Mock).mockReturnValueOnce({ + validateRuleType: jest + .fn() + .mockResolvedValue({ valid: false, message: 'mocked validation message' }), + }); const response = await server.inject(createBulkMlRuleRequest(), context); expect(response.status).toEqual(200); expect(response.body).toEqual([ { error: { - message: 'Your license does not support machine learning. Please upgrade your license.', - status_code: 400, + message: 'mocked validation message', + status_code: 403, }, rule_id: 'rule-1', }, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts index cf841a9c88b32e..371faccfbe47c9 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_bulk_route.ts @@ -8,6 +8,9 @@ import uuid from 'uuid'; import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { SetupPlugins } from '../../../../plugin'; +import { buildMlAuthz } from '../../../machine_learning/authz'; +import { throwHttpError } from '../../../machine_learning/validation'; import { createRules } from '../../rules/create_rules'; import { RuleAlertParamsRest } from '../../types'; import { readRules } from '../../rules/read_rules'; @@ -19,13 +22,12 @@ import { createBulkErrorObject, buildRouteValidation, buildSiemResponse, - validateLicenseForRuleType, } from '../utils'; import { createRulesBulkSchema } from '../schemas/create_rules_bulk_schema'; import { rulesBulkSchema } from '../schemas/response/rules_bulk_schema'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; -export const createRulesBulkRoute = (router: IRouter) => { +export const createRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => { router.post( { path: `${DETECTION_ENGINE_RULES_URL}/_bulk_create`, @@ -47,6 +49,8 @@ export const createRulesBulkRoute = (router: IRouter) => { return siemResponse.error({ statusCode: 404 }); } + const mlAuthz = buildMlAuthz({ license: context.licensing.license, ml, request }); + const ruleDefinitions = request.body; const dupes = getDuplicates(ruleDefinitions, 'rule_id'); @@ -89,7 +93,7 @@ export const createRulesBulkRoute = (router: IRouter) => { } = payloadRule; const ruleIdOrUuid = ruleId ?? uuid.v4(); try { - validateLicenseForRuleType({ license: context.licensing.license, ruleType: type }); + throwHttpError(await mlAuthz.validateRuleType(type)); const finalIndex = outputIndex ?? siemClient.getSignalsIndex(); const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, finalIndex); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts index f15f47432f8389..afdcda7da251de 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.test.ts @@ -16,15 +16,19 @@ import { getFindResultWithSingleHit, createMlRuleRequest, } from '../__mocks__/request_responses'; +import { mlServicesMock, mlAuthzMock as mockMlAuthzFactory } from '../../../machine_learning/mocks'; +import { buildMlAuthz } from '../../../machine_learning/authz'; import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { createRulesRoute } from './create_rules_route'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; jest.mock('../../rules/update_rules_notifications'); +jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); describe('create_rules', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); + let ml: ReturnType; beforeAll(() => { setFeatureFlagsForTestsOnly(); @@ -37,13 +41,14 @@ describe('create_rules', () => { beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); + ml = mlServicesMock.create(); clients.clusterClient.callAsCurrentUser.mockResolvedValue(getNonEmptyIndex()); // index exists clients.alertsClient.find.mockResolvedValue(getEmptyFindResult()); // no current rules clients.alertsClient.create.mockResolvedValue(getResult()); // creation succeeds clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); // needed to transform - createRulesRoute(server.router); + createRulesRoute(server.router, ml); }); describe('status codes with actionClient and alertClient', () => { @@ -86,14 +91,18 @@ describe('create_rules', () => { expect(response.status).toEqual(200); }); - it('rejects the request if licensing is not platinum', async () => { - (context.licensing.license.hasAtLeast as jest.Mock).mockReturnValue(false); + it('returns a 403 if ML Authz fails', async () => { + (buildMlAuthz as jest.Mock).mockReturnValueOnce({ + validateRuleType: jest + .fn() + .mockResolvedValue({ valid: false, message: 'mocked validation message' }), + }); const response = await server.inject(createMlRuleRequest(), context); - expect(response.status).toEqual(400); + expect(response.status).toEqual(403); expect(response.body).toEqual({ - message: 'Your license does not support machine learning. Please upgrade your license.', - status_code: 400, + message: 'mocked validation message', + status_code: 403, }); }); }); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts index 6605b5abfcb09f..7cbb22221679a3 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/create_rules_route.ts @@ -8,22 +8,20 @@ import uuid from 'uuid'; import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { SetupPlugins } from '../../../../plugin'; +import { buildMlAuthz } from '../../../machine_learning/authz'; +import { throwHttpError } from '../../../machine_learning/validation'; import { createRules } from '../../rules/create_rules'; import { readRules } from '../../rules/read_rules'; import { RuleAlertParamsRest } from '../../types'; import { transformValidate } from './validate'; import { getIndexExists } from '../../index/get_index_exists'; import { createRulesSchema } from '../schemas/create_rules_schema'; -import { - buildRouteValidation, - transformError, - buildSiemResponse, - validateLicenseForRuleType, -} from '../utils'; +import { buildRouteValidation, transformError, buildSiemResponse } from '../utils'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; -export const createRulesRoute = (router: IRouter): void => { +export const createRulesRoute = (router: IRouter, ml: SetupPlugins['ml']): void => { router.post( { path: DETECTION_ENGINE_RULES_URL, @@ -70,7 +68,6 @@ export const createRulesRoute = (router: IRouter): void => { const siemResponse = buildSiemResponse(response); try { - validateLicenseForRuleType({ license: context.licensing.license, ruleType: type }); const alertsClient = context.alerting?.getAlertsClient(); const clusterClient = context.core.elasticsearch.dataClient; const savedObjectsClient = context.core.savedObjects.client; @@ -80,6 +77,9 @@ export const createRulesRoute = (router: IRouter): void => { return siemResponse.error({ statusCode: 404 }); } + const mlAuthz = buildMlAuthz({ license: context.licensing.license, ml, request }); + throwHttpError(await mlAuthz.validateRuleType(type)); + const finalIndex = outputIndex ?? siemClient.getSignalsIndex(); const indexExists = await getIndexExists(clusterClient.callAsCurrentUser, finalIndex); if (!indexExists) { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts index 91685a68a60ae9..c33c917c2e9872 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.test.ts @@ -9,8 +9,6 @@ import { ruleIdsToNdJsonString, rulesToNdJsonString, getSimpleRuleWithId, - getSimpleRule, - getSimpleMlRule, } from '../__mocks__/utils'; import { getImportRulesRequest, @@ -22,10 +20,14 @@ import { getNonEmptyIndex, } from '../__mocks__/request_responses'; import { createMockConfig, requestContextMock, serverMock, requestMock } from '../__mocks__'; +import { mlServicesMock, mlAuthzMock as mockMlAuthzFactory } from '../../../machine_learning/mocks'; +import { buildMlAuthz } from '../../../machine_learning/authz'; import { importRulesRoute } from './import_rules_route'; import * as createRulesStreamFromNdJson from '../../rules/create_rules_stream_from_ndjson'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; +jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); + describe('import_rules_route', () => { beforeAll(() => { setFeatureFlagsForTestsOnly(); @@ -39,25 +41,20 @@ describe('import_rules_route', () => { let server: ReturnType; let request: ReturnType; let { clients, context } = requestContextMock.createTools(); + let ml: ReturnType; beforeEach(() => { - // jest carries state between mocked implementations when using - // spyOn. So now we're doing all three of these. - // https://github.com/facebook/jest/issues/7136#issuecomment-565976599 - jest.resetAllMocks(); - jest.restoreAllMocks(); - jest.clearAllMocks(); - server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); config = createMockConfig(); const hapiStream = buildHapiStream(ruleIdsToNdJsonString(['rule-1'])); request = getImportRulesRequest(hapiStream); + ml = mlServicesMock.create(); clients.clusterClient.callAsCurrentUser.mockResolvedValue(getNonEmptyIndex()); // index exists clients.alertsClient.find.mockResolvedValue(getEmptyFindResult()); // no extant rules - importRulesRoute(server.router, config); + importRulesRoute(server.router, config, ml); }); describe('status codes', () => { @@ -83,11 +80,12 @@ describe('import_rules_route', () => { }); describe('unhappy paths', () => { - it('returns an error object if creating an ML rule with an insufficient license', async () => { - (context.licensing.license.hasAtLeast as jest.Mock).mockReturnValue(false); - const rules = [getSimpleRule(), getSimpleMlRule('rule-2')]; - const hapiStreamWithMlRule = buildHapiStream(rulesToNdJsonString(rules)); - request = getImportRulesRequest(hapiStreamWithMlRule); + it('returns a 403 error object if ML Authz fails', async () => { + (buildMlAuthz as jest.Mock).mockReturnValueOnce({ + validateRuleType: jest + .fn() + .mockResolvedValue({ valid: false, message: 'mocked validation message' }), + }); const response = await server.inject(request, context); expect(response.status).toEqual(200); @@ -95,20 +93,19 @@ describe('import_rules_route', () => { errors: [ { error: { - message: - 'Your license does not support machine learning. Please upgrade your license.', - status_code: 400, + message: 'mocked validation message', + status_code: 403, }, - rule_id: 'rule-2', + rule_id: 'rule-1', }, ], success: false, - success_count: 1, + success_count: 0, }); }); test('returns error if createPromiseFromStreams throws error', async () => { - jest + const transformMock = jest .spyOn(createRulesStreamFromNdJson, 'createRulesStreamFromNdJson') .mockImplementation(() => { throw new Error('Test error'); @@ -116,6 +113,8 @@ describe('import_rules_route', () => { const response = await server.inject(request, context); expect(response.status).toEqual(500); expect(response.body).toEqual({ message: 'Test error', status_code: 500 }); + + transformMock.mockRestore(); }); test('returns an error if the index does not exist', async () => { diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts index 9ba083ae48086e..00010027f106ba 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -11,6 +11,9 @@ import { IRouter } from '../../../../../../../../src/core/server'; import { createPromiseFromStreams } from '../../../../../../../../src/legacy/utils/streams'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { ConfigType } from '../../../../config'; +import { SetupPlugins } from '../../../../plugin'; +import { buildMlAuthz } from '../../../machine_learning/authz'; +import { throwHttpError } from '../../../machine_learning/validation'; import { createRules } from '../../rules/create_rules'; import { ImportRulesRequestParams } from '../../rules/types'; import { readRules } from '../../rules/read_rules'; @@ -24,7 +27,6 @@ import { isImportRegular, transformError, buildSiemResponse, - validateLicenseForRuleType, } from '../utils'; import { ImportRuleAlertRest } from '../../types'; import { patchRules } from '../../rules/patch_rules'; @@ -38,7 +40,7 @@ type PromiseFromStreams = ImportRuleAlertRest | Error; const CHUNK_PARSED_OBJECT_SIZE = 10; -export const importRulesRoute = (router: IRouter, config: ConfigType) => { +export const importRulesRoute = (router: IRouter, config: ConfigType, ml: SetupPlugins['ml']) => { router.post( { path: `${DETECTION_ENGINE_RULES_URL}/_import`, @@ -67,6 +69,8 @@ export const importRulesRoute = (router: IRouter, config: ConfigType) => { return siemResponse.error({ statusCode: 404 }); } + const mlAuthz = buildMlAuthz({ license: context.licensing.license, ml, request }); + const { filename } = request.body.file.hapi; const fileExtension = extname(filename).toLowerCase(); if (fileExtension !== '.ndjson') { @@ -148,10 +152,7 @@ export const importRulesRoute = (router: IRouter, config: ConfigType) => { } = parsedRule; try { - validateLicenseForRuleType({ - license: context.licensing.license, - ruleType: type, - }); + throwHttpError(await mlAuthz.validateRuleType(type)); const rule = await readRules({ alertsClient, ruleId }); if (rule == null) { @@ -207,8 +208,7 @@ export const importRulesRoute = (router: IRouter, config: ConfigType) => { timelineTitle, meta, filters, - id: undefined, - ruleId, + rule, index, interval, maxSignals, @@ -240,7 +240,7 @@ export const importRulesRoute = (router: IRouter, config: ConfigType) => { resolve( createBulkErrorObject({ ruleId, - statusCode: 400, + statusCode: err.statusCode ?? 400, message: err.message, }) ); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts index a1f39936dd674e..24b2d5631b3a7f 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.test.ts @@ -5,6 +5,8 @@ */ import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { mlServicesMock, mlAuthzMock as mockMlAuthzFactory } from '../../../machine_learning/mocks'; +import { buildMlAuthz } from '../../../machine_learning/authz'; import { getEmptyFindResult, typicalPayload, @@ -17,9 +19,12 @@ import { serverMock, requestContextMock, requestMock } from '../__mocks__'; import { patchRulesBulkRoute } from './patch_rules_bulk_route'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; +jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); + describe('patch_rules_bulk', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); + let ml: ReturnType; beforeAll(() => { setFeatureFlagsForTestsOnly(); @@ -32,11 +37,12 @@ describe('patch_rules_bulk', () => { beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); + ml = mlServicesMock.create(); clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists clients.alertsClient.update.mockResolvedValue(getResult()); // update succeeds - patchRulesBulkRoute(server.router); + patchRulesBulkRoute(server.router, ml); }); describe('status codes with actionClient and alertClient', () => { @@ -90,21 +96,51 @@ describe('patch_rules_bulk', () => { expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); }); - it('rejects patching of an ML rule with an insufficient license', async () => { - (context.licensing.license.hasAtLeast as jest.Mock).mockReturnValue(false); + it('rejects patching a rule to ML if mlAuthz fails', async () => { + (buildMlAuthz as jest.Mock).mockReturnValueOnce({ + validateRuleType: jest + .fn() + .mockResolvedValue({ valid: false, message: 'mocked validation message' }), + }); const request = requestMock.create({ method: 'patch', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, body: [typicalMlRulePayload()], }); + const response = await server.inject(request, context); + expect(response.status).toEqual(200); + expect(response.body).toEqual([ + { + error: { + message: 'mocked validation message', + status_code: 403, + }, + rule_id: 'rule-1', + }, + ]); + }); + + it('rejects patching an existing ML rule if mlAuthz fails', async () => { + (buildMlAuthz as jest.Mock).mockReturnValueOnce({ + validateRuleType: jest + .fn() + .mockResolvedValue({ valid: false, message: 'mocked validation message' }), + }); + const { type, ...payloadWithoutType } = typicalMlRulePayload(); + const request = requestMock.create({ + method: 'patch', + path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, + body: [payloadWithoutType], + }); const response = await server.inject(request, context); + expect(response.status).toEqual(200); expect(response.body).toEqual([ { error: { - message: 'Your license does not support machine learning. Please upgrade your license.', - status_code: 400, + message: 'mocked validation message', + status_code: 403, }, rule_id: 'rule-1', }, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts index 201e1f823b4cbb..69789fe9466221 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts @@ -6,13 +6,11 @@ import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { SetupPlugins } from '../../../../plugin'; +import { buildMlAuthz } from '../../../machine_learning/authz'; +import { throwHttpError } from '../../../machine_learning/validation'; import { PatchRuleAlertParamsRest } from '../../rules/types'; -import { - transformBulkError, - buildRouteValidation, - buildSiemResponse, - validateLicenseForRuleType, -} from '../utils'; +import { transformBulkError, buildRouteValidation, buildSiemResponse } from '../utils'; import { getIdBulkError } from './utils'; import { transformValidateBulkError, validate } from './validate'; import { patchRulesBulkSchema } from '../schemas/patch_rules_bulk_schema'; @@ -20,8 +18,9 @@ import { rulesBulkSchema } from '../schemas/response/rules_bulk_schema'; import { patchRules } from '../../rules/patch_rules'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; +import { readRules } from '../../rules/read_rules'; -export const patchRulesBulkRoute = (router: IRouter) => { +export const patchRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => { router.patch( { path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, @@ -42,6 +41,7 @@ export const patchRulesBulkRoute = (router: IRouter) => { return siemResponse.error({ statusCode: 404 }); } + const mlAuthz = buildMlAuthz({ license: context.licensing.license, ml, request }); const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const rules = await Promise.all( request.body.map(async payloadRule => { @@ -81,10 +81,18 @@ export const patchRulesBulkRoute = (router: IRouter) => { const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)'; try { if (type) { - validateLicenseForRuleType({ license: context.licensing.license, ruleType: type }); + // reject an unauthorized "promotion" to ML + throwHttpError(await mlAuthz.validateRuleType(type)); + } + + const existingRule = await readRules({ alertsClient, ruleId, id }); + if (existingRule?.params.type) { + // reject an unauthorized modification of an ML rule + throwHttpError(await mlAuthz.validateRuleType(existingRule?.params.type)); } const rule = await patchRules({ + rule: existingRule, alertsClient, description, enabled, @@ -99,8 +107,6 @@ export const patchRulesBulkRoute = (router: IRouter) => { timelineTitle, meta, filters, - id, - ruleId, index, interval, maxSignals, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts index dbb0a3bb3e1dae..9ae7e83ef7989a 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.test.ts @@ -5,6 +5,8 @@ */ import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { mlServicesMock, mlAuthzMock as mockMlAuthzFactory } from '../../../machine_learning/mocks'; +import { buildMlAuthz } from '../../../machine_learning/authz'; import { getEmptyFindResult, getFindResultStatus, @@ -19,9 +21,12 @@ import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { patchRulesRoute } from './patch_rules_route'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; +jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); + describe('patch_rules', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); + let ml: ReturnType; beforeAll(() => { setFeatureFlagsForTestsOnly(); @@ -34,13 +39,14 @@ describe('patch_rules', () => { beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); + ml = mlServicesMock.create(); clients.alertsClient.get.mockResolvedValue(getResult()); // existing rule clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); // existing rule clients.alertsClient.update.mockResolvedValue(getResult()); // successful update clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); // successful transform - patchRulesRoute(server.router); + patchRulesRoute(server.router, ml); }); describe('status codes with actionClient and alertClient', () => { @@ -112,8 +118,12 @@ describe('patch_rules', () => { ); }); - it('rejects patching a rule to ML if licensing is not platinum', async () => { - (context.licensing.license.hasAtLeast as jest.Mock).mockReturnValue(false); + it('rejects patching a rule to ML if mlAuthz fails', async () => { + (buildMlAuthz as jest.Mock).mockReturnValueOnce({ + validateRuleType: jest + .fn() + .mockResolvedValue({ valid: false, message: 'mocked validation message' }), + }); const request = requestMock.create({ method: 'patch', path: DETECTION_ENGINE_RULES_URL, @@ -121,10 +131,31 @@ describe('patch_rules', () => { }); const response = await server.inject(request, context); - expect(response.status).toEqual(400); + expect(response.status).toEqual(403); + expect(response.body).toEqual({ + message: 'mocked validation message', + status_code: 403, + }); + }); + + it('rejects patching an ML rule if mlAuthz fails', async () => { + (buildMlAuthz as jest.Mock).mockReturnValueOnce({ + validateRuleType: jest + .fn() + .mockResolvedValue({ valid: false, message: 'mocked validation message' }), + }); + const { type, ...payloadWithoutType } = typicalMlRulePayload(); + const request = requestMock.create({ + method: 'patch', + path: DETECTION_ENGINE_RULES_URL, + body: payloadWithoutType, + }); + const response = await server.inject(request, context); + + expect(response.status).toEqual(403); expect(response.body).toEqual({ - message: 'Your license does not support machine learning. Please upgrade your license.', - status_code: 400, + message: 'mocked validation message', + status_code: 403, }); }); }); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts index 00ccd3059b38d9..ae23e0efc857d3 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/patch_rules_route.ts @@ -6,21 +6,20 @@ import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { SetupPlugins } from '../../../../plugin'; +import { buildMlAuthz } from '../../../machine_learning/authz'; +import { throwHttpError } from '../../../machine_learning/validation'; import { patchRules } from '../../rules/patch_rules'; import { PatchRuleAlertParamsRest } from '../../rules/types'; import { patchRulesSchema } from '../schemas/patch_rules_schema'; -import { - buildRouteValidation, - transformError, - buildSiemResponse, - validateLicenseForRuleType, -} from '../utils'; +import { buildRouteValidation, transformError, buildSiemResponse } from '../utils'; import { getIdError } from './utils'; import { transformValidate } from './validate'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; +import { readRules } from '../../rules/read_rules'; -export const patchRulesRoute = (router: IRouter) => { +export const patchRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { router.patch( { path: DETECTION_ENGINE_RULES_URL, @@ -68,10 +67,6 @@ export const patchRulesRoute = (router: IRouter) => { const siemResponse = buildSiemResponse(response); try { - if (type) { - validateLicenseForRuleType({ license: context.licensing.license, ruleType: type }); - } - const alertsClient = context.alerting?.getAlertsClient(); const savedObjectsClient = context.core.savedObjects.client; @@ -79,6 +74,18 @@ export const patchRulesRoute = (router: IRouter) => { return siemResponse.error({ statusCode: 404 }); } + const mlAuthz = buildMlAuthz({ license: context.licensing.license, ml, request }); + if (type) { + // reject an unauthorized "promotion" to ML + throwHttpError(await mlAuthz.validateRuleType(type)); + } + + const existingRule = await readRules({ alertsClient, ruleId, id }); + if (existingRule?.params.type) { + // reject an unauthorized modification of an ML rule + throwHttpError(await mlAuthz.validateRuleType(existingRule?.params.type)); + } + const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const rule = await patchRules({ alertsClient, @@ -95,8 +102,7 @@ export const patchRulesRoute = (router: IRouter) => { timelineTitle, meta, filters, - id, - ruleId, + rule: existingRule, index, interval, maxSignals, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts index 332a47d0c0fc25..e48c72ce9579e0 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts @@ -4,6 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ +import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { mlServicesMock, mlAuthzMock as mockMlAuthzFactory } from '../../../machine_learning/mocks'; +import { buildMlAuthz } from '../../../machine_learning/authz'; import { getEmptyFindResult, getResult, @@ -16,12 +19,14 @@ import { import { serverMock, requestContextMock, requestMock } from '../__mocks__'; import { updateRulesBulkRoute } from './update_rules_bulk_route'; import { BulkError } from '../utils'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; +jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); + describe('update_rules_bulk', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); + let ml: ReturnType; beforeAll(() => { setFeatureFlagsForTestsOnly(); @@ -34,12 +39,13 @@ describe('update_rules_bulk', () => { beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); + ml = mlServicesMock.create(); clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); clients.alertsClient.update.mockResolvedValue(getResult()); clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatus()); - updateRulesBulkRoute(server.router); + updateRulesBulkRoute(server.router, ml); }); describe('status codes with actionClient and alertClient', () => { @@ -92,8 +98,12 @@ describe('update_rules_bulk', () => { expect(response.body).toEqual(expected); }); - it('returns an error object if creating an ML rule with an insufficient license', async () => { - (context.licensing.license.hasAtLeast as jest.Mock).mockReturnValue(false); + it('returns a 403 error object if mlAuthz fails', async () => { + (buildMlAuthz as jest.Mock).mockReturnValueOnce({ + validateRuleType: jest + .fn() + .mockResolvedValue({ valid: false, message: 'mocked validation message' }), + }); const request = requestMock.create({ method: 'put', path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, @@ -105,8 +115,8 @@ describe('update_rules_bulk', () => { expect(response.body).toEqual([ { error: { - message: 'Your license does not support machine learning. Please upgrade your license.', - status_code: 400, + message: 'mocked validation message', + status_code: 403, }, rule_id: 'rule-1', }, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index 6d8f2243787e87..11892898d214b6 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -6,22 +6,20 @@ import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { SetupPlugins } from '../../../../plugin'; +import { buildMlAuthz } from '../../../machine_learning/authz'; +import { throwHttpError } from '../../../machine_learning/validation'; import { UpdateRuleAlertParamsRest } from '../../rules/types'; import { getIdBulkError } from './utils'; import { transformValidateBulkError, validate } from './validate'; -import { - buildRouteValidation, - transformBulkError, - buildSiemResponse, - validateLicenseForRuleType, -} from '../utils'; +import { buildRouteValidation, transformBulkError, buildSiemResponse } from '../utils'; import { updateRulesBulkSchema } from '../schemas/update_rules_bulk_schema'; import { updateRules } from '../../rules/update_rules'; import { rulesBulkSchema } from '../schemas/response/rules_bulk_schema'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; -export const updateRulesBulkRoute = (router: IRouter) => { +export const updateRulesBulkRoute = (router: IRouter, ml: SetupPlugins['ml']) => { router.put( { path: `${DETECTION_ENGINE_RULES_URL}/_bulk_update`, @@ -43,6 +41,7 @@ export const updateRulesBulkRoute = (router: IRouter) => { return siemResponse.error({ statusCode: 404 }); } + const mlAuthz = buildMlAuthz({ license: context.licensing.license, ml, request }); const ruleStatusClient = ruleStatusSavedObjectsClientFactory(savedObjectsClient); const rules = await Promise.all( request.body.map(async payloadRule => { @@ -83,7 +82,7 @@ export const updateRulesBulkRoute = (router: IRouter) => { const finalIndex = outputIndex ?? siemClient.getSignalsIndex(); const idOrRuleIdOrUnknown = id ?? ruleId ?? '(unknown id)'; try { - validateLicenseForRuleType({ license: context.licensing.license, ruleType: type }); + throwHttpError(await mlAuthz.validateRuleType(type)); const rule = await updateRules({ alertsClient, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts index 53c52153e84e6e..ce25a0204a6063 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.test.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { updateRulesRoute } from './update_rules_route'; +import { mlServicesMock, mlAuthzMock as mockMlAuthzFactory } from '../../../machine_learning/mocks'; +import { buildMlAuthz } from '../../../machine_learning/authz'; import { getEmptyFindResult, getResult, @@ -19,11 +20,15 @@ import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../../feature_flags'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; +import { updateRulesRoute } from './update_rules_route'; + +jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); jest.mock('../../rules/update_rules_notifications'); describe('update_rules', () => { let server: ReturnType; let { clients, context } = requestContextMock.createTools(); + let ml: ReturnType; beforeAll(() => { setFeatureFlagsForTestsOnly(); @@ -36,13 +41,14 @@ describe('update_rules', () => { beforeEach(() => { server = serverMock.create(); ({ clients, context } = requestContextMock.createTools()); + ml = mlServicesMock.create(); clients.alertsClient.get.mockResolvedValue(getResult()); // existing rule clients.alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); // rule exists clients.alertsClient.update.mockResolvedValue(getResult()); // successful update clients.savedObjectsClient.find.mockResolvedValue(getFindResultStatusEmpty()); // successful transform - updateRulesRoute(server.router); + updateRulesRoute(server.router, ml); }); describe('status codes with actionClient and alertClient', () => { @@ -106,8 +112,12 @@ describe('update_rules', () => { }); }); - it('rejects the request if licensing is not adequate', async () => { - (context.licensing.license.hasAtLeast as jest.Mock).mockReturnValue(false); + it('returns a 403 if mlAuthz fails', async () => { + (buildMlAuthz as jest.Mock).mockReturnValueOnce({ + validateRuleType: jest + .fn() + .mockResolvedValue({ valid: false, message: 'mocked validation message' }), + }); const request = requestMock.create({ method: 'put', path: DETECTION_ENGINE_RULES_URL, @@ -115,10 +125,10 @@ describe('update_rules', () => { }); const response = await server.inject(request, context); - expect(response.status).toEqual(400); + expect(response.status).toEqual(403); expect(response.body).toEqual({ - message: 'Your license does not support machine learning. Please upgrade your license.', - status_code: 400, + message: 'mocked validation message', + status_code: 403, }); }); }); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts index bfbeef8be2fea3..f15154a09657db 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -6,21 +6,19 @@ import { IRouter } from '../../../../../../../../src/core/server'; import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; +import { SetupPlugins } from '../../../../plugin'; +import { buildMlAuthz } from '../../../machine_learning/authz'; +import { throwHttpError } from '../../../machine_learning/validation'; import { UpdateRuleAlertParamsRest } from '../../rules/types'; import { updateRulesSchema } from '../schemas/update_rules_schema'; -import { - buildRouteValidation, - transformError, - buildSiemResponse, - validateLicenseForRuleType, -} from '../utils'; +import { buildRouteValidation, transformError, buildSiemResponse } from '../utils'; import { getIdError } from './utils'; import { transformValidate } from './validate'; import { updateRules } from '../../rules/update_rules'; import { updateRulesNotifications } from '../../rules/update_rules_notifications'; import { ruleStatusSavedObjectsClientFactory } from '../../signals/rule_status_saved_objects_client'; -export const updateRulesRoute = (router: IRouter) => { +export const updateRulesRoute = (router: IRouter, ml: SetupPlugins['ml']) => { router.put( { path: DETECTION_ENGINE_RULES_URL, @@ -69,8 +67,6 @@ export const updateRulesRoute = (router: IRouter) => { const siemResponse = buildSiemResponse(response); try { - validateLicenseForRuleType({ license: context.licensing.license, ruleType: type }); - const alertsClient = context.alerting?.getAlertsClient(); const savedObjectsClient = context.core.savedObjects.client; const siemClient = context.siem?.getSiemClient(); @@ -80,6 +76,9 @@ export const updateRulesRoute = (router: IRouter) => { return siemResponse.error({ statusCode: 404 }); } + const mlAuthz = buildMlAuthz({ license: context.licensing.license, ml, request }); + throwHttpError(await mlAuthz.validateRuleType(type)); + const finalIndex = outputIndex ?? siemClient.getSignalsIndex(); const rule = await updateRules({ alertsClient, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.ts index 25e76f367037a4..1c1bee58f0c973 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/schemas/response/check_type_dependents.ts @@ -8,7 +8,7 @@ import * as t from 'io-ts'; import { Either, left, fold } from 'fp-ts/lib/Either'; import { pipe } from 'fp-ts/lib/pipeable'; -import { isMlRule } from '../../../../../../common/detection_engine/ml_helpers'; +import { isMlRule } from '../../../../../../common/machine_learning/helpers'; import { dependentRulesSchema, RequiredRulesSchema, diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/utils.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/utils.test.ts index 8af5df60569135..fdb1cd148c7fa0 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/utils.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/utils.test.ts @@ -19,11 +19,9 @@ import { transformImportError, convertToSnakeCase, SiemResponseFactory, - validateLicenseForRuleType, } from './utils'; import { responseMock } from './__mocks__'; import { setFeatureFlagsForTestsOnly, unSetFeatureFlagsForTestsOnly } from '../feature_flags'; -import { licensingMock } from '../../../../../licensing/server/mocks'; describe('utils', () => { beforeAll(() => { @@ -361,36 +359,4 @@ describe('utils', () => { ); }); }); - - describe('validateLicenseForRuleType', () => { - let licenseMock: ReturnType; - - beforeEach(() => { - licenseMock = licensingMock.createLicenseMock(); - }); - - it('throws a BadRequestError if operating on an ML Rule with an insufficient license', () => { - licenseMock.hasAtLeast.mockReturnValue(false); - - expect(() => - validateLicenseForRuleType({ license: licenseMock, ruleType: 'machine_learning' }) - ).toThrowError(BadRequestError); - }); - - it('does not throw if operating on an ML Rule with a sufficient license', () => { - licenseMock.hasAtLeast.mockReturnValue(true); - - expect(() => - validateLicenseForRuleType({ license: licenseMock, ruleType: 'machine_learning' }) - ).not.toThrowError(BadRequestError); - }); - - it('does not throw if operating on a query rule', () => { - licenseMock.hasAtLeast.mockReturnValue(false); - - expect(() => - validateLicenseForRuleType({ license: licenseMock, ruleType: 'query' }) - ).not.toThrowError(BadRequestError); - }); - }); }); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/routes/utils.ts b/x-pack/plugins/siem/server/lib/detection_engine/routes/utils.ts index 52493a9be9b8fb..9903840b99c6f0 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/routes/utils.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/routes/utils.ts @@ -7,17 +7,12 @@ import Boom from 'boom'; import Joi from 'joi'; import { has, snakeCase } from 'lodash/fp'; -import { i18n } from '@kbn/i18n'; import { RouteValidationFunction, KibanaResponseFactory, CustomHttpResponseOptions, } from '../../../../../../../src/core/server'; -import { ILicense } from '../../../../../licensing/server'; -import { MINIMUM_ML_LICENSE } from '../../../../common/constants'; -import { RuleType } from '../../../../common/detection_engine/types'; -import { isMlRule } from '../../../../common/detection_engine/ml_helpers'; import { BadRequestError } from '../errors/bad_request_error'; export interface OutputError { @@ -294,28 +289,3 @@ export const convertToSnakeCase = >( return { ...acc, [newKey]: obj[item] }; }, {}); }; - -/** - * Checks the current Kibana License against the rule under operation. - * - * @param license ILicense representing the user license - * @param ruleType the type of the current rule - * - * @throws BadRequestError if rule and license are incompatible - */ -export const validateLicenseForRuleType = ({ - license, - ruleType, -}: { - license: ILicense; - ruleType: RuleType; -}): void => { - if (isMlRule(ruleType) && !license.hasAtLeast(MINIMUM_ML_LICENSE)) { - const message = i18n.translate('xpack.siem.licensing.unsupportedMachineLearningMessage', { - defaultMessage: - 'Your license does not support machine learning. Please upgrade your license.', - }); - - throw new BadRequestError(message); - } -}; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.test.ts index c551eb164ee07f..a42500223012e1 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.test.ts @@ -19,14 +19,14 @@ describe('patchRules', () => { }); it('should call alertsClient.disable is the rule was enabled and enabled is false', async () => { - const rule = getResult(); - alertsClient.get.mockResolvedValue(getResult()); + const existingRule = getResult(); + const params = getResult().params; await patchRules({ alertsClient, savedObjectsClient, - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - ...rule.params, + rule: existingRule, + ...params, enabled: false, interval: '', name: '', @@ -35,23 +35,23 @@ describe('patchRules', () => { expect(alertsClient.disable).toHaveBeenCalledWith( expect.objectContaining({ - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + id: existingRule.id, }) ); }); it('should call alertsClient.enable is the rule was disabled and enabled is true', async () => { - const rule = getResult(); - alertsClient.get.mockResolvedValue({ + const existingRule = { ...getResult(), enabled: false, - }); + }; + const params = getResult().params; await patchRules({ alertsClient, savedObjectsClient, - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - ...rule.params, + rule: existingRule, + ...params, enabled: true, interval: '', name: '', @@ -60,13 +60,13 @@ describe('patchRules', () => { expect(alertsClient.enable).toHaveBeenCalledWith( expect.objectContaining({ - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + id: existingRule.id, }) ); }); it('calls the alertsClient with ML params', async () => { - alertsClient.get.mockResolvedValue(getMlResult()); + const existingRule = getMlResult(); const params = { ...getMlResult().params, anomalyThreshold: 55, @@ -76,7 +76,7 @@ describe('patchRules', () => { await patchRules({ alertsClient, savedObjectsClient, - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + rule: existingRule, ...params, }); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts index da5e90ec14b0b5..6dfb72532afbbf 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/patch_rules.ts @@ -6,7 +6,6 @@ import { defaults } from 'lodash/fp'; import { PartialAlert } from '../../../../../alerting/server'; -import { readRules } from './read_rules'; import { PatchRuleParams } from './types'; import { addTags } from './add_tags'; import { calculateVersion, calculateName, calculateInterval } from './utils'; @@ -28,12 +27,11 @@ export const patchRules = async ({ filters, from, immutable, - id, - ruleId, index, interval, maxSignals, riskScore, + rule, name, severity, tags, @@ -47,7 +45,6 @@ export const patchRules = async ({ anomalyThreshold, machineLearningJobId, }: PatchRuleParams): Promise => { - const rule = await readRules({ alertsClient, ruleId, id }); if (rule == null) { return null; } diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/types.ts index b5dbfc92cf528e..217a966478e781 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/types.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/types.ts @@ -14,7 +14,7 @@ import { SavedObjectsClientContract, } from 'kibana/server'; import { AlertsClient, PartialAlert } from '../../../../../alerting/server'; -import { Alert } from '../../../../../alerting/common'; +import { Alert, SanitizedAlert } from '../../../../../alerting/common'; import { SIGNALS_ID } from '../../../../common/constants'; import { RuleAlertParams, RuleTypeParams, RuleAlertParamsRest } from '../types'; @@ -140,8 +140,8 @@ export interface Clients { alertsClient: AlertsClient; } -export type PatchRuleParams = Partial> & { - id: string | undefined | null; +export type PatchRuleParams = Partial> & { + rule: SanitizedAlert | null; savedObjectsClient: SavedObjectsClientContract; } & Clients; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.test.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.test.ts index e8fb4fa96ab512..2d77e9a707f746 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.test.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.test.ts @@ -6,7 +6,10 @@ import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks'; import { alertsClientMock } from '../../../../../alerting/server/mocks'; -import { mockPrepackagedRule } from '../routes/__mocks__/request_responses'; +import { + mockPrepackagedRule, + getFindResultWithSingleHit, +} from '../routes/__mocks__/request_responses'; import { updatePrepackagedRules } from './update_prepacked_rules'; import { patchRules } from './patch_rules'; jest.mock('./patch_rules'); @@ -31,6 +34,7 @@ describe('updatePrepackagedRules', () => { ]; const outputIndex = 'outputIndex'; const prepackagedRule = mockPrepackagedRule(); + alertsClient.find.mockResolvedValue(getFindResultWithSingleHit()); await updatePrepackagedRules( alertsClient, @@ -40,17 +44,8 @@ describe('updatePrepackagedRules', () => { ); expect(patchRules).toHaveBeenCalledWith( - expect.objectContaining({ - ruleId: 'rule-1', - }) - ); - expect(patchRules).not.toHaveBeenCalledWith( - expect.objectContaining({ + expect.not.objectContaining({ enabled: true, - }) - ); - expect(patchRules).not.toHaveBeenCalledWith( - expect.objectContaining({ actions, }) ); diff --git a/x-pack/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts index 4c183c51d16eab..618dee26b4812a 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/rules/update_prepacked_rules.ts @@ -8,6 +8,7 @@ import { SavedObjectsClientContract } from 'kibana/server'; import { AlertsClient } from '../../../../../alerting/server'; import { patchRules } from './patch_rules'; import { PrepackagedRules } from '../types'; +import { readRules } from './read_rules'; export const updatePrepackagedRules = async ( alertsClient: AlertsClient, @@ -15,63 +16,66 @@ export const updatePrepackagedRules = async ( rules: PrepackagedRules[], outputIndex: string ): Promise => { - await rules.forEach(async rule => { - const { - description, - false_positives: falsePositives, - from, - immutable, - query, - language, - saved_id: savedId, - meta, - filters, - rule_id: ruleId, - index, - interval, - max_signals: maxSignals, - risk_score: riskScore, - name, - severity, - tags, - to, - type, - threat, - references, - version, - note, - } = rule; + await Promise.all( + rules.map(async rule => { + const { + description, + false_positives: falsePositives, + from, + immutable, + query, + language, + saved_id: savedId, + meta, + filters, + rule_id: ruleId, + index, + interval, + max_signals: maxSignals, + risk_score: riskScore, + name, + severity, + tags, + to, + type, + threat, + references, + version, + note, + } = rule; - // Note: we do not pass down enabled as we do not want to suddenly disable - // or enable rules on the user when they were not expecting it if a rule updates - return patchRules({ - alertsClient, - description, - falsePositives, - from, - immutable, - query, - language, - outputIndex, - id: undefined, // We never have an id when updating from pre-packaged rules - savedId, - savedObjectsClient, - meta, - filters, - ruleId, - index, - interval, - maxSignals, - riskScore, - name, - severity, - tags, - to, - type, - threat, - references, - version, - note, - }); - }); + const existingRule = await readRules({ alertsClient, ruleId, id: undefined }); + + // Note: we do not pass down enabled as we do not want to suddenly disable + // or enable rules on the user when they were not expecting it if a rule updates + return patchRules({ + alertsClient, + description, + falsePositives, + from, + immutable, + query, + language, + outputIndex, + rule: existingRule, + savedId, + savedObjectsClient, + meta, + filters, + index, + interval, + maxSignals, + riskScore, + name, + severity, + tags, + to, + type, + threat, + references, + version, + note, + }); + }) + ); }; diff --git a/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts index ca259b3581720b..6160f34faef3f4 100644 --- a/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -8,7 +8,7 @@ import { performance } from 'perf_hooks'; import { Logger, KibanaRequest } from 'src/core/server'; import { SIGNALS_ID, DEFAULT_SEARCH_AFTER_PAGE_SIZE } from '../../../../common/constants'; -import { isJobStarted, isMlRule } from '../../../../common/detection_engine/ml_helpers'; +import { isJobStarted, isMlRule } from '../../../../common/machine_learning/helpers'; import { SetupPlugins } from '../../../plugin'; import { buildEventsSearchQuery } from './build_events_query'; diff --git a/x-pack/plugins/siem/server/lib/machine_learning/authz.test.ts b/x-pack/plugins/siem/server/lib/machine_learning/authz.test.ts new file mode 100644 index 00000000000000..93c3a74c713782 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/machine_learning/authz.test.ts @@ -0,0 +1,265 @@ +/* + * 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 { KibanaRequest } from '../../../../../../src/core/server'; +import { httpServerMock } from '../../../../../../src/core/server/mocks'; +import { hasMlAdminPermissions } from '../../../common/machine_learning/has_ml_admin_permissions'; +import { mlServicesMock } from './mocks'; +import { hasMlLicense, isMlAdmin, buildMlAuthz } from './authz'; +import { licensingMock } from '../../../../licensing/server/mocks'; + +jest.mock('../../../common/machine_learning/has_ml_admin_permissions'); + +describe('isMlAdmin', () => { + it('returns true if hasMlAdminPermissions is true', async () => { + const mockMl = mlServicesMock.create(); + const request = httpServerMock.createKibanaRequest(); + (hasMlAdminPermissions as jest.Mock).mockReturnValue(true); + + expect(await isMlAdmin({ ml: mockMl, request })).toEqual(true); + }); + + it('returns false if hasMlAdminPermissions is false', async () => { + const mockMl = mlServicesMock.create(); + const request = httpServerMock.createKibanaRequest(); + (hasMlAdminPermissions as jest.Mock).mockReturnValue(false); + + expect(await isMlAdmin({ ml: mockMl, request })).toEqual(false); + }); +}); + +describe('hasMlLicense', () => { + let licenseMock: ReturnType; + + beforeEach(() => { + licenseMock = licensingMock.createLicenseMock(); + }); + + it('returns false for an insufficient license', () => { + licenseMock.hasAtLeast.mockReturnValue(false); + + expect(hasMlLicense(licenseMock)).toEqual(false); + }); + + it('returns true for a sufficient license', () => { + licenseMock.hasAtLeast.mockReturnValue(true); + + expect(hasMlLicense(licenseMock)).toEqual(true); + }); +}); + +describe('mlAuthz', () => { + let licenseMock: ReturnType; + let mlMock: ReturnType; + let request: KibanaRequest; + + beforeEach(() => { + licenseMock = licensingMock.createLicenseMock(); + mlMock = mlServicesMock.create(); + request = httpServerMock.createKibanaRequest(); + }); + + describe('#validateRuleType', () => { + it('is valid for a non-ML rule when ML plugin is unavailable', async () => { + const mlAuthz = buildMlAuthz({ + license: licenseMock, + ml: undefined, + request, + }); + + const validation = await mlAuthz.validateRuleType('query'); + + expect(validation.valid).toEqual(true); + }); + + it('is invalid for an ML rule when ML plugin is unavailable', async () => { + const mlAuthz = buildMlAuthz({ + license: licenseMock, + ml: undefined, + request, + }); + + const validation = await mlAuthz.validateRuleType('machine_learning'); + + expect(validation.valid).toEqual(false); + expect(validation.message).toEqual( + 'The machine learning plugin is not available. Try enabling the plugin.' + ); + }); + + it('is valid for a non-ML rule when license is insufficient', async () => { + licenseMock.hasAtLeast.mockReturnValue(false); + + const mlAuthz = buildMlAuthz({ + license: licenseMock, + ml: mlMock, + request, + }); + + const validation = await mlAuthz.validateRuleType('query'); + + expect(validation.valid).toEqual(true); + }); + + it('is invalid for an ML rule when license is insufficient', async () => { + licenseMock.hasAtLeast.mockReturnValue(false); + + const mlAuthz = buildMlAuthz({ + license: licenseMock, + ml: mlMock, + request, + }); + + const validation = await mlAuthz.validateRuleType('machine_learning'); + + expect(validation.valid).toEqual(false); + expect(validation.message).toEqual( + 'Your license does not support machine learning. Please upgrade your license.' + ); + }); + + it('is valid for a non-ML rule when not an ML Admin', async () => { + (hasMlAdminPermissions as jest.Mock).mockReturnValue(false); + + const mlAuthz = buildMlAuthz({ + license: licenseMock, + ml: mlMock, + request, + }); + + const validation = await mlAuthz.validateRuleType('query'); + + expect(validation.valid).toEqual(true); + }); + + it('is invalid for an ML rule when not an ML Admin', async () => { + licenseMock.hasAtLeast.mockReturnValue(true); // prevents short-circuit on license check + (hasMlAdminPermissions as jest.Mock).mockReturnValue(false); + + const mlAuthz = buildMlAuthz({ + license: licenseMock, + ml: mlMock, + request, + }); + + const validation = await mlAuthz.validateRuleType('machine_learning'); + + expect(validation.valid).toEqual(false); + expect(validation.message).toEqual( + 'The current user is not a machine learning administrator.' + ); + }); + + it('is valid for an ML rule if ML available, license is sufficient, and an ML Admin', async () => { + licenseMock.hasAtLeast.mockReturnValue(true); + (hasMlAdminPermissions as jest.Mock).mockReturnValue(true); + + const mlAuthz = buildMlAuthz({ + license: licenseMock, + ml: mlMock, + request, + }); + + const validation = await mlAuthz.validateRuleType('machine_learning'); + + expect(validation.valid).toEqual(true); + expect(validation.message).toBeUndefined(); + }); + + it('only calls ml services once for multiple invocations', async () => { + const mockMlCapabilities = jest.fn(); + mlMock.mlSystemProvider.mockImplementation(() => ({ + mlInfo: jest.fn(), + mlSearch: jest.fn(), + mlCapabilities: mockMlCapabilities, + })); + + const mlAuthz = buildMlAuthz({ + license: licenseMock, + ml: mlMock, + request, + }); + + await mlAuthz.validateRuleType('machine_learning'); + await mlAuthz.validateRuleType('machine_learning'); + await mlAuthz.validateRuleType('machine_learning'); + + expect(mockMlCapabilities).toHaveBeenCalledTimes(1); + }); + + it('does not call ml services for non-ML rules', async () => { + const mockMlCapabilities = jest.fn(); + mlMock.mlSystemProvider.mockImplementation(() => ({ + mlInfo: jest.fn(), + mlSearch: jest.fn(), + mlCapabilities: mockMlCapabilities, + })); + + const mlAuthz = buildMlAuthz({ + license: licenseMock, + ml: mlMock, + request, + }); + + await mlAuthz.validateRuleType('query'); + await mlAuthz.validateRuleType('query'); + await mlAuthz.validateRuleType('query'); + + expect(mockMlCapabilities).not.toHaveBeenCalled(); + }); + + it('validates the same cache result per request if permissions change mid-stream', async () => { + licenseMock.hasAtLeast.mockReturnValueOnce(false); + licenseMock.hasAtLeast.mockReturnValueOnce(true); + + const mlAuthz = buildMlAuthz({ + license: licenseMock, + ml: mlMock, + request, + }); + + const validationFirst = await mlAuthz.validateRuleType('machine_learning'); + const validationSecond = await mlAuthz.validateRuleType('machine_learning'); + + expect(validationFirst.valid).toEqual(false); + expect(validationFirst.message).toEqual( + 'Your license does not support machine learning. Please upgrade your license.' + ); + expect(validationSecond.valid).toEqual(false); + expect(validationSecond.message).toEqual( + 'Your license does not support machine learning. Please upgrade your license.' + ); + }); + + it('will invalidate the cache result if the builder is called a second time after a license change', async () => { + licenseMock.hasAtLeast.mockReturnValueOnce(false); + licenseMock.hasAtLeast.mockReturnValueOnce(true); + (hasMlAdminPermissions as jest.Mock).mockReturnValueOnce(true); + + const mlAuthzFirst = buildMlAuthz({ + license: licenseMock, + ml: mlMock, + request, + }); + + const mlAuthzSecond = buildMlAuthz({ + license: licenseMock, + ml: mlMock, + request, + }); + + const validationFirst = await mlAuthzFirst.validateRuleType('machine_learning'); + const validationSecond = await mlAuthzSecond.validateRuleType('machine_learning'); + + expect(validationFirst.valid).toEqual(false); + expect(validationFirst.message).toEqual( + 'Your license does not support machine learning. Please upgrade your license.' + ); + expect(validationSecond.valid).toEqual(true); + expect(validationSecond.message).toBeUndefined(); + }); + }); +}); diff --git a/x-pack/plugins/siem/server/lib/machine_learning/authz.ts b/x-pack/plugins/siem/server/lib/machine_learning/authz.ts new file mode 100644 index 00000000000000..fb74f46244361c --- /dev/null +++ b/x-pack/plugins/siem/server/lib/machine_learning/authz.ts @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +import { KibanaRequest } from '../../../../../../src/core/server/'; +import { ILicense } from '../../../../licensing/server'; +import { MlPluginSetup } from '../../../../ml/server'; +import { SetupPlugins } from '../../plugin'; +import { MINIMUM_ML_LICENSE } from '../../../common/constants'; +import { hasMlAdminPermissions } from '../../../common/machine_learning/has_ml_admin_permissions'; +import { isMlRule } from '../../../common/machine_learning/helpers'; +import { RuleType } from '../../../common/detection_engine/types'; +import { Validation } from './validation'; +import { cache } from './cache'; + +export interface MlAuthz { + validateRuleType: (type: RuleType) => Promise; +} + +/** + * Builds ML authz services + * + * @param license A {@link ILicense} representing the user license + * @param ml {@link MlPluginSetup} ML services to fetch ML capabilities + * @param request A {@link KibanaRequest} representing the authenticated user + * + * @returns A {@link MLAuthz} service object + */ +export const buildMlAuthz = ({ + license, + ml, + request, +}: { + license: ILicense; + ml: SetupPlugins['ml']; + request: KibanaRequest; +}): MlAuthz => { + const cachedValidate = cache(() => validateMlAuthz({ license, ml, request })); + const validateRuleType = async (type: RuleType): Promise => { + if (!isMlRule(type)) { + return { valid: true, message: undefined }; + } else { + return cachedValidate(); + } + }; + + return { validateRuleType }; +}; + +/** + * Validates ML authorization for the current request + * + * @param license A {@link ILicense} representing the user license + * @param ml {@link MlPluginSetup} ML services to fetch ML capabilities + * @param request A {@link KibanaRequest} representing the authenticated user + * + * @returns A {@link Validation} validation + */ +export const validateMlAuthz = async ({ + license, + ml, + request, +}: { + license: ILicense; + ml: SetupPlugins['ml']; + request: KibanaRequest; +}): Promise => { + let message: string | undefined; + + if (ml == null) { + message = i18n.translate('xpack.siem.authz.mlUnavailable', { + defaultMessage: 'The machine learning plugin is not available. Try enabling the plugin.', + }); + } else if (!hasMlLicense(license)) { + message = i18n.translate('xpack.siem.licensing.unsupportedMachineLearningMessage', { + defaultMessage: + 'Your license does not support machine learning. Please upgrade your license.', + }); + } else if (!(await isMlAdmin({ ml, request }))) { + message = i18n.translate('xpack.siem.authz.userIsNotMlAdminMessage', { + defaultMessage: 'The current user is not a machine learning administrator.', + }); + } + + return { + valid: message === undefined, + message, + }; +}; + +/** + * Whether the license allows ML usage + * + * @param license A {@link ILicense} representing the user license + * + */ +export const hasMlLicense = (license: ILicense): boolean => license.hasAtLeast(MINIMUM_ML_LICENSE); + +/** + * Whether the requesting user is an ML Admin + * + * @param request A {@link KibanaRequest} representing the authenticated user + * @param ml {@link MlPluginSetup} ML services to fetch ML capabilities + * + */ +export const isMlAdmin = async ({ + request, + ml, +}: { + request: KibanaRequest; + ml: MlPluginSetup; +}): Promise => { + const scopedMlClient = ml.mlClient.asScoped(request).callAsCurrentUser; + const mlCapabilities = await ml.mlSystemProvider(scopedMlClient, request).mlCapabilities(); + return hasMlAdminPermissions(mlCapabilities); +}; diff --git a/x-pack/plugins/siem/server/lib/machine_learning/cache.test.ts b/x-pack/plugins/siem/server/lib/machine_learning/cache.test.ts new file mode 100644 index 00000000000000..14e4cfe8ebdda8 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/machine_learning/cache.test.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { cache } from './cache'; + +describe('cache', () => { + it('does not call the function if not invoked', () => { + const fn = jest.fn(); + cache(fn); + + expect(fn).not.toHaveBeenCalled(); + }); + + it('returns the function result', () => { + const fn = jest.fn().mockReturnValue('result'); + const cachedFn = cache(fn); + + expect(cachedFn()).toEqual('result'); + }); + + it('only calls the function once for multiple invocations', () => { + const fn = jest.fn(); + const cachedFn = cache(fn); + + cachedFn(); + cachedFn(); + cachedFn(); + + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('returns the function result on subsequent invocations', () => { + const fn = jest.fn().mockReturnValue('result'); + const cachedFn = cache(fn); + + expect([cachedFn(), cachedFn(), cachedFn()]).toEqual(['result', 'result', 'result']); + }); +}); diff --git a/x-pack/plugins/siem/server/lib/machine_learning/cache.ts b/x-pack/plugins/siem/server/lib/machine_learning/cache.ts new file mode 100644 index 00000000000000..1a7b95f2c5af24 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/machine_learning/cache.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * Caches the result of a function call + * + * @param fn the function to be invoked + * + * @returns A function that will invoke the given function on its first invocation, + * and then simply return the result on subsequent calls + */ +export const cache = (fn: () => T): (() => T) => { + let result: T | null = null; + + return () => { + if (result === null) { + result = fn(); + } + return result; + }; +}; diff --git a/x-pack/plugins/siem/server/lib/machine_learning/mocks.ts b/x-pack/plugins/siem/server/lib/machine_learning/mocks.ts new file mode 100644 index 00000000000000..f044022d6db69e --- /dev/null +++ b/x-pack/plugins/siem/server/lib/machine_learning/mocks.ts @@ -0,0 +1,32 @@ +/* + * 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 { MlPluginSetup } from '../../../../ml/server'; +import { elasticsearchServiceMock } from '../../../../../../src/core/server/mocks'; + +const createMockClient = () => elasticsearchServiceMock.createClusterClient(); +const createMockMlSystemProvider = () => + jest.fn(() => ({ + mlCapabilities: jest.fn(), + })); + +export const mlServicesMock = { + create: () => + (({ + mlSystemProvider: createMockMlSystemProvider(), + mlClient: createMockClient(), + } as unknown) as jest.Mocked), +}; + +const mockValidateRuleType = jest.fn().mockResolvedValue({ valid: true, message: undefined }); +const createBuildMlAuthzMock = () => + jest.fn().mockReturnValue({ validateRuleType: mockValidateRuleType }); + +export const mlAuthzMock = { + create: () => ({ + buildMlAuthz: createBuildMlAuthzMock(), + }), +}; diff --git a/x-pack/plugins/siem/server/lib/machine_learning/validation.test.ts b/x-pack/plugins/siem/server/lib/machine_learning/validation.test.ts new file mode 100644 index 00000000000000..effe59c073c59c --- /dev/null +++ b/x-pack/plugins/siem/server/lib/machine_learning/validation.test.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { toHttpError, throwHttpError } from './validation'; + +describe('toHttpError', () => { + it('returns nothing if validation is valid', () => { + expect(toHttpError({ valid: true, message: undefined })).toBeUndefined(); + }); + + it('returns an HTTP error if validation is invalid', () => { + const error = toHttpError({ valid: false, message: 'validation message' }); + expect(error?.statusCode).toEqual(403); + expect(error?.message).toEqual('validation message'); + }); +}); + +describe('throwHttpError', () => { + it('does nothing if validation is valid', () => { + expect(() => throwHttpError({ valid: true, message: undefined })).not.toThrowError(); + }); + + it('throws an error if validation is invalid', () => { + let error; + try { + throwHttpError({ valid: false, message: 'validation failed' }); + } catch (e) { + error = e; + } + expect(error?.statusCode).toEqual(403); + expect(error?.message).toEqual('validation failed'); + }); +}); diff --git a/x-pack/plugins/siem/server/lib/machine_learning/validation.ts b/x-pack/plugins/siem/server/lib/machine_learning/validation.ts new file mode 100644 index 00000000000000..eab85bbb510be2 --- /dev/null +++ b/x-pack/plugins/siem/server/lib/machine_learning/validation.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface Validation { + valid: boolean; + message: string | undefined; +} + +export class HttpAuthzError extends Error { + public readonly statusCode: number; + + constructor(message: string | undefined) { + super(message); + this.name = 'HttpAuthzError'; + this.statusCode = 403; + } +} + +export const toHttpError = (validation: Validation): HttpAuthzError | undefined => { + if (!validation.valid) { + return new HttpAuthzError(validation.message); + } +}; + +export const throwHttpError = (validation: Validation): void => { + const error = toHttpError(validation); + if (error) { + throw error; + } +}; diff --git a/x-pack/plugins/siem/server/plugin.ts b/x-pack/plugins/siem/server/plugin.ts index 3ef4b39bd0979c..d296ee94e89587 100644 --- a/x-pack/plugins/siem/server/plugin.ts +++ b/x-pack/plugins/siem/server/plugin.ts @@ -98,7 +98,8 @@ export class Plugin implements IPlugin { // Detection Engine Rule routes that have the REST endpoints of /api/detection_engine/rules // All REST rule creation, deletion, updating, etc...... - createRulesRoute(router); + createRulesRoute(router, ml); readRulesRoute(router); - updateRulesRoute(router); - patchRulesRoute(router); + updateRulesRoute(router, ml); + patchRulesRoute(router, ml); deleteRulesRoute(router); findRulesRoute(router); addPrepackedRulesRoute(router); getPrepackagedRulesStatusRoute(router); - createRulesBulkRoute(router); - updateRulesBulkRoute(router); - patchRulesBulkRoute(router); + createRulesBulkRoute(router, ml); + updateRulesBulkRoute(router, ml); + patchRulesBulkRoute(router, ml); deleteRulesBulkRoute(router); createTimelinesRoute(router, config, security); updateTimelinesRoute(router, config, security); - importRulesRoute(router, config); + importRulesRoute(router, config, ml); exportRulesRoute(router, config); importTimelinesRoute(router, config, security); diff --git a/x-pack/plugins/task_manager/server/saved_objects/index.ts b/x-pack/plugins/task_manager/server/saved_objects/index.ts index 0ad9021cd7f39c..fbb54b566ece0d 100644 --- a/x-pack/plugins/task_manager/server/saved_objects/index.ts +++ b/x-pack/plugins/task_manager/server/saved_objects/index.ts @@ -6,6 +6,7 @@ import { SavedObjectsServiceSetup } from 'kibana/server'; import mappings from './mappings.json'; +import { migrations } from './migrations'; import { TaskManagerConfig } from '../config.js'; export function setupSavedObjects( @@ -18,6 +19,7 @@ export function setupSavedObjects( hidden: true, convertToAliasScript: `ctx._id = ctx._source.type + ':' + ctx._id`, mappings: mappings.task, + migrations, indexPattern: config.index, }); } diff --git a/x-pack/plugins/task_manager/server/saved_objects/migrations.ts b/x-pack/plugins/task_manager/server/saved_objects/migrations.ts index 1c2cf73d0fe134..617ef80749d6a9 100644 --- a/x-pack/plugins/task_manager/server/saved_objects/migrations.ts +++ b/x-pack/plugins/task_manager/server/saved_objects/migrations.ts @@ -3,22 +3,23 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { SavedObject } from '../../../../../src/core/server'; +import { SavedObjectMigrationMap, SavedObjectUnsanitizedDoc } from '../../../../../src/core/server'; +import { TaskInstance, TaskInstanceWithDeprecatedFields } from '../task'; -export const migrations = { - task: { - '7.4.0': (doc: SavedObject>) => ({ - ...doc, - updated_at: new Date().toISOString(), - }), - '7.6.0': moveIntervalIntoSchedule, - }, +export const migrations: SavedObjectMigrationMap = { + '7.4.0': doc => ({ + ...doc, + updated_at: new Date().toISOString(), + }), + '7.6.0': moveIntervalIntoSchedule, }; function moveIntervalIntoSchedule({ attributes: { interval, ...attributes }, ...doc -}: SavedObject>) { +}: SavedObjectUnsanitizedDoc): SavedObjectUnsanitizedDoc< + TaskInstance +> { return { ...doc, attributes: { diff --git a/x-pack/plugins/uptime/common/constants/settings_defaults.ts b/x-pack/plugins/uptime/common/constants/settings_defaults.ts index 82575e875577bb..b9e99a54b3b11a 100644 --- a/x-pack/plugins/uptime/common/constants/settings_defaults.ts +++ b/x-pack/plugins/uptime/common/constants/settings_defaults.ts @@ -8,6 +8,6 @@ import { DynamicSettings } from '../runtime_types'; export const DYNAMIC_SETTINGS_DEFAULTS: DynamicSettings = { heartbeatIndices: 'heartbeat-8*', - certAgeThreshold: 365, + certAgeThreshold: 730, certExpirationThreshold: 30, }; diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/add_filter_btn.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/add_filter_btn.tsx index c52489984dab4e..c33cba73ee976c 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/add_filter_btn.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/add_filter_btn.tsx @@ -17,9 +17,9 @@ interface Props { export const AddFilterButton: React.FC = ({ newFilters, onNewFilter }) => { const [isPopoverOpen, setPopover] = useState(false); - const currentFilters = useFilterUpdate(); + const { selectedFilters } = useFilterUpdate(); - const getSelectedItems = (fieldName: string) => currentFilters.get(fieldName) || []; + const getSelectedItems = (fieldName: string) => selectedFilters.get(fieldName) || []; const onButtonClick = () => { setPopover(!isPopoverOpen); diff --git a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/filters_expression_select.tsx b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/filters_expression_select.tsx index 8298f202b94584..a6728643146df9 100644 --- a/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/filters_expression_select.tsx +++ b/x-pack/plugins/uptime/public/components/overview/alerts/monitor_expressions/filters_expression_select.tsx @@ -31,7 +31,10 @@ export const FiltersExpressionsSelect: React.FC = ({ values: string[]; }>({ fieldName: '', values: [] }); - const currentFilters = useFilterUpdate(updatedFieldValues.fieldName, updatedFieldValues.values); + const { selectedLocations, selectedPorts, selectedSchemes, selectedTags } = useFilterUpdate( + updatedFieldValues.fieldName, + updatedFieldValues.values + ); useEffect(() => { if (updatedFieldValues.fieldName === 'observer.geo.name') { @@ -45,13 +48,6 @@ export const FiltersExpressionsSelect: React.FC = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const selectedTags = currentFilters.get('tags'); - const selectedPorts = currentFilters.get('url.port'); - const selectedScheme = currentFilters.get('monitor.type'); - const selectedLocation = currentFilters.get('observer.geo.name'); - - const getSelectedItems = (fieldName: string) => currentFilters.get(fieldName) || []; - const onFilterFieldChange = (fieldName: string, values: string[]) => { setUpdatedFieldValues({ fieldName, values }); }; @@ -64,10 +60,11 @@ export const FiltersExpressionsSelect: React.FC = ({ id: 'filter_port', disabled: ports?.length === 0, items: ports?.map((p: number) => p.toString()) ?? [], - selectedItems: getSelectedItems('url.port'), + selectedItems: selectedPorts, title: filterLabels.PORT, - description: selectedPorts ? alertFilterLabels.USING_PORT : alertFilterLabels.USING, - value: selectedPorts?.join(',') ?? alertFilterLabels.ANY_PORT, + description: + selectedPorts.length === 0 ? alertFilterLabels.USING : alertFilterLabels.USING_PORT, + value: selectedPorts.length === 0 ? alertFilterLabels.ANY_PORT : selectedPorts?.join(','), }, { onFilterFieldChange, @@ -76,10 +73,10 @@ export const FiltersExpressionsSelect: React.FC = ({ id: 'filter_tags', disabled: tags?.length === 0, items: tags ?? [], - selectedItems: getSelectedItems('tags'), + selectedItems: selectedTags, title: filterLabels.TAGS, - description: selectedTags ? alertFilterLabels.WITH_TAG : alertFilterLabels.WITH, - value: selectedTags?.join(',') ?? alertFilterLabels.ANY_TAG, + description: selectedTags.length === 0 ? alertFilterLabels.WITH : alertFilterLabels.WITH_TAG, + value: selectedTags.length === 0 ? alertFilterLabels.ANY_TAG : selectedTags?.join(','), }, { onFilterFieldChange, @@ -88,10 +85,10 @@ export const FiltersExpressionsSelect: React.FC = ({ id: 'filter_scheme', disabled: schemes?.length === 0, items: schemes ?? [], - selectedItems: getSelectedItems('monitor.type'), + selectedItems: selectedSchemes, title: filterLabels.SCHEME, - description: selectedScheme ? alertFilterLabels.OF_TYPE : alertFilterLabels.OF, - value: selectedScheme?.join(',') ?? alertFilterLabels.ANY_TYPE, + description: selectedSchemes.length === 0 ? alertFilterLabels.OF : alertFilterLabels.OF_TYPE, + value: selectedSchemes.length === 0 ? alertFilterLabels.ANY_TYPE : selectedSchemes?.join(','), }, { onFilterFieldChange, @@ -100,10 +97,14 @@ export const FiltersExpressionsSelect: React.FC = ({ id: 'filter_location', disabled: locations?.length === 0, items: locations ?? [], - selectedItems: getSelectedItems('observer.geo.name'), + selectedItems: selectedLocations, title: filterLabels.SCHEME, - description: selectedLocation ? alertFilterLabels.FROM_LOCATION : alertFilterLabels.FROM, - value: selectedLocation?.join(',') ?? alertFilterLabels.ANY_LOCATION, + description: + selectedLocations.length === 0 ? alertFilterLabels.FROM : alertFilterLabels.FROM_LOCATION, + value: + selectedLocations.length === 0 + ? alertFilterLabels.ANY_LOCATION + : selectedLocations?.join(','), }, ]; diff --git a/x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_popover.test.tsx.snap b/x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_popover.test.tsx.snap index c7ffc36532b71c..2677fd828c9571 100644 --- a/x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_popover.test.tsx.snap +++ b/x-pack/plugins/uptime/public/components/overview/filter_group/__tests__/__snapshots__/filter_popover.test.tsx.snap @@ -7,7 +7,7 @@ exports[`FilterPopover component does not show item list when loading 1`] = ` @@ -26,7 +26,7 @@ exports[`FilterPopover component does not show item list when loading 1`] = ` @@ -64,7 +64,7 @@ exports[`FilterPopover component renders without errors for valid props 1`] = ` - 3 + 1 diff --git a/x-pack/plugins/uptime/public/components/overview/filter_group/filter_group.tsx b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_group.tsx index b881dfcdd2e00a..f5527a17b0cda4 100644 --- a/x-pack/plugins/uptime/public/components/overview/filter_group/filter_group.tsx +++ b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_group.tsx @@ -27,14 +27,15 @@ export const FilterGroupComponent: React.FC = ({ values: string[]; }>({ fieldName: '', values: [] }); - const currentFilters = useFilterUpdate(updatedFieldValues.fieldName, updatedFieldValues.values); + const { selectedLocations, selectedPorts, selectedSchemes, selectedTags } = useFilterUpdate( + updatedFieldValues.fieldName, + updatedFieldValues.values + ); const onFilterFieldChange = (fieldName: string, values: string[]) => { setUpdatedFieldValues({ fieldName, values }); }; - const getSelectedItems = (fieldName: string) => currentFilters.get(fieldName) || []; - const filterPopoverProps: FilterPopoverProps[] = [ { loading, @@ -42,7 +43,7 @@ export const FilterGroupComponent: React.FC = ({ fieldName: 'observer.geo.name', id: 'location', items: locations, - selectedItems: getSelectedItems('observer.geo.name'), + selectedItems: selectedLocations, title: filterLabels.LOCATION, }, { @@ -52,7 +53,7 @@ export const FilterGroupComponent: React.FC = ({ id: 'port', disabled: ports.length === 0, items: ports.map((p: number) => p.toString()), - selectedItems: getSelectedItems('url.port'), + selectedItems: selectedPorts, title: filterLabels.PORT, }, { @@ -62,7 +63,7 @@ export const FilterGroupComponent: React.FC = ({ id: 'scheme', disabled: schemes.length === 0, items: schemes, - selectedItems: getSelectedItems('monitor.type'), + selectedItems: selectedSchemes, title: filterLabels.SCHEME, }, { @@ -72,7 +73,7 @@ export const FilterGroupComponent: React.FC = ({ id: 'tags', disabled: tags.length === 0, items: tags, - selectedItems: getSelectedItems('tags'), + selectedItems: selectedTags, title: filterLabels.TAGS, }, ]; diff --git a/x-pack/plugins/uptime/public/components/overview/filter_group/filter_popover.tsx b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_popover.tsx index 18d40b83be3696..c04460237f4f13 100644 --- a/x-pack/plugins/uptime/public/components/overview/filter_group/filter_popover.tsx +++ b/x-pack/plugins/uptime/public/components/overview/filter_group/filter_popover.tsx @@ -33,7 +33,7 @@ export const FilterPopover = ({ id, disabled, loading, - items, + items: allItems, onFilterFieldChange, selectedItems, title, @@ -46,6 +46,16 @@ export const FilterPopover = ({ const [searchQuery, setSearchQuery] = useState(''); const [tempSelectedItems, setTempSelectedItems] = useState(selectedItems); + const [items, setItems] = useState([]); + + useEffect(() => { + // Merge incoming items with selected items, to enable deselection + + const mItems = selectedItems.concat(allItems ?? []); + const newItems = mItems.filter((item, index) => mItems.indexOf(item) === index); + setItems(newItems); + }, [allItems, selectedItems]); + useEffect(() => { if (searchQuery !== '') { const toDisplay = items.filter(item => item.indexOf(searchQuery) >= 0); @@ -60,7 +70,7 @@ export const FilterPopover = ({ button={ btnContent ?? ( 0} numFilters={items.length} numActiveFilters={tempSelectedItems.length} diff --git a/x-pack/plugins/uptime/public/hooks/use_filter_update.ts b/x-pack/plugins/uptime/public/hooks/use_filter_update.ts index 97ef6b0e67ad2e..e7e59ff2e22e63 100644 --- a/x-pack/plugins/uptime/public/hooks/use_filter_update.ts +++ b/x-pack/plugins/uptime/public/hooks/use_filter_update.ts @@ -12,8 +12,15 @@ import { useUrlParams } from './use_url_params'; * @param fieldName the name of the field to filter against * @param values the list of values to use when filter a field */ +interface SelectedFilters { + selectedTags: string[]; + selectedPorts: string[]; + selectedSchemes: string[]; + selectedLocations: string[]; + selectedFilters: Map; +} -export const useFilterUpdate = (fieldName?: string, values?: string[]) => { +export const useFilterUpdate = (fieldName?: string, values?: string[]): SelectedFilters => { const [getUrlParams, updateUrl] = useUrlParams(); const { filters: currentFilters } = getUrlParams(); @@ -52,5 +59,11 @@ export const useFilterUpdate = (fieldName?: string, values?: string[]) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [fieldName, values]); - return filterKueries; + return { + selectedTags: filterKueries.get('tags') || [], + selectedPorts: filterKueries.get('url.port') || [], + selectedSchemes: filterKueries.get('monitor.type') || [], + selectedLocations: filterKueries.get('observer.geo.name') || [], + selectedFilters: filterKueries, + }; }; diff --git a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts index a89e5ff62319d5..73d104c1d21aef 100644 --- a/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts +++ b/x-pack/plugins/uptime/server/lib/alerts/__tests__/status_check.test.ts @@ -87,7 +87,7 @@ describe('status check alert', () => { Object { "callES": [MockFunction], "dynamicSettings": Object { - "certAgeThreshold": 365, + "certAgeThreshold": 730, "certExpirationThreshold": 30, "heartbeatIndices": "heartbeat-8*", }, @@ -132,7 +132,7 @@ describe('status check alert', () => { Object { "callES": [MockFunction], "dynamicSettings": Object { - "certAgeThreshold": 365, + "certAgeThreshold": 730, "certExpirationThreshold": 30, "heartbeatIndices": "heartbeat-8*", }, diff --git a/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts b/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts index 88a78d048a3b60..f2f749a58dac3c 100644 --- a/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts +++ b/x-pack/test/api_integration/apis/management/ingest_pipelines/ingest_pipelines.ts @@ -19,7 +19,12 @@ export default function({ getService }: FtrProviderContext) { describe('Pipelines', function() { describe('Create', () => { const PIPELINE_ID = 'test_create_pipeline'; - after(() => deletePipeline(PIPELINE_ID)); + const REQUIRED_FIELDS_PIPELINE_ID = 'test_create_required_fields_pipeline'; + + after(() => { + deletePipeline(PIPELINE_ID); + deletePipeline(REQUIRED_FIELDS_PIPELINE_ID); + }); it('should create a pipeline', async () => { const { body } = await supertest @@ -52,6 +57,28 @@ export default function({ getService }: FtrProviderContext) { }); }); + it('should create a pipeline with only required fields', async () => { + const { body } = await supertest + .post(API_BASE_PATH) + .set('kbn-xsrf', 'xxx') + // Excludes description, version and on_failure processors + .send({ + name: REQUIRED_FIELDS_PIPELINE_ID, + processors: [ + { + script: { + source: 'ctx._type = null', + }, + }, + ], + }) + .expect(200); + + expect(body).to.eql({ + acknowledged: true, + }); + }); + it('should not allow creation of an existing pipeline', async () => { const { body } = await supertest .post(API_BASE_PATH) diff --git a/x-pack/test/api_integration/apis/ml/job_validation/cardinality.ts b/x-pack/test/api_integration/apis/ml/job_validation/cardinality.ts new file mode 100644 index 00000000000000..e51a3b3c1772c2 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/job_validation/cardinality.ts @@ -0,0 +1,175 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/machine_learning/security_common'; + +const COMMON_HEADERS = { + 'kbn-xsrf': 'some-xsrf-token', +}; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + describe('ValidateCardinality', function() { + before(async () => { + await esArchiver.loadIfNeeded('ml/ecommerce'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it(`should recognize a valid cardinality`, async () => { + const requestBody = { + job_id: '', + description: '', + groups: [], + analysis_config: { + bucket_span: '10m', + detectors: [ + { + function: 'mean', + field_name: 'products.base_price', + partition_field_name: 'geoip.city_name', + }, + ], + influencers: ['geoip.city_name'], + }, + data_description: { time_field: 'order_date' }, + analysis_limits: { model_memory_limit: '12MB' }, + model_plot_config: { enabled: true }, + datafeed_config: { + datafeed_id: 'datafeed-', + job_id: '', + indices: ['ft_ecommerce'], + query: { bool: { must: [{ match_all: {} }], filter: [], must_not: [] } }, + }, + }; + + const { body } = await supertest + .post('/api/ml/validate/cardinality') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_HEADERS) + .send(requestBody) + .expect(200); + + expect(body).to.eql([{ id: 'success_cardinality' }]); + }); + + it(`should recognize a high model plot cardinality`, async () => { + const requestBody = { + job_id: '', + description: '', + groups: [], + analysis_config: { + bucket_span: '10m', + detectors: [ + { + function: 'mean', + field_name: 'products.base_price', + // some high cardinality field + partition_field_name: 'order_id', + }, + ], + influencers: ['geoip.city_name'], + }, + data_description: { time_field: 'order_date' }, + analysis_limits: { model_memory_limit: '11MB' }, + model_plot_config: { enabled: true }, + datafeed_config: { + datafeed_id: 'datafeed-', + job_id: '', + indices: ['ft_ecommerce'], + query: { bool: { must: [{ match_all: {} }], filter: [], must_not: [] } }, + }, + }; + const { body } = await supertest + .post('/api/ml/validate/cardinality') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_HEADERS) + .send(requestBody) + .expect(200); + + expect(body).to.eql([ + { id: 'cardinality_model_plot_high', modelPlotCardinality: 4711 }, + { id: 'cardinality_partition_field', fieldName: 'order_id' }, + ]); + }); + + it('should not validate cardinality in case request payload is invalid', async () => { + const requestBody = { + job_id: '', + description: '', + groups: [], + // missing analysis_config + data_description: { time_field: 'order_date' }, + analysis_limits: { model_memory_limit: '12MB' }, + model_plot_config: { enabled: true }, + datafeed_config: { + datafeed_id: 'datafeed-', + job_id: '', + indices: ['ft_ecommerce'], + query: { bool: { must: [{ match_all: {} }], filter: [], must_not: [] } }, + }, + }; + + const { body } = await supertest + .post('/api/ml/validate/cardinality') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_HEADERS) + .send(requestBody) + .expect(400); + + expect(body.error).to.eql('Bad Request'); + expect(body.message).to.eql( + '[request body.analysis_config.detectors]: expected value of type [array] but got [undefined]' + ); + }); + + it('should not validate cardinality if the user does not have required permissions', async () => { + const requestBody = { + job_id: '', + description: '', + groups: [], + analysis_config: { + bucket_span: '10m', + detectors: [ + { + function: 'mean', + field_name: 'products.base_price', + partition_field_name: 'geoip.city_name', + }, + ], + influencers: ['geoip.city_name'], + }, + data_description: { time_field: 'order_date' }, + analysis_limits: { model_memory_limit: '12MB' }, + model_plot_config: { enabled: true }, + datafeed_config: { + datafeed_id: 'datafeed-', + job_id: '', + indices: ['ft_ecommerce'], + query: { bool: { must: [{ match_all: {} }], filter: [], must_not: [] } }, + }, + }; + + const { body } = await supertest + .post('/api/ml/validate/cardinality') + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_HEADERS) + .send(requestBody) + .expect(404); + + expect(body.error).to.eql('Not Found'); + expect(body.message).to.eql('Not Found'); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/job_validation/index.ts b/x-pack/test/api_integration/apis/ml/job_validation/index.ts index 6ca9dcbbe9e5b8..fa894de839cd28 100644 --- a/x-pack/test/api_integration/apis/ml/job_validation/index.ts +++ b/x-pack/test/api_integration/apis/ml/job_validation/index.ts @@ -9,5 +9,6 @@ export default function({ loadTestFile }: FtrProviderContext) { describe('job validation', function() { loadTestFile(require.resolve('./bucket_span_estimator')); loadTestFile(require.resolve('./calculate_model_memory_limit')); + loadTestFile(require.resolve('./cardinality')); }); } diff --git a/x-pack/test/api_integration/apis/siem/authentications.ts b/x-pack/test/api_integration/apis/siem/authentications.ts index b89a1448d5fe6f..2a9a6d669f3fd0 100644 --- a/x-pack/test/api_integration/apis/siem/authentications.ts +++ b/x-pack/test/api_integration/apis/siem/authentications.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; -import { authenticationsQuery } from '../../../../plugins/siem/public/containers/authentications/index.gql_query'; +import { authenticationsQuery } from '../../../../plugins/siem/public/hosts/containers/authentications/index.gql_query'; import { GetAuthenticationsQuery } from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/test/api_integration/apis/siem/hosts.ts b/x-pack/test/api_integration/apis/siem/hosts.ts index 0a2ee9c82bce22..8acf87d88c5fa8 100644 --- a/x-pack/test/api_integration/apis/siem/hosts.ts +++ b/x-pack/test/api_integration/apis/siem/hosts.ts @@ -13,9 +13,9 @@ import { GetHostsTableQuery, HostsFields, } from '../../../../plugins/siem/public/graphql/types'; -import { HostOverviewQuery } from '../../../../plugins/siem/public/containers/hosts/overview/host_overview.gql_query'; -import { HostFirstLastSeenGqlQuery } from '../../../../plugins/siem/public/containers/hosts/first_last_seen/first_last_seen.gql_query'; -import { HostsTableQuery } from '../../../../plugins/siem/public/containers/hosts/hosts_table.gql_query'; +import { HostOverviewQuery } from '../../../../plugins/siem/public/hosts/containers/hosts/overview/host_overview.gql_query'; +import { HostFirstLastSeenGqlQuery } from '../../../../plugins/siem/public/hosts/containers/hosts/first_last_seen/first_last_seen.gql_query'; +import { HostsTableQuery } from '../../../../plugins/siem/public/hosts/containers/hosts/hosts_table.gql_query'; import { FtrProviderContext } from '../../ftr_provider_context'; const FROM = new Date('2000-01-01T00:00:00.000Z').valueOf(); diff --git a/x-pack/test/api_integration/apis/siem/ip_overview.ts b/x-pack/test/api_integration/apis/siem/ip_overview.ts index 2f1a792aff25bf..c0ed9cd2da8427 100644 --- a/x-pack/test/api_integration/apis/siem/ip_overview.ts +++ b/x-pack/test/api_integration/apis/siem/ip_overview.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { ipOverviewQuery } from '../../../../plugins/siem/public/containers/ip_overview/index.gql_query'; +import { ipOverviewQuery } from '../../../../plugins/siem/public/network/containers/ip_overview/index.gql_query'; import { GetIpOverviewQuery } from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/test/api_integration/apis/siem/kpi_host_details.ts b/x-pack/test/api_integration/apis/siem/kpi_host_details.ts index 30f9f6f04a2423..c108a6dcbc7497 100644 --- a/x-pack/test/api_integration/apis/siem/kpi_host_details.ts +++ b/x-pack/test/api_integration/apis/siem/kpi_host_details.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { kpiHostDetailsQuery } from '../../../../plugins/siem/public/containers/kpi_host_details/index.gql_query'; +import { kpiHostDetailsQuery } from '../../../../plugins/siem/public/hosts/containers/kpi_host_details/index.gql_query'; import { GetKpiHostDetailsQuery } from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/test/api_integration/apis/siem/kpi_hosts.ts b/x-pack/test/api_integration/apis/siem/kpi_hosts.ts index 2303b9ecfb78fd..ed4a19f2d7d996 100644 --- a/x-pack/test/api_integration/apis/siem/kpi_hosts.ts +++ b/x-pack/test/api_integration/apis/siem/kpi_hosts.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { kpiHostsQuery } from '../../../../plugins/siem/public/containers/kpi_hosts/index.gql_query'; +import { kpiHostsQuery } from '../../../../plugins/siem/public/hosts/containers/kpi_hosts/index.gql_query'; import { GetKpiHostsQuery } from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/test/api_integration/apis/siem/kpi_network.ts b/x-pack/test/api_integration/apis/siem/kpi_network.ts index 22e133e48bbd2b..28f7c80eb32042 100644 --- a/x-pack/test/api_integration/apis/siem/kpi_network.ts +++ b/x-pack/test/api_integration/apis/siem/kpi_network.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { kpiNetworkQuery } from '../../../../plugins/siem/public/containers/kpi_network/index.gql_query'; +import { kpiNetworkQuery } from '../../../../plugins/siem/public/network/containers/kpi_network/index.gql_query'; import { GetKpiNetworkQuery } from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/test/api_integration/apis/siem/network_dns.ts b/x-pack/test/api_integration/apis/siem/network_dns.ts index 1eba41e238c819..590727362d7ae8 100644 --- a/x-pack/test/api_integration/apis/siem/network_dns.ts +++ b/x-pack/test/api_integration/apis/siem/network_dns.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { networkDnsQuery } from '../../../../plugins/siem/public/containers/network_dns/index.gql_query'; +import { networkDnsQuery } from '../../../../plugins/siem/public/network/containers/network_dns/index.gql_query'; import { Direction, GetNetworkDnsQuery, diff --git a/x-pack/test/api_integration/apis/siem/network_top_n_flow.ts b/x-pack/test/api_integration/apis/siem/network_top_n_flow.ts index 6ab7945e9000d6..19948967c18090 100644 --- a/x-pack/test/api_integration/apis/siem/network_top_n_flow.ts +++ b/x-pack/test/api_integration/apis/siem/network_top_n_flow.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { networkTopNFlowQuery } from '../../../../plugins/siem/public/containers/network_top_n_flow/index.gql_query'; +import { networkTopNFlowQuery } from '../../../../plugins/siem/public/network/containers/network_top_n_flow/index.gql_query'; import { Direction, FlowTargetSourceDest, diff --git a/x-pack/test/api_integration/apis/siem/overview_host.ts b/x-pack/test/api_integration/apis/siem/overview_host.ts index 95dbb44e30c41c..fe9d04a16c626f 100644 --- a/x-pack/test/api_integration/apis/siem/overview_host.ts +++ b/x-pack/test/api_integration/apis/siem/overview_host.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { DEFAULT_INDEX_PATTERN } from '../../../../plugins/siem/common/constants'; -import { overviewHostQuery } from '../../../../plugins/siem/public/containers/overview/overview_host/index.gql_query'; +import { overviewHostQuery } from '../../../../plugins/siem/public/overview/containers//overview_host/index.gql_query'; import { GetOverviewHostQuery } from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/test/api_integration/apis/siem/overview_network.ts b/x-pack/test/api_integration/apis/siem/overview_network.ts index ef7d82d2ea8d96..1b8354e0632f1c 100644 --- a/x-pack/test/api_integration/apis/siem/overview_network.ts +++ b/x-pack/test/api_integration/apis/siem/overview_network.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { overviewNetworkQuery } from '../../../../plugins/siem/public/containers/overview/overview_network/index.gql_query'; +import { overviewNetworkQuery } from '../../../../plugins/siem/public/overview/containers/overview_network/index.gql_query'; import { GetOverviewNetworkQuery } from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/test/api_integration/apis/siem/saved_objects/notes.ts b/x-pack/test/api_integration/apis/siem/saved_objects/notes.ts index 75670374b6f632..76c4afb08466e0 100644 --- a/x-pack/test/api_integration/apis/siem/saved_objects/notes.ts +++ b/x-pack/test/api_integration/apis/siem/saved_objects/notes.ts @@ -8,7 +8,7 @@ import expect from '@kbn/expect'; import gql from 'graphql-tag'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { persistTimelineNoteMutation } from '../../../../../plugins/siem/public/containers/timeline/notes/persist.gql_query'; +import { persistTimelineNoteMutation } from '../../../../../plugins/siem/public/timelines/containers/notes/persist.gql_query'; export default function({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/api_integration/apis/siem/saved_objects/pinned_events.ts b/x-pack/test/api_integration/apis/siem/saved_objects/pinned_events.ts index 39055e971d1185..4d24ea98821529 100644 --- a/x-pack/test/api_integration/apis/siem/saved_objects/pinned_events.ts +++ b/x-pack/test/api_integration/apis/siem/saved_objects/pinned_events.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { persistTimelinePinnedEventMutation } from '../../../../../plugins/siem/public/containers/timeline/pinned_event/persist.gql_query'; +import { persistTimelinePinnedEventMutation } from '../../../../../plugins/siem/public/timelines/containers/pinned_event/persist.gql_query'; export default function({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/api_integration/apis/siem/saved_objects/timeline.ts b/x-pack/test/api_integration/apis/siem/saved_objects/timeline.ts index 2d9f576ef37e95..b6f272b8d75408 100644 --- a/x-pack/test/api_integration/apis/siem/saved_objects/timeline.ts +++ b/x-pack/test/api_integration/apis/siem/saved_objects/timeline.ts @@ -15,9 +15,9 @@ import ApolloClient from 'apollo-client'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { deleteTimelineMutation } from '../../../../../plugins/siem/public/containers/timeline/delete/persist.gql_query'; -import { persistTimelineFavoriteMutation } from '../../../../../plugins/siem/public/containers/timeline/favorite/persist.gql_query'; -import { persistTimelineMutation } from '../../../../../plugins/siem/public/containers/timeline/persist.gql_query'; +import { deleteTimelineMutation } from '../../../../../plugins/siem/public/timelines/containers/delete/persist.gql_query'; +import { persistTimelineFavoriteMutation } from '../../../../../plugins/siem/public/timelines/containers/favorite/persist.gql_query'; +import { persistTimelineMutation } from '../../../../../plugins/siem/public/timelines/containers/persist.gql_query'; import { TimelineResult } from '../../../../../plugins/siem/public/graphql/types'; export default function({ getService }: FtrProviderContext) { diff --git a/x-pack/test/api_integration/apis/siem/sources.ts b/x-pack/test/api_integration/apis/siem/sources.ts index 2338d4ce45c8db..b17280703c9463 100644 --- a/x-pack/test/api_integration/apis/siem/sources.ts +++ b/x-pack/test/api_integration/apis/siem/sources.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { sourceQuery } from '../../../../plugins/siem/public/containers/source/index.gql_query'; +import { sourceQuery } from '../../../../plugins/siem/public/common/containers/source/index.gql_query'; import { SourceQuery } from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/test/api_integration/apis/siem/timeline.ts b/x-pack/test/api_integration/apis/siem/timeline.ts index de57b0c3f469fb..14cc957d98eb8f 100644 --- a/x-pack/test/api_integration/apis/siem/timeline.ts +++ b/x-pack/test/api_integration/apis/siem/timeline.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; -import { timelineQuery } from '../../../../plugins/siem/public/containers/timeline/index.gql_query'; +import { timelineQuery } from '../../../../plugins/siem/public/timelines/containers/index.gql_query'; import { Direction, GetTimelineQuery } from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/test/api_integration/apis/siem/timeline_details.ts b/x-pack/test/api_integration/apis/siem/timeline_details.ts index f88d5355f22c1c..920879cf9cf3ef 100644 --- a/x-pack/test/api_integration/apis/siem/timeline_details.ts +++ b/x-pack/test/api_integration/apis/siem/timeline_details.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { sortBy } from 'lodash'; -import { timelineDetailsQuery } from '../../../../plugins/siem/public/containers/timeline/details/index.gql_query'; +import { timelineDetailsQuery } from '../../../../plugins/siem/public/timelines/containers/details/index.gql_query'; import { DetailItem, GetTimelineDetailsQuery } from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/test/api_integration/apis/siem/tls.ts b/x-pack/test/api_integration/apis/siem/tls.ts index e4e8b5db3d7e38..8ee2ef43efe386 100644 --- a/x-pack/test/api_integration/apis/siem/tls.ts +++ b/x-pack/test/api_integration/apis/siem/tls.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { tlsQuery } from '../../../../plugins/siem/public/containers/tls/index.gql_query'; +import { tlsQuery } from '../../../../plugins/siem/public/network/containers/tls/index.gql_query'; import { Direction, TlsFields, diff --git a/x-pack/test/api_integration/apis/siem/uncommon_processes.ts b/x-pack/test/api_integration/apis/siem/uncommon_processes.ts index c9674e740f76d9..325f2f83e53df6 100644 --- a/x-pack/test/api_integration/apis/siem/uncommon_processes.ts +++ b/x-pack/test/api_integration/apis/siem/uncommon_processes.ts @@ -6,7 +6,7 @@ import expect from '@kbn/expect'; -import { uncommonProcessesQuery } from '../../../../plugins/siem/public/containers/uncommon_processes/index.gql_query'; +import { uncommonProcessesQuery } from '../../../../plugins/siem/public/hosts/containers/uncommon_processes/index.gql_query'; import { GetUncommonProcessesQuery } from '../../../../plugins/siem/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/test/api_integration/apis/siem/users.ts b/x-pack/test/api_integration/apis/siem/users.ts index c8ea1be7d3f11d..c6ac571e86eb33 100644 --- a/x-pack/test/api_integration/apis/siem/users.ts +++ b/x-pack/test/api_integration/apis/siem/users.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { usersQuery } from '../../../../plugins/siem/public/containers/users/index.gql_query'; +import { usersQuery } from '../../../../plugins/siem/public/network/containers/users/index.gql_query'; import { Direction, UsersFields, diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/delete_comment.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/delete_comment.ts new file mode 100644 index 00000000000000..afae04ae9cf5b5 --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/delete_comment.ts @@ -0,0 +1,76 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { CASES_URL } from '../../../../../../plugins/case/common/constants'; +import { postCaseReq, postCommentReq } from '../../../../common/lib/mock'; +import { deleteCases, deleteCasesUserActions, deleteComments } from '../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('delete_comment', () => { + afterEach(async () => { + await deleteCases(es); + await deleteComments(es); + await deleteCasesUserActions(es); + }); + + it('should delete a comment', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const { body: patchedCase } = await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentReq); + + const { body: comment } = await supertest + .delete(`${CASES_URL}/${postedCase.id}/comments/${patchedCase.comments[0].id}`) + .set('kbn-xsrf', 'true') + .send(); + + expect(comment).to.eql({}); + }); + + it('unhappy path - 404s when comment belongs to different case', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const { body: patchedCase } = await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentReq); + + const { body } = await supertest + .delete(`${CASES_URL}/fake-id/comments/${patchedCase.comments[0].id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(404); + expect(body.message).to.eql( + `This comment ${patchedCase.comments[0].id} does not exist in fake-id).` + ); + }); + + it('unhappy path - 404s when comment is not there', async () => { + await supertest + .delete(`${CASES_URL}/fake-id/comments/fake-id`) + .set('kbn-xsrf', 'true') + .send() + .expect(404); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/find_comments.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/find_comments.ts new file mode 100644 index 00000000000000..e5c44de90b5a1a --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/find_comments.ts @@ -0,0 +1,93 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { CASES_URL } from '../../../../../../plugins/case/common/constants'; +import { postCaseReq, postCommentReq } from '../../../../common/lib/mock'; +import { deleteCases, deleteCasesUserActions, deleteComments } from '../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('find_comments', () => { + afterEach(async () => { + await deleteCases(es); + await deleteComments(es); + await deleteCasesUserActions(es); + }); + + it('should find all case comment', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + // post 2 comments + await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentReq); + + const { body: patchedCase } = await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentReq); + + const { body: caseComments } = await supertest + .get(`${CASES_URL}/${postedCase.id}/comments/_find`) + .set('kbn-xsrf', 'true') + .send(); + + expect(caseComments.comments).to.eql(patchedCase.comments); + }); + + it('should filter case comments', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + // post 2 comments + await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentReq); + + const { body: patchedCase } = await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send({ comment: 'unique' }); + + const { body: caseComments } = await supertest + .get(`${CASES_URL}/${postedCase.id}/comments/_find?search=unique`) + .set('kbn-xsrf', 'true') + .send(); + + expect(caseComments.comments).to.eql([patchedCase.comments[1]]); + }); + + it('unhappy path - 400s when query is bad', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentReq); + await supertest + .get(`${CASES_URL}/${postedCase.id}/comments/_find?perPage=true`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/get_comment.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/get_comment.ts new file mode 100644 index 00000000000000..53da0ef1d2b166 --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/get_comment.ts @@ -0,0 +1,53 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { CASES_URL } from '../../../../../../plugins/case/common/constants'; +import { postCaseReq, postCommentReq } from '../../../../common/lib/mock'; +import { deleteCases, deleteCasesUserActions, deleteComments } from '../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('get_comment', () => { + afterEach(async () => { + await deleteCases(es); + await deleteComments(es); + await deleteCasesUserActions(es); + }); + + it('should get a comment', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq); + + const { body: patchedCase } = await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentReq); + + const { body: comment } = await supertest + .get(`${CASES_URL}/${postedCase.id}/comments/${patchedCase.comments[0].id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(comment).to.eql(patchedCase.comments[0]); + }); + it('unhappy path - 404s when comment is not there', async () => { + await supertest + .get(`${CASES_URL}/fake-id/comments/fake-comment`) + .set('kbn-xsrf', 'true') + .send() + .expect(404); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts new file mode 100644 index 00000000000000..73aeeb0fb989a0 --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/patch_comment.ts @@ -0,0 +1,123 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { CASES_URL } from '../../../../../../plugins/case/common/constants'; +import { defaultUser, postCaseReq, postCommentReq } from '../../../../common/lib/mock'; +import { deleteCases, deleteCasesUserActions, deleteComments } from '../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('patch_comment', () => { + afterEach(async () => { + await deleteCases(es); + await deleteComments(es); + await deleteCasesUserActions(es); + }); + + it('should patch a comment', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const { body: patchedCase } = await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentReq); + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; + const { body } = await supertest + .patch(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send({ + id: patchedCase.comments[0].id, + version: patchedCase.comments[0].version, + comment: newComment, + }); + expect(body.comments[0].comment).to.eql(newComment); + expect(body.updated_by).to.eql(defaultUser); + }); + + it('unhappy path - 404s when comment is not there', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq); + await supertest + .patch(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send({ + id: 'id', + version: 'version', + comment: 'comment', + }) + .expect(404); + }); + + it('unhappy path - 404s when case is not there', async () => { + await supertest + .patch(`${CASES_URL}/fake-id/comments`) + .set('kbn-xsrf', 'true') + .send({ + id: 'id', + version: 'version', + comment: 'comment', + }) + .expect(404); + }); + + it('unhappy path - 400s when patch body is bad', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const { body: patchedCase } = await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentReq); + await supertest + .patch(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send({ + id: patchedCase.comments[0].id, + version: patchedCase.comments[0].version, + comment: true, + }) + .expect(400); + }); + + it('unhappy path - 409s when conflict', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const { body: patchedCase } = await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentReq); + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; + await supertest + .patch(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send({ + id: patchedCase.comments[0].id, + version: 'version-mismatch', + comment: newComment, + }) + .expect(409); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts b/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts new file mode 100644 index 00000000000000..6e8353f8ea86a5 --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/cases/comments/post_comment.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { CASES_URL } from '../../../../../../plugins/case/common/constants'; +import { defaultUser, postCaseReq, postCommentReq } from '../../../../common/lib/mock'; +import { deleteCases, deleteCasesUserActions, deleteComments } from '../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('post_comment', () => { + afterEach(async () => { + await deleteCases(es); + await deleteComments(es); + await deleteCasesUserActions(es); + }); + + it('should post a comment', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const { body: patchedCase } = await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentReq); + expect(patchedCase.comments[0].comment).to.eql(postCommentReq.comment); + expect(patchedCase.updated_by).to.eql(defaultUser); + }); + + it('unhappy path - 400s when post body is bad', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send({ + bad: 'comment', + }) + .expect(400); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts new file mode 100644 index 00000000000000..aa2465e44c5c15 --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/cases/delete_cases.ts @@ -0,0 +1,76 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +import { CASES_URL } from '../../../../../plugins/case/common/constants'; +import { postCaseReq, postCommentReq } from '../../../common/lib/mock'; +import { deleteCases, deleteCasesUserActions, deleteComments } from '../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('delete_cases', () => { + afterEach(async () => { + await deleteCases(es); + await deleteComments(es); + await deleteCasesUserActions(es); + }); + + it('should delete a case', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq); + + const { body } = await supertest + .delete(`${CASES_URL}?ids=["${postedCase.id}"]`) + .set('kbn-xsrf', 'true') + .send() + .expect(204); + + expect(body).to.eql({}); + }); + + it(`should delete a case's comments when that case gets deleted`, async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq); + + const { body: patchedCase } = await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentReq); + + await supertest + .get(`${CASES_URL}/${postedCase.id}/comments/${patchedCase.comments[0].id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + await supertest + .delete(`${CASES_URL}?ids=["${postedCase.id}"]`) + .set('kbn-xsrf', 'true') + .send() + .expect(204); + await supertest + .get(`${CASES_URL}/${postedCase.id}/comments/${patchedCase.comments[0].id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(404); + }); + it('unhappy path - 404s when case is not there', async () => { + await supertest + .delete(`${CASES_URL}?ids=["fake-id"]`) + .set('kbn-xsrf', 'true') + .send() + .expect(404); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/basic/tests/cases/find_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/find_cases.ts new file mode 100644 index 00000000000000..04d195ea735091 --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/cases/find_cases.ts @@ -0,0 +1,158 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +import { CASES_URL } from '../../../../../plugins/case/common/constants'; +import { postCaseReq, postCommentReq, findCasesResp } from '../../../common/lib/mock'; +import { deleteCases, deleteComments, deleteCasesUserActions } from '../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + describe('find_cases', () => { + afterEach(async () => { + await deleteCases(es); + await deleteComments(es); + await deleteCasesUserActions(es); + }); + it('should return empty response', async () => { + const { body } = await supertest + .get(`${CASES_URL}/_find`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body).to.eql(findCasesResp); + }); + + it('should return cases', async () => { + const { body: a } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq); + const { body: b } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq); + const { body: c } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq); + const { body } = await supertest + .get(`${CASES_URL}/_find?sortOrder=asc`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body).to.eql({ + ...findCasesResp, + total: 3, + cases: [a, b, c], + count_open_cases: 3, + }); + }); + + it('filters by tags', async () => { + await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq); + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ ...postCaseReq, tags: ['unique'] }); + const { body } = await supertest + .get(`${CASES_URL}/_find?sortOrder=asc&tags=unique`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body).to.eql({ + ...findCasesResp, + total: 1, + cases: [postedCase], + count_open_cases: 1, + }); + }); + + it('correctly counts comments', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq); + + // post 2 comments + await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentReq); + const { body: patchedCase } = await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentReq); + const { body } = await supertest + .get(`${CASES_URL}/_find?sortOrder=asc`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body).to.eql({ + ...findCasesResp, + total: 1, + cases: [ + { + ...patchedCase, + comments: [], + totalComment: 2, + }, + ], + count_open_cases: 1, + }); + }); + + it('correctly counts open/closed', async () => { + await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq); + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq); + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: 'closed', + }, + ], + }) + .expect(200); + const { body } = await supertest + .get(`${CASES_URL}/_find?sortOrder=asc`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body.count_open_cases).to.eql(1); + expect(body.count_closed_cases).to.eql(1); + }); + it('unhappy path - 400s when bad query supplied', async () => { + await supertest + .get(`${CASES_URL}/_find?perPage=true`) + .set('kbn-xsrf', 'true') + .send() + .expect(400); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/basic/tests/cases/get_case.ts b/x-pack/test/case_api_integration/basic/tests/cases/get_case.ts new file mode 100644 index 00000000000000..9aad86126ceafa --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/cases/get_case.ts @@ -0,0 +1,52 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +import { CASES_URL } from '../../../../../plugins/case/common/constants'; +import { + postCaseReq, + postCaseResp, + removeServerGeneratedPropertiesFromCase, +} from '../../../common/lib/mock'; +import { deleteCases } from '../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('get_case', () => { + afterEach(async () => { + await deleteCases(es); + }); + + it('should return a case', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${postedCase.id}`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + const data = removeServerGeneratedPropertiesFromCase(body); + expect(data).to.eql(postCaseResp(postedCase.id)); + }); + it('unhappy path - 404s when case is not there', async () => { + await supertest + .get(`${CASES_URL}/fake-id`) + .set('kbn-xsrf', 'true') + .send() + .expect(404); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts b/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts new file mode 100644 index 00000000000000..caeaf46cbc953c --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/cases/patch_cases.ts @@ -0,0 +1,139 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +import { CASES_URL } from '../../../../../plugins/case/common/constants'; +import { + defaultUser, + postCaseReq, + postCaseResp, + removeServerGeneratedPropertiesFromCase, +} from '../../../common/lib/mock'; +import { deleteCases, deleteCasesUserActions } from '../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('patch_cases', () => { + afterEach(async () => { + await deleteCases(es); + await deleteCasesUserActions(es); + }); + + it('should patch a case', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + const { body: patchedCases } = await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: 'closed', + }, + ], + }) + .expect(200); + + const data = removeServerGeneratedPropertiesFromCase(patchedCases[0]); + expect(data).to.eql({ + ...postCaseResp(postedCase.id), + closed_by: defaultUser, + status: 'closed', + updated_by: defaultUser, + }); + }); + + it('unhappy path - 404s when case is not there', async () => { + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: 'not-real', + version: 'version', + status: 'closed', + }, + ], + }) + .expect(404); + }); + + it('unhappy path - 406s when excess data sent', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: postedCase.id, + version: postedCase.version, + badKey: 'closed', + }, + ], + }) + .expect(406); + }); + + it('unhappy path - 400s when bad data sent', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: true, + }, + ], + }) + .expect(400); + }); + + it('unhappy path - 409s when conflict', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + await supertest + .patch(`${CASES_URL}`) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: postedCase.id, + version: 'version', + status: 'closed', + }, + ], + }) + .expect(409); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/basic/tests/cases/post_case.ts b/x-pack/test/case_api_integration/basic/tests/cases/post_case.ts new file mode 100644 index 00000000000000..ab668c2c32725a --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/cases/post_case.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +import { CASES_URL } from '../../../../../plugins/case/common/constants'; +import { + postCaseReq, + postCaseResp, + removeServerGeneratedPropertiesFromCase, +} from '../../../common/lib/mock'; +import { deleteCases } from '../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('post_case', () => { + afterEach(async () => { + await deleteCases(es); + }); + + it('should post a case', async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const data = removeServerGeneratedPropertiesFromCase(postedCase); + expect(data).to.eql(postCaseResp(postedCase.id)); + }); + it('unhappy path - 400s when bad query supplied', async () => { + await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ ...postCaseReq, badKey: true }) + .expect(400); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts b/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts new file mode 100644 index 00000000000000..848b980dee769e --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/cases/push_case.ts @@ -0,0 +1,161 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +import { CASE_CONFIGURE_URL, CASES_URL } from '../../../../../plugins/case/common/constants'; +import { postCaseReq, defaultUser, postCommentReq } from '../../../common/lib/mock'; +import { + deleteCases, + deleteCasesUserActions, + deleteComments, + deleteConfiguration, + getConfiguration, +} from '../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('push_case', () => { + afterEach(async () => { + await deleteCases(es); + await deleteComments(es); + await deleteConfiguration(es); + await deleteCasesUserActions(es); + }); + + it('should push a case', async () => { + const { body: configure } = await supertest + .post(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send(getConfiguration()) + .expect(200); + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const { body } = await supertest + .post(`${CASES_URL}/${postedCase.id}/_push`) + .set('kbn-xsrf', 'true') + .send({ + connector_id: configure.connector_id, + connector_name: configure.connector_name, + external_id: 'external_id', + external_title: 'external_title', + external_url: 'external_url', + }) + .expect(200); + expect(body.connector_id).to.eql(configure.connector_id); + expect(body.external_service.pushed_by).to.eql(defaultUser); + }); + + it('pushes a comment appropriately', async () => { + const { body: configure } = await supertest + .post(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send(getConfiguration()) + .expect(200); + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + await supertest + .post(`${CASES_URL}/${postedCase.id}/_push`) + .set('kbn-xsrf', 'true') + .send({ + connector_id: configure.connector_id, + connector_name: configure.connector_name, + external_id: 'external_id', + external_title: 'external_title', + external_url: 'external_url', + }) + .expect(200); + + await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentReq); + + const { body } = await supertest + .post(`${CASES_URL}/${postedCase.id}/_push`) + .set('kbn-xsrf', 'true') + .send({ + connector_id: configure.connector_id, + connector_name: configure.connector_name, + external_id: 'external_id', + external_title: 'external_title', + external_url: 'external_url', + }) + .expect(200); + expect(body.comments[0].pushed_by).to.eql(defaultUser); + }); + it('unhappy path - 404s when case does not exist', async () => { + await supertest + .post(`${CASES_URL}/fake-id/_push`) + .set('kbn-xsrf', 'true') + .send({ + connector_id: 'connector_id', + connector_name: 'connector_name', + external_id: 'external_id', + external_title: 'external_title', + external_url: 'external_url', + }) + .expect(404); + }); + it('unhappy path - 400s when bad data supplied', async () => { + await supertest + .post(`${CASES_URL}/fake-id/_push`) + .set('kbn-xsrf', 'true') + .send({ + badKey: 'connector_id', + }) + .expect(400); + }); + it('unhappy path = 409s when case is closed', async () => { + const { body: configure } = await supertest + .post(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send(getConfiguration()) + .expect(200); + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: 'closed', + }, + ], + }) + .expect(200); + await supertest + .post(`${CASES_URL}/${postedCase.id}/_push`) + .set('kbn-xsrf', 'true') + .send({ + connector_id: configure.connector_id, + connector_name: configure.connector_name, + external_id: 'external_id', + external_title: 'external_title', + external_url: 'external_url', + }) + .expect(409); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/basic/tests/cases/reporters/get_reporters.ts b/x-pack/test/case_api_integration/basic/tests/cases/reporters/get_reporters.ts new file mode 100644 index 00000000000000..a781b928b2b683 --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/cases/reporters/get_reporters.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { CASES_URL, CASE_REPORTERS_URL } from '../../../../../../plugins/case/common/constants'; +import { defaultUser, postCaseReq } from '../../../../common/lib/mock'; +import { deleteCases } from '../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('get_reporters', () => { + afterEach(async () => { + await deleteCases(es); + }); + + it('should return reporters', async () => { + await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq) + .expect(200); + + const { body } = await supertest + .get(CASE_REPORTERS_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body).to.eql([defaultUser]); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/basic/tests/cases/status/get_status.ts b/x-pack/test/case_api_integration/basic/tests/cases/status/get_status.ts new file mode 100644 index 00000000000000..6552f588bdc198 --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/cases/status/get_status.ts @@ -0,0 +1,59 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { CASES_URL, CASE_STATUS_URL } from '../../../../../../plugins/case/common/constants'; +import { postCaseReq } from '../../../../common/lib/mock'; +import { deleteCases } from '../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('get_status', () => { + afterEach(async () => { + await deleteCases(es); + }); + + it('should return case statuses', async () => { + await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq); + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq); + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: 'closed', + }, + ], + }) + .expect(200); + + const { body } = await supertest + .get(CASE_STATUS_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body).to.eql({ + count_open_cases: 1, + count_closed_cases: 1, + }); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/basic/tests/cases/tags/get_tags.ts b/x-pack/test/case_api_integration/basic/tests/cases/tags/get_tags.ts new file mode 100644 index 00000000000000..9b769e3c5eef45 --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/cases/tags/get_tags.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { CASES_URL, CASE_TAGS_URL } from '../../../../../../plugins/case/common/constants'; +import { postCaseReq } from '../../../../common/lib/mock'; +import { deleteCases } from '../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('get_tags', () => { + afterEach(async () => { + await deleteCases(es); + }); + + it('should return case tags', async () => { + await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq); + await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ ...postCaseReq, tags: ['unique'] }); + + const { body } = await supertest + .get(CASE_TAGS_URL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + + expect(body).to.eql(['defacement', 'unique']); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts new file mode 100644 index 00000000000000..6bbd43eef1439d --- /dev/null +++ b/x-pack/test/case_api_integration/basic/tests/cases/user_actions/get_all_user_actions.ts @@ -0,0 +1,307 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { CASE_CONFIGURE_URL, CASES_URL } from '../../../../../../plugins/case/common/constants'; +import { defaultUser, postCaseReq, postCommentReq } from '../../../../common/lib/mock'; +import { + deleteCases, + deleteCasesUserActions, + deleteComments, + deleteConfiguration, + getConfiguration, +} from '../../../../common/lib/utils'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext): void => { + const supertest = getService('supertest'); + const es = getService('es'); + + describe('get_all_user_actions', () => { + afterEach(async () => { + await deleteCases(es); + await deleteComments(es); + await deleteConfiguration(es); + await deleteCasesUserActions(es); + }); + + it(`on new case, user action: 'create' should be called with actionFields: ['description', 'status', 'tags', 'title']`, async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq); + + const { body } = await supertest + .get(`${CASES_URL}/${postedCase.id}/user_actions`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + expect(body.length).to.eql(1); + + expect(body[0].action_field).to.eql(['description', 'status', 'tags', 'title']); + expect(body[0].action).to.eql('create'); + expect(body[0].old_value).to.eql(null); + expect(body[0].new_value).to.eql(JSON.stringify(postCaseReq)); + }); + + it(`on close case, user action: 'update' should be called with actionFields: ['status']`, async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq); + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: postedCase.id, + version: postedCase.version, + status: 'closed', + }, + ], + }) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${postedCase.id}/user_actions`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + expect(body.length).to.eql(2); + expect(body[1].action_field).to.eql(['status']); + expect(body[1].action).to.eql('update'); + expect(body[1].old_value).to.eql('open'); + expect(body[1].new_value).to.eql('closed'); + }); + + it(`on update case connector, user action: 'update' should be called with actionFields: ['connector_id']`, async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq); + const newConnectorId = '12345'; + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: postedCase.id, + version: postedCase.version, + connector_id: newConnectorId, + }, + ], + }) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${postedCase.id}/user_actions`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + expect(body.length).to.eql(2); + expect(body[1].action_field).to.eql(['connector_id']); + expect(body[1].action).to.eql('update'); + expect(body[1].old_value).to.eql('none'); + expect(body[1].new_value).to.eql(newConnectorId); + }); + + it(`on update tags, user action: 'add' and 'delete' should be called with actionFields: ['tags']`, async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq); + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: postedCase.id, + version: postedCase.version, + tags: ['cool', 'neat'], + }, + ], + }) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${postedCase.id}/user_actions`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + expect(body.length).to.eql(3); + expect(body[1].action_field).to.eql(['tags']); + expect(body[1].action).to.eql('add'); + expect(body[1].old_value).to.eql(null); + expect(body[1].new_value).to.eql('cool, neat'); + expect(body[2].action_field).to.eql(['tags']); + expect(body[2].action).to.eql('delete'); + expect(body[2].old_value).to.eql(null); + expect(body[2].new_value).to.eql('defacement'); + }); + + it(`on update title, user action: 'update' should be called with actionFields: ['title']`, async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq); + const newTitle = 'Such a great title'; + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: postedCase.id, + version: postedCase.version, + title: newTitle, + }, + ], + }) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${postedCase.id}/user_actions`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + expect(body.length).to.eql(2); + expect(body[1].action_field).to.eql(['title']); + expect(body[1].action).to.eql('update'); + expect(body[1].old_value).to.eql(postCaseReq.title); + expect(body[1].new_value).to.eql(newTitle); + }); + + it(`on update description, user action: 'update' should be called with actionFields: ['description']`, async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq); + const newDesc = 'Such a great description'; + await supertest + .patch(CASES_URL) + .set('kbn-xsrf', 'true') + .send({ + cases: [ + { + id: postedCase.id, + version: postedCase.version, + description: newDesc, + }, + ], + }) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${postedCase.id}/user_actions`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + expect(body.length).to.eql(2); + expect(body[1].action_field).to.eql(['description']); + expect(body[1].action).to.eql('update'); + expect(body[1].old_value).to.eql(postCaseReq.description); + expect(body[1].new_value).to.eql(newDesc); + }); + + it(`on new comment, user action: 'create' should be called with actionFields: ['comments']`, async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq); + await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentReq); + + const { body } = await supertest + .get(`${CASES_URL}/${postedCase.id}/user_actions`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + expect(body.length).to.eql(2); + + expect(body[1].action_field).to.eql(['comment']); + expect(body[1].action).to.eql('create'); + expect(body[1].old_value).to.eql(null); + expect(body[1].new_value).to.eql(postCommentReq.comment); + }); + + it(`on update comment, user action: 'update' should be called with actionFields: ['comments']`, async () => { + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq); + const { body: patchedCase } = await supertest + .post(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send(postCommentReq); + const newComment = 'Well I decided to update my comment. So what? Deal with it.'; + await supertest + .patch(`${CASES_URL}/${postedCase.id}/comments`) + .set('kbn-xsrf', 'true') + .send({ + id: patchedCase.comments[0].id, + version: patchedCase.comments[0].version, + comment: newComment, + }); + + const { body } = await supertest + .get(`${CASES_URL}/${postedCase.id}/user_actions`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + expect(body.length).to.eql(3); + + expect(body[2].action_field).to.eql(['comment']); + expect(body[2].action).to.eql('update'); + expect(body[2].old_value).to.eql(postCommentReq.comment); + expect(body[2].new_value).to.eql(newComment); + }); + + it(`on new push to service, user action: 'push-to-service' should be called with actionFields: ['pushed']`, async () => { + const { body: configure } = await supertest + .post(CASE_CONFIGURE_URL) + .set('kbn-xsrf', 'true') + .send(getConfiguration()) + .expect(200); + const { body: postedCase } = await supertest + .post(CASES_URL) + .set('kbn-xsrf', 'true') + .send(postCaseReq); + + await supertest + .post(`${CASES_URL}/${postedCase.id}/_push`) + .set('kbn-xsrf', 'true') + .send({ + connector_id: configure.connector_id, + connector_name: configure.connector_name, + external_id: 'external_id', + external_title: 'external_title', + external_url: 'external_url', + }) + .expect(200); + + const { body } = await supertest + .get(`${CASES_URL}/${postedCase.id}/user_actions`) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + expect(body.length).to.eql(2); + + expect(body[1].action_field).to.eql(['pushed']); + expect(body[1].action).to.eql('push-to-service'); + expect(body[1].old_value).to.eql(null); + const newValue = JSON.parse(body[1].new_value); + expect(newValue.connector_id).to.eql(configure.connector_id); + expect(newValue.pushed_by).to.eql(defaultUser); + }); + }); +}; diff --git a/x-pack/test/case_api_integration/basic/tests/index.ts b/x-pack/test/case_api_integration/basic/tests/index.ts index efd5369c019d82..b152a97a28616b 100644 --- a/x-pack/test/case_api_integration/basic/tests/index.ts +++ b/x-pack/test/case_api_integration/basic/tests/index.ts @@ -12,9 +12,24 @@ export default ({ loadTestFile }: FtrProviderContext): void => { // Fastest ciGroup for the moment. this.tags('ciGroup2'); + loadTestFile(require.resolve('./cases/comments/delete_comment')); + loadTestFile(require.resolve('./cases/comments/find_comments')); + loadTestFile(require.resolve('./cases/comments/get_comment')); + loadTestFile(require.resolve('./cases/comments/patch_comment')); + loadTestFile(require.resolve('./cases/comments/post_comment')); + loadTestFile(require.resolve('./cases/delete_cases')); + loadTestFile(require.resolve('./cases/find_cases')); + loadTestFile(require.resolve('./cases/get_case')); + loadTestFile(require.resolve('./cases/patch_cases')); + loadTestFile(require.resolve('./cases/post_case')); + loadTestFile(require.resolve('./cases/push_case')); + loadTestFile(require.resolve('./cases/reporters/get_reporters')); + loadTestFile(require.resolve('./cases/status/get_status')); + loadTestFile(require.resolve('./cases/tags/get_tags')); + loadTestFile(require.resolve('./cases/user_actions/get_all_user_actions')); loadTestFile(require.resolve('./configure/get_configure')); - loadTestFile(require.resolve('./configure/post_configure')); - loadTestFile(require.resolve('./configure/patch_configure')); loadTestFile(require.resolve('./configure/get_connectors')); + loadTestFile(require.resolve('./configure/patch_configure')); + loadTestFile(require.resolve('./configure/post_configure')); }); }; diff --git a/x-pack/test/case_api_integration/common/lib/mock.ts b/x-pack/test/case_api_integration/common/lib/mock.ts new file mode 100644 index 00000000000000..728eaf88617e9c --- /dev/null +++ b/x-pack/test/case_api_integration/common/lib/mock.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + CasePostRequest, + CaseResponse, + CasesFindResponse, +} from '../../../../plugins/case/common/api'; +export const defaultUser = { email: null, full_name: null, username: 'elastic' }; +export const postCaseReq: CasePostRequest = { + description: 'This is a brand new case of a bad meanie defacing data', + title: 'Super Bad Security Issue', + tags: ['defacement'], +}; + +export const postCommentReq: { comment: string } = { + comment: 'This is a cool comment', +}; + +export const postCaseResp = (id: string): Partial => ({ + ...postCaseReq, + id, + comments: [], + totalComment: 0, + connector_id: 'none', + closed_by: null, + created_by: defaultUser, + external_service: null, + status: 'open', + updated_by: null, +}); + +export const removeServerGeneratedPropertiesFromCase = ( + config: Partial +): Partial => { + const { closed_at, created_at, updated_at, version, ...rest } = config; + return rest; +}; + +export const findCasesResp: CasesFindResponse = { + page: 1, + per_page: 20, + total: 0, + cases: [], + count_open_cases: 0, + count_closed_cases: 0, +}; diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts index df768ff09b3689..4b1dc6ffa58915 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -30,6 +30,36 @@ export const removeServerGeneratedPropertiesFromConfigure = ( return rest; }; +export const deleteCasesUserActions = async (es: Client): Promise => { + await es.deleteByQuery({ + index: '.kibana', + q: 'type:cases-user-actions', + wait_for_completion: true, + refresh: true, + body: {}, + }); +}; + +export const deleteCases = async (es: Client): Promise => { + await es.deleteByQuery({ + index: '.kibana', + q: 'type:cases', + wait_for_completion: true, + refresh: true, + body: {}, + }); +}; + +export const deleteComments = async (es: Client): Promise => { + await es.deleteByQuery({ + index: '.kibana', + q: 'type:cases-comments', + wait_for_completion: true, + refresh: true, + body: {}, + }); +}; + export const deleteConfiguration = async (es: Client): Promise => { await es.deleteByQuery({ index: '.kibana', @@ -39,34 +69,3 @@ export const deleteConfiguration = async (es: Client): Promise => { body: {}, }); }; - -export const getConnector = () => ({ - name: 'ServiceNow Connector', - actionTypeId: '.servicenow', - secrets: { - username: 'admin', - password: 'admin', - }, - config: { - apiUrl: 'localhost', - casesConfiguration: { - mapping: [ - { - source: 'title', - target: 'short_description', - actionType: 'overwrite', - }, - { - source: 'description', - target: 'description', - actionType: 'overwrite', - }, - { - source: 'comments', - target: 'comments', - actionType: 'append', - }, - ], - }, - }, -}); diff --git a/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts b/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts new file mode 100644 index 00000000000000..b66f9d2baeb365 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { REPO_ROOT } from '@kbn/dev-utils'; +import expect from '@kbn/expect'; +import fs from 'fs'; +import path from 'path'; +import * as Rx from 'rxjs'; +import { filter, first, map, timeout } from 'rxjs/operators'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +const csvPath = path.resolve(REPO_ROOT, 'target/functional-tests/downloads/Ecommerce Data.csv'); + +export default function({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const browser = getService('browser'); + const dashboardPanelActions = getService('dashboardPanelActions'); + const log = getService('log'); + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['reporting', 'common', 'dashboard']); + + describe('Reporting Download CSV', () => { + before('initialize tests', async () => { + log.debug('ReportingPage:initTests'); + await esArchiver.loadIfNeeded('reporting/ecommerce'); + await esArchiver.loadIfNeeded('reporting/ecommerce_kibana'); + await browser.setWindowSize(1600, 850); + }); + + after('clean up archives and previous file download', async () => { + await esArchiver.unload('reporting/ecommerce'); + await esArchiver.unload('reporting/ecommerce_kibana'); + try { + fs.unlinkSync(csvPath); + } catch (e) { + // nothing to worry + } + }); + + it('Downloads a CSV export of a saved search panel', async function() { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard('Ecom Dashboard'); + const savedSearchPanel = await testSubjects.find('embeddablePanelHeading-EcommerceData'); + await dashboardPanelActions.toggleContextMenu(savedSearchPanel); + + await testSubjects.existOrFail('embeddablePanelAction-downloadCsvReport'); // wait for the full panel to display or else the test runner could click the wrong option! + await testSubjects.click('embeddablePanelAction-downloadCsvReport'); + await testSubjects.existOrFail('csvDownloadStarted'); // validate toast panel + + // check every 100ms for the file to exist in the download dir + // just wait up to 5 seconds + const success$ = Rx.interval(100).pipe( + map(() => fs.existsSync(csvPath)), + filter(value => value === true), + first(), + timeout(5000) + ); + + const fileExists = await success$.toPromise(); + expect(fileExists).to.be(true); + + // no need to validate download contents, API Integration tests do that some different variations + }); + }); +} diff --git a/x-pack/test/functional/apps/dashboard/reporting/index.ts b/x-pack/test/functional/apps/dashboard/reporting/index.ts index 796e15b4e270f4..1dc2a958e3dd51 100644 --- a/x-pack/test/functional/apps/dashboard/reporting/index.ts +++ b/x-pack/test/functional/apps/dashboard/reporting/index.ts @@ -3,125 +3,11 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -import expect from '@kbn/expect'; -import fs from 'fs'; -import path from 'path'; -import { promisify } from 'util'; import { FtrProviderContext } from '../../../ftr_provider_context'; -import { checkIfPngsMatch } from './lib/compare_pngs'; - -const writeFileAsync = promisify(fs.writeFile); -const mkdirAsync = promisify(fs.mkdir); - -const REPORTS_FOLDER = path.resolve(__dirname, 'reports'); - -export default function({ getService, getPageObjects }: FtrProviderContext) { - const esArchiver = getService('esArchiver'); - const browser = getService('browser'); - const log = getService('log'); - const config = getService('config'); - const PageObjects = getPageObjects(['reporting', 'common', 'dashboard']); - - describe('Reporting', () => { - before('initialize tests', async () => { - log.debug('ReportingPage:initTests'); - await esArchiver.loadIfNeeded('reporting/ecommerce'); - await esArchiver.loadIfNeeded('reporting/ecommerce_kibana'); - await browser.setWindowSize(1600, 850); - }); - after('clean up archives', async () => { - await esArchiver.unload('reporting/ecommerce'); - await esArchiver.unload('reporting/ecommerce_kibana'); - }); - - describe('Print PDF button', () => { - it('is not available if new', async () => { - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.clickNewDashboard(); - await PageObjects.reporting.openPdfReportingPanel(); - expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be('true'); - }); - - it('becomes available when saved', async () => { - await PageObjects.dashboard.saveDashboard('My PDF Dashboard'); - await PageObjects.reporting.openPdfReportingPanel(); - expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null); - }); - }); - - describe('Print Layout', () => { - it('downloads a PDF file', async function() { - // Generating and then comparing reports can take longer than the default 60s timeout because the comparePngs - // function is taking about 15 seconds per comparison in jenkins. - this.timeout(300000); - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.loadSavedDashboard('Ecom Dashboard'); - await PageObjects.reporting.openPdfReportingPanel(); - await PageObjects.reporting.checkUsePrintLayout(); - await PageObjects.reporting.clickGenerateReportButton(); - - const url = await PageObjects.reporting.getReportURL(60000); - const res = await PageObjects.reporting.getResponse(url); - - expect(res.statusCode).to.equal(200); - expect(res.headers['content-type']).to.equal('application/pdf'); - }); - }); - - describe('Print PNG button', () => { - it('is not available if new', async () => { - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.clickNewDashboard(); - await PageObjects.reporting.openPngReportingPanel(); - expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be('true'); - }); - - it('becomes available when saved', async () => { - await PageObjects.dashboard.saveDashboard('My PNG Dash'); - await PageObjects.reporting.openPngReportingPanel(); - expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null); - }); - }); - - describe('Preserve Layout', () => { - it('matches baseline report', async function() { - const writeSessionReport = async (name: string, rawPdf: Buffer, reportExt: string) => { - const sessionDirectory = path.resolve(REPORTS_FOLDER, 'session'); - await mkdirAsync(sessionDirectory, { recursive: true }); - const sessionReportPath = path.resolve(sessionDirectory, `${name}.${reportExt}`); - await writeFileAsync(sessionReportPath, rawPdf); - return sessionReportPath; - }; - const getBaselineReportPath = (fileName: string, reportExt: string) => { - const baselineFolder = path.resolve(REPORTS_FOLDER, 'baseline'); - const fullPath = path.resolve(baselineFolder, `${fileName}.${reportExt}`); - log.debug(`getBaselineReportPath (${fullPath})`); - return fullPath; - }; - - this.timeout(300000); - - await PageObjects.common.navigateToApp('dashboard'); - await PageObjects.dashboard.loadSavedDashboard('Ecom Dashboard'); - await PageObjects.reporting.openPngReportingPanel(); - await PageObjects.reporting.forceSharedItemsContainerSize({ width: 1405 }); - await PageObjects.reporting.clickGenerateReportButton(); - await PageObjects.reporting.removeForceSharedItemsContainerSize(); - - const url = await PageObjects.reporting.getReportURL(60000); - const reportData = await PageObjects.reporting.getRawPdfReportData(url); - const reportFileName = 'dashboard_preserve_layout'; - const sessionReportPath = await writeSessionReport(reportFileName, reportData, 'png'); - const percentSimilar = await checkIfPngsMatch( - sessionReportPath, - getBaselineReportPath(reportFileName, 'png'), - config.get('screenshots.directory'), - log - ); - expect(percentSimilar).to.be.lessThan(0.1); - }); - }); +export default function({ loadTestFile }: FtrProviderContext) { + describe('Reporting', function() { + loadTestFile(require.resolve('./screenshots')); + loadTestFile(require.resolve('./download_csv')); }); } diff --git a/x-pack/test/functional/apps/dashboard/reporting/screenshots.ts b/x-pack/test/functional/apps/dashboard/reporting/screenshots.ts new file mode 100644 index 00000000000000..2cc1686b8c7cae --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/reporting/screenshots.ts @@ -0,0 +1,127 @@ +/* + * 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 expect from '@kbn/expect'; +import fs from 'fs'; +import path from 'path'; +import { promisify } from 'util'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { checkIfPngsMatch } from './lib/compare_pngs'; + +const writeFileAsync = promisify(fs.writeFile); +const mkdirAsync = promisify(fs.mkdir); + +const REPORTS_FOLDER = path.resolve(__dirname, 'reports'); + +export default function({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const browser = getService('browser'); + const log = getService('log'); + const config = getService('config'); + const PageObjects = getPageObjects(['reporting', 'common', 'dashboard']); + + describe('Screenshots', () => { + before('initialize tests', async () => { + log.debug('ReportingPage:initTests'); + await esArchiver.loadIfNeeded('reporting/ecommerce'); + await esArchiver.loadIfNeeded('reporting/ecommerce_kibana'); + await browser.setWindowSize(1600, 850); + }); + after('clean up archives', async () => { + await esArchiver.unload('reporting/ecommerce'); + await esArchiver.unload('reporting/ecommerce_kibana'); + }); + + describe('Print PDF button', () => { + it('is not available if new', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await PageObjects.reporting.openPdfReportingPanel(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be('true'); + }); + + it('becomes available when saved', async () => { + await PageObjects.dashboard.saveDashboard('My PDF Dashboard'); + await PageObjects.reporting.openPdfReportingPanel(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null); + }); + }); + + describe('Print Layout', () => { + it('downloads a PDF file', async function() { + // Generating and then comparing reports can take longer than the default 60s timeout because the comparePngs + // function is taking about 15 seconds per comparison in jenkins. + this.timeout(300000); + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard('Ecom Dashboard'); + await PageObjects.reporting.openPdfReportingPanel(); + await PageObjects.reporting.checkUsePrintLayout(); + await PageObjects.reporting.clickGenerateReportButton(); + + const url = await PageObjects.reporting.getReportURL(60000); + const res = await PageObjects.reporting.getResponse(url); + + expect(res.statusCode).to.equal(200); + expect(res.headers['content-type']).to.equal('application/pdf'); + }); + }); + + describe('Print PNG button', () => { + it('is not available if new', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.clickNewDashboard(); + await PageObjects.reporting.openPngReportingPanel(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be('true'); + }); + + it('becomes available when saved', async () => { + await PageObjects.dashboard.saveDashboard('My PNG Dash'); + await PageObjects.reporting.openPngReportingPanel(); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null); + }); + }); + + describe('Preserve Layout', () => { + it('matches baseline report', async function() { + const writeSessionReport = async (name: string, rawPdf: Buffer, reportExt: string) => { + const sessionDirectory = path.resolve(REPORTS_FOLDER, 'session'); + await mkdirAsync(sessionDirectory, { recursive: true }); + const sessionReportPath = path.resolve(sessionDirectory, `${name}.${reportExt}`); + await writeFileAsync(sessionReportPath, rawPdf); + return sessionReportPath; + }; + const getBaselineReportPath = (fileName: string, reportExt: string) => { + const baselineFolder = path.resolve(REPORTS_FOLDER, 'baseline'); + const fullPath = path.resolve(baselineFolder, `${fileName}.${reportExt}`); + log.debug(`getBaselineReportPath (${fullPath})`); + return fullPath; + }; + + this.timeout(300000); + + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard('Ecom Dashboard'); + await PageObjects.reporting.openPngReportingPanel(); + await PageObjects.reporting.forceSharedItemsContainerSize({ width: 1405 }); + await PageObjects.reporting.clickGenerateReportButton(); + await PageObjects.reporting.removeForceSharedItemsContainerSize(); + + const url = await PageObjects.reporting.getReportURL(60000); + const reportData = await PageObjects.reporting.getRawPdfReportData(url); + const reportFileName = 'dashboard_preserve_layout'; + const sessionReportPath = await writeSessionReport(reportFileName, reportData, 'png'); + const percentSimilar = await checkIfPngsMatch( + sessionReportPath, + getBaselineReportPath(reportFileName, 'png'), + config.get('screenshots.directory'), + log + ); + + expect(percentSimilar).to.be.lessThan(0.1); + }); + }); + }); +} diff --git a/x-pack/test/functional/page_objects/reporting_page.ts b/x-pack/test/functional/page_objects/reporting_page.ts index 2c20519a8d2140..320171f8c89cdf 100644 --- a/x-pack/test/functional/page_objects/reporting_page.ts +++ b/x-pack/test/functional/page_objects/reporting_page.ts @@ -9,10 +9,11 @@ import { FtrProviderContext } from 'test/functional/ftr_provider_context'; import { parse } from 'url'; export function ReportingPageProvider({ getService, getPageObjects }: FtrProviderContext) { - const retry = getService('retry'); + const browser = getService('browser'); const log = getService('log'); + const retry = getService('retry'); const testSubjects = getService('testSubjects'); - const browser = getService('browser'); + const PageObjects = getPageObjects(['common', 'security' as any, 'share', 'timePicker']); // FIXME: Security PageObject is not Typescript class ReportingPage { diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/index.js b/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/index.js deleted file mode 100644 index b8dbdeb07d7fe2..00000000000000 --- a/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/index.js +++ /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. - */ - -import mappings from './mappings.json'; - -export default function(kibana) { - return new kibana.Plugin({ - require: ['kibana', 'elasticsearch', 'xpack_main'], - name: 'namespace_agnostic_type_plugin', - uiExports: { - savedObjectsManagement: { - globaltype: { - isImportableAndExportable: true, - }, - }, - savedObjectSchemas: { - globaltype: { - isNamespaceAgnostic: true, - }, - }, - mappings, - }, - - config() {}, - - init(server) { - server.plugins.xpack_main.registerFeature({ - id: 'namespace_agnostic_type_plugin', - name: 'namespace_agnostic_type_plugin', - icon: 'upArrow', - navLinkId: 'namespace_agnostic_type_plugin', - app: [], - privileges: { - all: { - savedObject: { - all: ['globaltype'], - read: [], - }, - ui: [], - }, - read: { - savedObject: { - all: [], - read: ['globaltype'], - }, - ui: [], - }, - }, - }); - }, - }); -} diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/kibana.json b/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/kibana.json new file mode 100644 index 00000000000000..ec3077999d8bdc --- /dev/null +++ b/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "namespace_agnostic_type_plugin", + "version": "1.0.0", + "kibanaVersion": "kibana", + "requiredPlugins": ["features"], + "server": true, + "ui": false +} diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/mappings.json b/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/mappings.json deleted file mode 100644 index 64d309b4209a20..00000000000000 --- a/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/mappings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "globaltype": { - "properties": { - "title": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 2048 - } - } - } - } - } -} diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/package.json b/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/package.json deleted file mode 100644 index 1a0afbc6bfcb30..00000000000000 --- a/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "namespace_agnostic_type_plugin", - "version": "0.0.0", - "kibana": { - "version": "kibana" - } -} diff --git a/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/server/index.ts b/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/server/index.ts new file mode 100644 index 00000000000000..d8940c96e676a0 --- /dev/null +++ b/x-pack/test/saved_object_api_integration/common/fixtures/namespace_agnostic_type_plugin/server/index.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { CoreSetup, Plugin } from 'src/core/server'; +import { PluginSetupContract as FeaturesPluginSetupContract } from '../../../../../../plugins/features/server'; + +export const plugin = () => new NamespaceAgnosticTypePlugin(); + +interface SetupDeps { + features: FeaturesPluginSetupContract; +} + +class NamespaceAgnosticTypePlugin implements Plugin { + setup(core: CoreSetup, plugins: SetupDeps) { + core.savedObjects.registerType({ + name: 'globaltype', + hidden: false, + namespaceType: 'agnostic', + management: { + importableAndExportable: true, + }, + mappings: { + properties: { + title: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + ignore_above: 2048, + }, + }, + }, + }, + }, + }); + + plugins.features.registerFeature({ + id: 'namespace_agnostic_type_plugin', + name: 'namespace_agnostic_type_plugin', + icon: 'upArrow', + navLinkId: 'namespace_agnostic_type_plugin', + app: [], + privileges: { + all: { + savedObject: { + all: ['globaltype'], + read: [], + }, + ui: [], + }, + read: { + savedObject: { + all: [], + read: ['globaltype'], + }, + ui: [], + }, + }, + }); + } + start() {} +} diff --git a/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/index.js b/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/index.js deleted file mode 100644 index 89ae0125614b67..00000000000000 --- a/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/index.js +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export default function(kibana) { - return new kibana.Plugin({ - require: ['kibana', 'elasticsearch', 'xpack_main'], - name: 'foo', - uiExports: { - app: { - title: 'Foo', - order: 1000, - euiIconType: 'uiArray', - description: 'Foo app', - main: 'plugins/foo_plugin/app', - }, - }, - - init(server) { - server.plugins.xpack_main.registerFeature({ - id: 'foo', - name: 'Foo', - icon: 'upArrow', - navLinkId: 'foo_plugin', - app: ['kibana'], - catalogue: ['foo'], - privileges: { - all: { - app: ['kibana'], - catalogue: ['foo'], - savedObject: { - all: ['foo'], - read: ['index-pattern'], - }, - ui: ['create', 'edit', 'delete', 'show'], - }, - read: { - app: ['kibana'], - catalogue: ['foo'], - savedObject: { - all: [], - read: ['foo', 'index-pattern'], - }, - ui: ['show'], - }, - }, - }); - }, - }); -} diff --git a/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/kibana.json b/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/kibana.json new file mode 100644 index 00000000000000..cec1640fbb047b --- /dev/null +++ b/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/kibana.json @@ -0,0 +1,8 @@ +{ + "id": "foo_plugin", + "version": "1.0.0", + "kibanaVersion": "kibana", + "requiredPlugins": ["features"], + "server": true, + "ui": true +} diff --git a/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/package.json b/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/package.json deleted file mode 100644 index 45228b47c0a318..00000000000000 --- a/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "foo_plugin", - "version": "0.0.0", - "kibana": { - "version": "kibana" - } -} diff --git a/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/public/app.js b/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/public/app.js deleted file mode 100644 index 41bc2aa2588073..00000000000000 --- a/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/public/app.js +++ /dev/null @@ -1,5 +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. - */ diff --git a/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/public/index.ts b/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/public/index.ts new file mode 100644 index 00000000000000..357cbba3349284 --- /dev/null +++ b/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/public/index.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 { CoreSetup, Plugin } from 'src/core/public'; +export const plugin = () => new FooPlugin(); + +class FooPlugin implements Plugin { + setup(core: CoreSetup) { + core.application.register({ + id: 'foo', + title: 'Foo app', + euiIconType: 'uiArray', + mount() { + return () => null; + }, + }); + } + start() {} +} diff --git a/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/server/index.ts b/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/server/index.ts new file mode 100644 index 00000000000000..bff794801119af --- /dev/null +++ b/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/server/index.ts @@ -0,0 +1,47 @@ +/* + * 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 { CoreSetup, Plugin } from 'src/core/server'; +import { PluginSetupContract as FeaturesPluginSetupContract } from '../../../../../../../plugins/features/server'; + +export const plugin = () => new FooPlugin(); + +interface SetupDeps { + features: FeaturesPluginSetupContract; +} + +class FooPlugin implements Plugin { + setup(core: CoreSetup, plugins: SetupDeps) { + plugins.features.registerFeature({ + id: 'foo', + name: 'Foo', + icon: 'upArrow', + navLinkId: 'foo_plugin', + app: ['kibana'], + catalogue: ['foo'], + privileges: { + all: { + app: ['kibana'], + catalogue: ['foo'], + savedObject: { + all: ['foo'], + read: ['index-pattern'], + }, + ui: ['create', 'edit', 'delete', 'show'], + }, + read: { + app: ['kibana'], + catalogue: ['foo'], + savedObject: { + all: [], + read: ['foo', 'index-pattern'], + }, + ui: ['show'], + }, + }, + }); + } + start() {} +}