diff --git a/.buildkite/ftr_configs.yml b/.buildkite/ftr_configs.yml index 3870c67506b42c..3d6e985018deef 100644 --- a/.buildkite/ftr_configs.yml +++ b/.buildkite/ftr_configs.yml @@ -146,6 +146,7 @@ enabled: - x-pack/test/functional/apps/cross_cluster_replication/config.ts - x-pack/test/functional/apps/dashboard/group1/config.ts - x-pack/test/functional/apps/dashboard/group2/config.ts + - x-pack/test/functional/apps/dashboard/group3/config.ts - x-pack/test/functional/apps/data_views/config.ts - x-pack/test/functional/apps/dev_tools/config.ts - x-pack/test/functional/apps/discover/config.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 69245de6eb8102..3cdd9a3c74ecdf 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -141,6 +141,8 @@ /x-pack/test/functional/es_archives/uptime @elastic/uptime /x-pack/test/functional/services/uptime @elastic/uptime /x-pack/test/api_integration/apis/uptime @elastic/uptime +/x-pack/plugins/observability/public/components/shared/exploratory_view @elastic/uptime + # Client Side Monitoring / Uptime (lives in APM directories but owned by Uptime) /x-pack/plugins/apm/public/application/uxApp.tsx @elastic/uptime diff --git a/dev_docs/contributing/how_we_use_github.mdx b/dev_docs/contributing/how_we_use_github.mdx index a45cad425a0e59..8c76c48003c3c3 100644 --- a/dev_docs/contributing/how_we_use_github.mdx +++ b/dev_docs/contributing/how_we_use_github.mdx @@ -131,7 +131,7 @@ would be useful to all teams, talk to your team or tech lead about getting it ad ### Team labels -Examples: `Team:Security`, `Team:Operations`. +Examples: `Team:Security`, `Team:Operations`, `Team:Docs`. These labels map the issue to the team that owns the particular area. Part of the responsibilities of (todo) is to ensure every issue has at least a team or a project @@ -178,3 +178,36 @@ it might mean the version the team is tentatively planning to merge a fix. Consult the owning team if you have a question about how a version label is meant to be used on an issue. + +### Issue type and workflow labels + +These labels categorize the type of work. For example: + +- `blocked`: Indicates the issue is currently blocked +- `blocker`: Indicates that we should not release the product at the next + proposed version without the issue being resolved +- `bug`: Indicates an unexpected problem or unintended behavior +- `discuss`: Indicates that an issue is a discussion topic +- `docs`/`documentation`: Indicates improvements or additions to documentation +- `enhancement`: Indicates new feature or enhancement requests +- `meta`: Indicates that the issue tracks tasks related to a project +- `needs_triage`: Indicates that someone from the area team needs to investigate. + +These labels affect whether your PR appears in the release notes (that is to say, +it's notable and affects our users) and which section it appears in. For example: + +- `release_note:breaking`: Specifies a breaking change and adds the PR to the Breaking changes section in the release notes +- `release_note:deprecation`: Specifies a deprecated feature and adds the PR to the Deprecations section in the release notes +- `release_note:enhancement`: Specifies a feature enhancement and adds the PR to the Enhancements section in the release notes +- `release_note:feature`: Specifies a new feature and adds the PR to the Features section in the release notes +- `release_note:fix`: Specifies a bug fix and adds the PR to the Bug fixes section in the release notes +- `release_node:plugin_api_changes`: Specifies a changes to the plugin API and adds the PR to the Plugin API changes page in the Developer Guide +- `release_note:skip`: Omits the PR from release notes + +These labels related to backporting PRs: + +- `auto-backport`: Automatically backport this PR (to the branches related to + version labels) after it's merged +- `backport`: This PR was backported +- `backport:skip`: This PR does not require backporting + diff --git a/dev_docs/operations/operations_landing.mdx b/dev_docs/operations/operations_landing.mdx index 165421deb84308..b9e68beb9637e8 100644 --- a/dev_docs/operations/operations_landing.mdx +++ b/dev_docs/operations/operations_landing.mdx @@ -50,6 +50,7 @@ layout: landing { pageId: "kibDevDocsOpsAmbientUiTypes" }, { pageId: "kibDevDocsOpsTestSubjSelector" }, { pageId: "kibDevDocsOpsBazelRunner" }, - { pageId: "kibDevDocsOpsCliDevMode" } + { pageId: "kibDevDocsOpsCliDevMode" }, + { pageId: "kibDevDocsOpsEs" }, ]} /> \ No newline at end of file diff --git a/docs/api/cases/cases-api-get-cases-by-alert.asciidoc b/docs/api/cases/cases-api-get-cases-by-alert.asciidoc index 3bd2e8debb3cd5..01aec7a7e4c773 100644 --- a/docs/api/cases/cases-api-get-cases-by-alert.asciidoc +++ b/docs/api/cases/cases-api-get-cases-by-alert.asciidoc @@ -61,12 +61,8 @@ For example: -------------------------------------------------- [ { - "id": "8af6ac20-74f6-11ea-b83a-553aecdb28b6", - "title": "Case 1" - }, - { - "id": "a18b38a0-71b0-11ea-a0b2-c51ea50a58e2", - "title": "Case 2" + "id": "06116b80-e1c3-11ec-be9b-9b1838238ee6", + "title":"security_case" } ] -------------------------------------------------- \ No newline at end of file diff --git a/docs/api/cases/cases-api-get-configuration.asciidoc b/docs/api/cases/cases-api-get-configuration.asciidoc index 778e95949e3f5c..46cb11dc63b032 100644 --- a/docs/api/cases/cases-api-get-configuration.asciidoc +++ b/docs/api/cases/cases-api-get-configuration.asciidoc @@ -44,7 +44,7 @@ read. [source,sh] -------------------------------------------------- -GET api/cases/configure?owner=securitySolution +GET api/cases/configure?owner=cases -------------------------------------------------- // KIBANA @@ -54,19 +54,19 @@ The API returns the following type of information: -------------------------------------------------- [ { - "owner": "securitySolution", "closure_type": "close-by-user", - "created_at": "2020-03-30T13:31:38.083Z", + "owner": "cases", + "created_at": "2022-06-01T17:07:17.767Z", "created_by": { - "email": "admin@hms.gov.uk", - "full_name": "Mr Admin", - "username": "admin" + "email": "null", + "full_name": "null", + "username": "elastic" }, "updated_at": null, "updated_by": null, "connector": { "id": "131d4448-abe0-4789-939d-8ef60680b498", - "name": "my-jira", + "name": "my-jira-connector", "type": ".jira", "fields": null }, diff --git a/docs/api/cases/cases-api-set-configuration.asciidoc b/docs/api/cases/cases-api-set-configuration.asciidoc index 6a7a7c26c66d27..89ec6f0600717a 100644 --- a/docs/api/cases/cases-api-set-configuration.asciidoc +++ b/docs/api/cases/cases-api-set-configuration.asciidoc @@ -109,9 +109,9 @@ POST api/cases/configure { "owner": "cases", "connector": { - "id": "131d4448-abe0-4789-939d-8ef60680b498", - "name": "my-serviceNow", - "type": ".servicenow", + "id": "5e656730-e1ca-11ec-be9b-9b1838238ee6", + "name": "my-jira-connector", + "type": ".jira", "fields": null, }, "closure_type": "close-by-user" @@ -123,41 +123,41 @@ The API returns the following response: [source,json] -------------------------------------------------- { - "owner": "cases", "closure_type": "close-by-user", - "created_at": "2022-04-02T01:09:02.303Z", + "owner": "cases", + "created_at": "2022-06-01T17:07:17.767Z", "created_by": { - "email": "moneypenny@hms.gov.uk", - "full_name": "Ms Moneypenny", - "username": "moneypenny" + "username": "elastic", + "email": null, + "full_name": null }, "updated_at": null, "updated_by": null, "connector": { - "id": "131d4448-abe0-4789-939d-8ef60680b498", - "name": "my-serviceNow", - "type": ".servicenow", - "fields": null, + "id": "5e656730-e1ca-11ec-be9b-9b1838238ee6", + "name": "my-jira-connector", + "type": ".jira", + "fields": null }, "mappings": [ { - "source": "title", - "target": "short_description", + "source": "title", + "target": "summary", "action_type": "overwrite" }, { - "source":"description", - "target":"description", - "action_type":"overwrite" + "source": "description", + "target": "description", + "action_type": "overwrite" }, { - "source":"comments", - "target":"work_notes", - "action_type":"append" + "source": "comments", + "target": "comments", + "action_type": "append" } ], - "version": "WzE3NywxXQ==", + "version": "WzIwNzMsMV0=", "error": null, - "id": "7349772f-421a-4de3-b8bb-2d9b22ccee30", + "id": "4a97a440-e1cd-11ec-be9b-9b1838238ee6" } -------------------------------------------------- diff --git a/docs/api/cases/cases-api-update-configuration.asciidoc b/docs/api/cases/cases-api-update-configuration.asciidoc index cf7d2ea7d8cfd9..c1dcb2a71e57cf 100644 --- a/docs/api/cases/cases-api-update-configuration.asciidoc +++ b/docs/api/cases/cases-api-update-configuration.asciidoc @@ -101,19 +101,19 @@ The API returns the following: [source,json] -------------------------------------------------- { - "closure_type": "close-by-user", + "closure_type": "close-by-pushing", "owner": "cases", - "created_at": "2022-04-06T20:57:40.746Z", + "created_at": "2022-06-01T17:07:17.767Z", "created_by": { - "email": "admin@hms.gov.uk", - "full_name": "Ms Admin", - "username": "admin" + "email": "null", + "full_name": "null", + "username": "elastic" }, - "updated_at": "2022-04-12T22:41:09.262Z", + "updated_at": "2022-06-01T19:58:48.169Z", "updated_by": { - "email": "admin@hms.gov.uk", - "full_name": "Ms Admin", - "username": "admin" + "email": "null", + "full_name": "null", + "username": "elastic" }, "connector": { "id": "none", diff --git a/docs/api/data-views.asciidoc b/docs/api/data-views.asciidoc index d7380cbd97c998..cf9524d4fdf30d 100644 --- a/docs/api/data-views.asciidoc +++ b/docs/api/data-views.asciidoc @@ -11,6 +11,7 @@ WARNING: Use the data views APIs for managing data views instead of lower-level The following data views APIs are available: * Data views + ** <> to retrieve a list of data views ** <> to retrieve a single data view ** <> to create data view ** <> to partially updated data view @@ -27,6 +28,7 @@ The following data views APIs are available: ** <> to partially update an existing runtime field ** <> to delete a runtime field +include::data-views/get-all.asciidoc[] include::data-views/get.asciidoc[] include::data-views/create.asciidoc[] include::data-views/update.asciidoc[] diff --git a/docs/api/data-views/get-all.asciidoc b/docs/api/data-views/get-all.asciidoc new file mode 100644 index 00000000000000..42727c38f6d98c --- /dev/null +++ b/docs/api/data-views/get-all.asciidoc @@ -0,0 +1,60 @@ +[[data-views-api-get-all]] +=== Get all data views API +++++ +Get all data views +++++ + +experimental[] Retrieve a list of all data views. + + +[[data-views-api-get-all-request]] +==== Request + +`GET :/api/data_views` + +`GET :/s//api/data_views` + + +[[data-views-api-get-all-codes]] +==== Response code + +`200`:: +Indicates a successful call. + + +[[data-views-api-get-all-example]] +==== Example + +Retrieve the list of data views: + +[source,sh] +-------------------------------------------------- +$ curl -X GET api/data_views +-------------------------------------------------- +// KIBANA + +The API returns a list of data views: + +[source,sh] +-------------------------------------------------- +{ + "data_view": [ + { + "id": "e9e024f0-d098-11ec-bbe9-c753adcb34bc", + "namespaces": [ + "default" + ], + "title": "tmp*", + "type": "rollup", + "typeMeta": {} + }, + { + "id": "90943e30-9a47-11e8-b64d-95841ca0b247", + "namespaces": [ + "default" + ], + "title": "kibana_sample_data_logs" + } + ] +} +-------------------------------------------------- diff --git a/docs/apm/apm-spaces.asciidoc b/docs/apm/apm-spaces.asciidoc new file mode 100644 index 00000000000000..c43a512768fad8 --- /dev/null +++ b/docs/apm/apm-spaces.asciidoc @@ -0,0 +1,415 @@ +[role="xpack"] +[[apm-spaces]] +=== Control access to APM data + +Starting in version 8.2.0, the APM app is <> aware. +This allows you to separate your data--and access to that data--by team, use case, service environment, +or any other filter that you choose. + +To take advantage of this feature, your APM data needs to be written to different data steams. +One way to accomplish this is with different namespaces. +For example, you can send production data to an APM integration with a namespace of `production`, +while sending staging data to a different APM integration with a namespace of `staging`. + +Multiple APM integration instances is not required though. The simplest way to take advantage of this feature +is by creating filtered aliases. See the guide below for more information. + +[float] +[[apm-spaces-example]] +=== Guide: Separate staging and production data + +This guide will explain how to separate your staging and production data. +This can be helpful to either remove noise when troubleshooting a production issue, +or to create more granular access control for certain data. + +This guide assumes that you: + +* Are sending both staging and production APM data to an {es} cluster. +* Have configured the `environment` variable in your APM agent configurations. +This variable sets the `service.environment` field in APM documents. +You should have documents where `service.environment: production` and `service.environment: staging`. +If this field is empty, see <> to learn how to set this value. + +[float] +==== Step 1: Create filtered aliases + +The APM app uses index patterns to query your APM data. An index pattern can match data streams, indices, and/or aliases. +The default values are: + +[options="header"] +|==== +| Index setting | Default index pattern +| Error | `logs-apm*` +| Span/Transaction | `traces-apm*` +| Metrics | `metrics-apm*` +|==== + +NOTE: The default index settings also query the `apm-*` data view. +This data view matches APM data shipped in earlier versions of APM (prior to v8.0). + +Instead of querying the default APM data views, we can create filtered aliases for the APM app to query. +A filtered alias is a secondary name for a group of data streams that has a user-defined +filter to limit the documents that the alias can access. + +To separate `staging` and `production` APM data, we'd need to create six filtered aliases--three +aliases for each service environment: + +[options="header"] +|==== +| Index setting | `production` env | `staging` evn +| Error | `production-logs-apm` | `staging-logs-apm` +| Span/Transaction | `production-traces-apm` | `staging-traces-apm` +| Metrics | `production-metrics-apm` | `staging-metrics-apm` +|==== + +The `production--apm` aliases will contain a filter that only provides access to documents +where the `service.environment` is `production`. +Similarly, the `staging--apm` aliases will contain a filter that only provides access to documents +where the `service.environment` is `staging`. + +To create these six filtered aliases, use the {es} {ref}/indices-aliases.html[Aliases API]. +In {kib}, open **Dev Tools** and run the following POST requests. + +[%collapsible%open] +.`traces-apm*` production alias example +==== +[source, console] +---- +POST /_aliases?pretty +{ + "actions": [ + { + "add": { + "index": "traces-apm*", <1> + "alias": "production-traces-apm", <2> + "filter": { + "term": { + "service.environment": { + "value": "production" <3> + } + } + } + } + } + ] +} +---- +<1> This example matches the APM traces data stream +<2> The alias must not match the default APM index (`traces-apm*,apm-*`) +<3> Only match documents where `service.environment: production` +==== + +[%collapsible] +.`logs-apm*` production alias example +==== +[source, console] +---- +POST /_aliases?pretty +{ + "actions": [ + { + "add": { + "index": "logs-apm*", <1> + "alias": "production-logs-apm", <2> + "filter": { + "term": { + "service.environment": { + "value": "production" <3> + } + } + } + } + } + ] +} +---- +<1> This example matches the APM logs data stream +<2> The alias must not match the default APM index (`logs-apm*,apm-*`) +<3> Only match documents where `service.environment: production` +==== + +[%collapsible] +.`metrics-apm*` production alias example +==== +[source, console] +---- +POST /_aliases?pretty +{ + "actions": [ + { + "add": { + "index": "metrics-apm*", <1> + "alias": "production-metrics-apm", <2> + "filter": { + "term": { + "service.environment": { + "value": "production" <3> + } + } + } + } + } + ] +} +---- +<1> This example matches the APM metrics data stream +<2> The alias must not match the default APM index (`metrics-apm*,apm-*`) +<3> Only match documents where `service.environment: production` +==== + +[%collapsible] +.`traces-apm*` staging alias example +==== +[source, console] +---- +POST /_aliases?pretty +{ + "actions": [ + { + "add": { + "index": "traces-apm*", <1> + "alias": "staging-traces-apm", <2> + "filter": { + "term": { + "service.environment": { + "value": "staging" <3> + } + } + } + } + } + ] +} +---- +<1> This example matches the APM traces data stream +<2> The alias must not match the default APM index (`traces-apm*,apm-*`) +<3> Only match documents where `service.environment: staging` +==== + +[%collapsible] +.`logs-apm*` staging alias example +==== +[source, console] +---- +POST /_aliases?pretty +{ + "actions": [ + { + "add": { + "index": "logs-apm*", <1> + "alias": "staging-logs-apm", <2> + "filter": { + "term": { + "service.environment": { + "value": "staging" <3> + } + } + } + } + } + ] +} +---- +<1> This example matches the APM logs data stream +<2> The alias must not match the default APM index (`logs-apm*,apm-*`) +<3> Only match documents where `service.environment: staging` +==== + +[%collapsible] +.`metrics-apm*` staging alias example +==== +[source, console] +---- +POST /_aliases?pretty +{ + "actions": [ + { + "add": { + "index": "metrics-apm*", <1> + "alias": "staging-metrics-apm", <2> + "filter": { + "term": { + "service.environment": { + "value": "staging" <3> + } + } + } + } + } + ] +} +---- +<1> This example matches the APM metrics data stream +<2> The alias must not match the default APM index (`metrics-apm*,apm-*`) +<3> Only match documents where `service.environment: staging` +==== + +[float] +==== Step 2: Create {kib} spaces + +Next, you'll need to create a {Kib} space for each service environment. +To create these spaces, navigate to **Stack Management** > **Spaces** > **Create a space**. +For this guide, we've created two Kibana spaces, one named `production` and one named `staging`. + +See <> for more information on creating a space. + +[float] +==== Step 3: Update APM index settings in each space + +Now we can change the default data views that the APM app queries in each space. + +Open the APM app and navigate to **Settings** > **Indices**. +Use the table below to update your settings for each space. +The values in each column match the names of the filtered aliases we created in step one. + +[options="header"] +|==== +| Index setting | `production` space | `staging` space +| Error indices | `production-logs-apm` | `staging-logs-apm` +| Span indices | `production-traces-apm` | `staging-traces-apm` +| Transaction indices | `production-traces-apm` | `staging-traces-apm` +| Metrics indices | `production-metrics-apm` | `staging-metrics-apm` +|==== + +[role="screenshot"] +image::settings/images/apm-settings.png[APM app settings in Kibana] + +[float] +==== Step 4: Create {kib} access roles + +In {kib}, navigate to **Stack Management** > **Roles** and click **Create role**. + +You'll need to create two roles: one for `staging` users (we'll call this role `staging_apm_viewer`) +and one for `production` users (we'll call this role `production_apm_viewer`). + +Using the table below, assign each role the following privileges: + +[options="header"] +|==== +| Privileges | `production_apm_viewer` | `staging_apm_viewer` +| Index privileges | index: `production-*-apm`, privilege: `read` | index: `staging-*-apm`, privilege: `read` +| Kibana privileges | space: `production`, feature privileges: `APM and User Experience: read` | space: `staging`, feature privileges: `APM and User Experience: read` +|==== + +[role="screenshot"] +image::./images/apm-roles-config.png[APM role config example] + +Alternatively, you can use the +{es} {ref}/security-api-put-role.html[Create or update roles API]: + +[%collapsible%open] +.Create a `production_apm_viewer` role +==== +This request creates a `production_apm_viewer` role: + +[source, console] +---- +POST /_security/role/production_apm_viewer +{ + "cluster": [ ], + "indices": [ + { + "names": ["production-*-apm"], <1> + "privileges": ["read"] + } + ], + "applications": [ + { + "application" : "kibana-.kibana", + "privileges" : [ + "feature_apm.read" <2> + ], + "resources" : [ + "space:production" <3> + ] + } + ] +} +---- +<1> This data view matches all of the production aliases created in step one. +<2> Assigns `read` privileges for the APM and User Experience apps. +<3> Provides access to the space named `production`. +==== + +[%collapsible] +.Create a `staging_apm_viewer` role +==== +This request creates a `staging_apm_viewer` role: + +[source, console] +---- +POST /_security/role/staging_apm_viewer +{ + "cluster": [ ], + "indices": [ + { + "names": ["staging-*-apm"], <1> + "privileges": ["read"] + } + ], + "applications": [ + { + "application" : "kibana-.kibana", + "privileges" : [ + "feature_apm.read" <2> + ], + "resources" : [ + "space:staging" <3> + ] + } + ] +} +---- +<1> This data view matches all of the staging aliases created in step one. +<2> Assigns `read` privileges for the APM and User Experience apps. +<3> Provides access to the space named `staging`. +==== + +[float] +==== Step 5: Assign users to roles + +The last thing to do is assign users to the newly created roles above. +Users will only have access to the data within the spaces that they are granted. + +For information on how to create users and assign them roles with the {kib} UI, +see <>. + +Alternatively, you can use the +{es} {ref}/security-api-put-user.html[Create or update users API]. + +This example creates a new user and assigns them the `production_apm_viewer` role created in the previous step. +This user will only have access to the production space and data with a `service.environment` of `production`. +Remember to change the `password`, `full_name`, and `email` fields. + +[source, console] +---- +POST /_security/user/production-apm-user +{ + "password" : "l0ng-r4nd0m-p@ssw0rd", + "roles" : [ "production_apm_viewer" ], <1> + "full_name" : "Jane Production Smith", + "email" : "janesmith@example.com" +} +---- +<1> Assigns the previously created `production_apm_viewer` role. + +This example creates a new user and assigns them the `staging_apm_viewer` role created in the previous step. +This user will only have access to the staging space and data with a `service.environment` of `staging`. +Remember to change the `password`, `full_name`, and `email` fields. + +[source, console] +---- +POST /_security/user/staging-apm-user +{ + "password" : "l0ng-r4nd0m-p@ssw0rd", + "roles" : [ "staging_apm_viewer" ], <1> + "full_name" : "John Staging Doe", + "email" : "johndoe@example.com" +} +---- +<1> Assigns the previously created `staging_apm_viewer` role. + +[float] +==== Step 6: Marvel + +That's it! Head back to the APM app and marvel at your space-specific data. diff --git a/docs/apm/how-to-guides.asciidoc b/docs/apm/how-to-guides.asciidoc index b4e49a69d5a7ef..b634c937588b0a 100644 --- a/docs/apm/how-to-guides.asciidoc +++ b/docs/apm/how-to-guides.asciidoc @@ -6,6 +6,7 @@ Learn how to perform common APM app tasks. * <> +* <> * <> * <> * <> @@ -17,6 +18,8 @@ Learn how to perform common APM app tasks. include::agent-configuration.asciidoc[] +include::apm-spaces.asciidoc[] + include::apm-alerts.asciidoc[] include::custom-links.asciidoc[] diff --git a/docs/apm/images/apm-integration-config.png b/docs/apm/images/apm-integration-config.png new file mode 100644 index 00000000000000..7ff5cb5e9d0bad Binary files /dev/null and b/docs/apm/images/apm-integration-config.png differ diff --git a/docs/apm/images/apm-roles-config.png b/docs/apm/images/apm-roles-config.png new file mode 100644 index 00000000000000..ebd992abe93039 Binary files /dev/null and b/docs/apm/images/apm-roles-config.png differ diff --git a/docs/apm/images/apm-settings.png b/docs/apm/images/apm-settings.png index 2201ed5fcaa725..2c8ebace287b82 100644 Binary files a/docs/apm/images/apm-settings.png and b/docs/apm/images/apm-settings.png differ diff --git a/docs/settings/apm-settings.asciidoc b/docs/settings/apm-settings.asciidoc index 2cfd3169b45a30..65f291a1c11cbc 100644 --- a/docs/settings/apm-settings.asciidoc +++ b/docs/settings/apm-settings.asciidoc @@ -18,9 +18,14 @@ It is enabled by default. // Any changes made in this file will be seen there as well. // tag::apm-indices-settings[] -Index defaults can be changed in the APM app. Select **Settings** > **Indices**. +The APM app uses data views to query APM indices. +To change the default APM indices that the APM app queries, open the APM app and select **Settings** > **Indices**. Index settings in the APM app take precedence over those set in `kibana.yml`. +Starting in version 8.2.0, APM indices are {kib} Spaces-aware; +Changes to APM index settings will only apply to the currently enabled space. +See <> for more information. + [role="screenshot"] image::settings/images/apm-settings.png[APM app settings in Kibana] @@ -72,7 +77,7 @@ Maximum number of child items displayed when viewing trace details. Defaults to Index name where Observability annotations are stored. Defaults to `observability-annotations`. `xpack.apm.searchAggregatedTransactions` {ess-icon}:: -Enables Transaction histogram metrics. Defaults to `auto` so the UI will use metric indices over transaction indices for transactions if aggregated transactions are found. When set to `always`, additional configuration in APM Server is required. When set to `never` and aggregated transactions are not used. +Enables Transaction histogram metrics. Defaults to `auto` so the UI will use metric indices over transaction indices for transactions if aggregated transactions are found. When set to `always`, additional configuration in APM Server is required. When set to `never` and aggregated transactions are not used. + See {apm-guide-ref}/transaction-metrics.html[Configure transaction metrics] for more information. diff --git a/docs/settings/images/apm-settings.png b/docs/settings/images/apm-settings.png index 876f135da93564..f3adae184348fa 100644 Binary files a/docs/settings/images/apm-settings.png and b/docs/settings/images/apm-settings.png differ diff --git a/nav-kibana-dev.docnav.json b/nav-kibana-dev.docnav.json index 0a2f2c4e3b6f1a..1473bf4d59a0e9 100644 --- a/nav-kibana-dev.docnav.json +++ b/nav-kibana-dev.docnav.json @@ -206,7 +206,8 @@ { "id": "kibDevDocsOpsAmbientUiTypes" }, { "id": "kibDevDocsOpsTestSubjSelector" }, { "id": "kibDevDocsOpsBazelRunner" }, - { "id": "kibDevDocsOpsCliDevMode" } + { "id": "kibDevDocsOpsCliDevMode" }, + { "id": "kibDevDocsOpsEs" } ] } ] diff --git a/packages/kbn-es-archiver/src/actions/save.ts b/packages/kbn-es-archiver/src/actions/save.ts index 16f0cbc3c18462..9fcbe45946eb77 100644 --- a/packages/kbn-es-archiver/src/actions/save.ts +++ b/packages/kbn-es-archiver/src/actions/save.ts @@ -52,7 +52,7 @@ export async function saveAction({ // export and save the matching indices to mappings.json createPromiseFromStreams([ createListStream(indices), - createGenerateIndexRecordsStream({ client, stats, keepIndexNames }), + createGenerateIndexRecordsStream({ client, stats, keepIndexNames, log }), ...createFormatArchiveStreams(), createWriteStream(resolve(outputDir, 'mappings.json')), ] as [Readable, ...Writable[]]), diff --git a/packages/kbn-es-archiver/src/actions/unload.ts b/packages/kbn-es-archiver/src/actions/unload.ts index 2d4b16d7186894..e564bcbb1a703e 100644 --- a/packages/kbn-es-archiver/src/actions/unload.ts +++ b/packages/kbn-es-archiver/src/actions/unload.ts @@ -45,7 +45,7 @@ export async function unloadAction({ await createPromiseFromStreams([ createReadStream(resolve(inputDir, filename)) as Readable, ...createParseArchiveStreams({ gzip: isGzip(filename) }), - createFilterRecordsStream('index'), + createFilterRecordsStream((record) => ['index', 'data_stream'].includes(record.type)), createDeleteIndexStream(client, stats, log), ] as [Readable, ...Writable[]]); } diff --git a/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.test.ts b/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.test.ts index e102ac50c3876b..386d6d4a088ce2 100644 --- a/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.test.ts +++ b/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.test.ts @@ -36,16 +36,29 @@ interface SearchResponses { }>; } -function createMockClient(responses: SearchResponses) { +function createMockClient(responses: SearchResponses, hasDataStreams = false) { // TODO: replace with proper mocked client const client: any = { helpers: { scrollSearch: jest.fn(function* ({ index }) { + if (hasDataStreams) { + index = `.ds-${index}`; + } + while (responses[index] && responses[index].length) { yield responses[index].shift()!; } }), }, + indices: { + get: jest.fn(async ({ index }) => { + return { [index]: { data_stream: hasDataStreams && index.substring(4) } }; + }), + getDataStream: jest.fn(async ({ name }) => { + if (!hasDataStreams) return { data_streams: [] }; + return { data_streams: [{ name }] }; + }), + }, }; return client; } @@ -217,6 +230,35 @@ describe('esArchiver: createGenerateDocRecordsStream()', () => { `); }); + it('supports data streams', async () => { + const hits = [ + { _index: '.ds-foo-datastream', _id: '0', _source: {} }, + { _index: '.ds-foo-datastream', _id: '1', _source: {} }, + ]; + const responses = { + '.ds-foo-datastream': [{ body: { hits: { hits, total: hits.length } } }], + }; + const client = createMockClient(responses, true); + + const stats = createStats('test', log); + const progress = new Progress(); + + const results = await createPromiseFromStreams([ + createListStream(['foo-datastream']), + createGenerateDocRecordsStream({ + client, + stats, + progress, + }), + createMapStream((record: any) => { + return `${record.value.data_stream}:${record.value.id}`; + }), + createConcatStream([]), + ]); + + expect(results).toEqual(['foo-datastream:0', 'foo-datastream:1']); + }); + describe('keepIndexNames', () => { it('changes .kibana* index names if keepIndexNames is not enabled', async () => { const hits = [{ _index: '.kibana_7.16.0_001', _id: '0', _source: {} }]; diff --git a/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.ts b/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.ts index 40907bd0af2386..6e3310a7347e73 100644 --- a/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.ts +++ b/packages/kbn-es-archiver/src/lib/docs/generate_doc_records_stream.ts @@ -47,6 +47,10 @@ export function createGenerateDocRecordsStream({ } ); + const hasDatastreams = + (await client.indices.getDataStream({ name: index })).data_streams.length > 0; + const indexToDatastream = new Map(); + let remainingHits: number | null = null; for await (const resp of interator) { @@ -57,7 +61,17 @@ export function createGenerateDocRecordsStream({ for (const hit of resp.body.hits.hits) { remainingHits -= 1; - stats.archivedDoc(hit._index); + + if (hasDatastreams && !indexToDatastream.has(hit._index)) { + const { + [hit._index]: { data_stream: dataStream }, + } = await client.indices.get({ index: hit._index, filter_path: ['*.data_stream'] }); + indexToDatastream.set(hit._index, dataStream); + } + + const dataStream = indexToDatastream.get(hit._index); + stats.archivedDoc(dataStream || hit._index); + this.push({ type: 'doc', value: { @@ -65,6 +79,7 @@ export function createGenerateDocRecordsStream({ // when it is loaded it can skip migration, if possible index: hit._index.startsWith('.kibana') && !keepIndexNames ? '.kibana_1' : hit._index, + data_stream: dataStream, id: hit._id, source: hit._source, }, diff --git a/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.test.ts b/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.test.ts index 5dc9b4b7bd8ddb..c1bb94ee134989 100644 --- a/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.test.ts +++ b/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.test.ts @@ -243,6 +243,55 @@ describe('bulk helper onDocument param', () => { createIndexDocRecordsStream(client as any, stats, progress, true), ]); }); + + it('returns create ops for data stream documents', async () => { + const records = [ + { + type: 'doc', + value: { + index: '.ds-foo-ds', + data_stream: 'foo-ds', + id: '0', + source: { + hello: 'world', + }, + }, + }, + { + type: 'doc', + value: { + index: '.ds-foo-ds', + data_stream: 'foo-ds', + id: '1', + source: { + hello: 'world', + }, + }, + }, + ]; + expect.assertions(records.length); + + const client = new MockClient(); + client.helpers.bulk.mockImplementation(async ({ datasource, onDocument }) => { + for (const d of datasource) { + const op = onDocument(d); + expect(op).toEqual({ + create: { + _index: 'foo-ds', + _id: expect.stringMatching(/^\d$/), + }, + }); + } + }); + + const stats = createStats('test', log); + const progress = new Progress(); + + await createPromiseFromStreams([ + createListStream(records), + createIndexDocRecordsStream(client as any, stats, progress), + ]); + }); }); describe('bulk helper onDrop param', () => { diff --git a/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.ts b/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.ts index 749bfd08723534..40e1c1932aeeec 100644 --- a/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.ts +++ b/packages/kbn-es-archiver/src/lib/docs/index_doc_records_stream.ts @@ -13,6 +13,11 @@ import { Stats } from '../stats'; import { Progress } from '../progress'; import { ES_CLIENT_HEADERS } from '../../client_headers'; +enum BulkOperation { + Create = 'create', + Index = 'index', +} + export function createIndexDocRecordsStream( client: Client, stats: Stats, @@ -20,7 +25,7 @@ export function createIndexDocRecordsStream( useCreate: boolean = false ) { async function indexDocs(docs: any[]) { - const operation = useCreate === true ? 'create' : 'index'; + const operation = useCreate === true ? BulkOperation.Create : BulkOperation.Index; const ops = new WeakMap(); const errors: string[] = []; @@ -29,9 +34,11 @@ export function createIndexDocRecordsStream( retries: 5, datasource: docs.map((doc) => { const body = doc.source; + const op = doc.data_stream ? BulkOperation.Create : operation; + const index = doc.data_stream || doc.index; ops.set(body, { - [operation]: { - _index: doc.index, + [op]: { + _index: index, _id: doc.id, }, }); @@ -56,7 +63,7 @@ export function createIndexDocRecordsStream( } for (const doc of docs) { - stats.indexedDoc(doc.index); + stats.indexedDoc(doc.data_stream || doc.index); } } diff --git a/packages/kbn-es-archiver/src/lib/index.ts b/packages/kbn-es-archiver/src/lib/index.ts index ee37591e1f2c30..8a857fb24002a7 100644 --- a/packages/kbn-es-archiver/src/lib/index.ts +++ b/packages/kbn-es-archiver/src/lib/index.ts @@ -33,3 +33,5 @@ export { export { readDirectory } from './directory'; export { Progress } from './progress'; + +export { getIndexTemplate } from './index_template'; diff --git a/packages/kbn-es-archiver/src/lib/index_template.test.ts b/packages/kbn-es-archiver/src/lib/index_template.test.ts new file mode 100644 index 00000000000000..b8f5330663ee15 --- /dev/null +++ b/packages/kbn-es-archiver/src/lib/index_template.test.ts @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import type { Client } from '@elastic/elasticsearch'; + +import sinon from 'sinon'; +import { getIndexTemplate } from './index_template'; + +describe('esArchiver: index template', () => { + describe('getIndexTemplate', () => { + it('returns the index template', async () => { + const client = { + indices: { + getIndexTemplate: sinon.stub().resolves({ + index_templates: [ + { + index_template: { + index_patterns: ['pattern-*'], + template: { + mappings: { properties: { foo: { type: 'keyword' } } }, + }, + priority: 500, + composed_of: [], + data_stream: { hidden: false }, + }, + }, + ], + }), + }, + } as unknown as Client; + + const template = await getIndexTemplate(client, 'template-foo'); + + expect(template).toEqual({ + name: 'template-foo', + index_patterns: ['pattern-*'], + template: { + mappings: { properties: { foo: { type: 'keyword' } } }, + }, + priority: 500, + data_stream: { hidden: false }, + }); + }); + + it('resolves component templates', async () => { + const client = { + indices: { + getIndexTemplate: sinon.stub().resolves({ + index_templates: [ + { + index_template: { + index_patterns: ['pattern-*'], + composed_of: ['the-settings', 'the-mappings'], + }, + }, + ], + }), + }, + cluster: { + getComponentTemplate: sinon + .stub() + .onFirstCall() + .resolves({ + component_templates: [ + { + component_template: { + template: { + aliases: { 'foo-alias': {} }, + }, + }, + }, + ], + }) + .onSecondCall() + .resolves({ + component_templates: [ + { + component_template: { + template: { + mappings: { properties: { foo: { type: 'keyword' } } }, + }, + }, + }, + ], + }), + }, + } as unknown as Client; + + const template = await getIndexTemplate(client, 'template-foo'); + + expect(template).toEqual({ + name: 'template-foo', + index_patterns: ['pattern-*'], + template: { + aliases: { 'foo-alias': {} }, + mappings: { properties: { foo: { type: 'keyword' } } }, + }, + }); + }); + }); +}); diff --git a/packages/kbn-es-archiver/src/lib/index_template.ts b/packages/kbn-es-archiver/src/lib/index_template.ts new file mode 100644 index 00000000000000..9d67add9757dbe --- /dev/null +++ b/packages/kbn-es-archiver/src/lib/index_template.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { merge } from 'lodash'; +import type { Client } from '@elastic/elasticsearch'; + +import { ES_CLIENT_HEADERS } from '../client_headers'; + +export const getIndexTemplate = async (client: Client, templateName: string) => { + const { index_templates: indexTemplates } = await client.indices.getIndexTemplate( + { name: templateName }, + { headers: ES_CLIENT_HEADERS } + ); + const { + index_template: { template, composed_of: composedOf = [], ...indexTemplate }, + } = indexTemplates[0]; + + const components = await Promise.all( + composedOf.map(async (component) => { + const { component_templates: componentTemplates } = await client.cluster.getComponentTemplate( + { name: component } + ); + return componentTemplates[0].component_template.template; + }) + ); + + return { + ...indexTemplate, + name: templateName, + template: merge(template, ...components), + }; +}; diff --git a/packages/kbn-es-archiver/src/lib/indices/__mocks__/stubs.ts b/packages/kbn-es-archiver/src/lib/indices/__mocks__/stubs.ts index c60c9201001743..1bfbc80f52a19f 100644 --- a/packages/kbn-es-archiver/src/lib/indices/__mocks__/stubs.ts +++ b/packages/kbn-es-archiver/src/lib/indices/__mocks__/stubs.ts @@ -19,7 +19,9 @@ export const createStubStats = (): StubStats => ({ createdIndex: sinon.stub(), createdAliases: sinon.stub(), + createdDataStream: sinon.stub(), deletedIndex: sinon.stub(), + deletedDataStream: sinon.stub(), skippedIndex: sinon.stub(), archivedIndex: sinon.stub(), getTestSummary() { @@ -47,6 +49,11 @@ export const createStubIndexRecord = (index: string, aliases = {}) => ({ value: { index, aliases }, }); +export const createStubDataStreamRecord = (dataStream: string, template: string) => ({ + type: 'data_stream', + value: { data_stream: dataStream, template: { name: template } }, +}); + export const createStubDocRecord = (index: string, id: number) => ({ type: 'doc', value: { index, id }, @@ -140,5 +147,10 @@ export const createStubClient = ( exists: sinon.spy(async () => { throw new Error('Do not use indices.exists(). React to errors instead.'); }), + + createDataStream: sinon.spy(async ({ name }) => {}), + deleteDataStream: sinon.spy(async ({ name }) => {}), + putIndexTemplate: sinon.spy(async ({ name }) => {}), + deleteIndexTemplate: sinon.spy(async ({ name }) => {}), }, } as any); diff --git a/packages/kbn-es-archiver/src/lib/indices/create_index_stream.test.ts b/packages/kbn-es-archiver/src/lib/indices/create_index_stream.test.ts index 615555b405e445..15efa539217437 100644 --- a/packages/kbn-es-archiver/src/lib/indices/create_index_stream.test.ts +++ b/packages/kbn-es-archiver/src/lib/indices/create_index_stream.test.ts @@ -17,6 +17,7 @@ import { createCreateIndexStream } from './create_index_stream'; import { createStubStats, createStubIndexRecord, + createStubDataStreamRecord, createStubDocRecord, createStubClient, createStubLogger, @@ -171,6 +172,19 @@ describe('esArchiver: createCreateIndexStream()', () => { expect(output).toEqual(nonRecordValues); }); + + it('creates data streams', async () => { + const client = createStubClient(); + const stats = createStubStats(); + + await createPromiseFromStreams([ + createListStream([createStubDataStreamRecord('foo-datastream', 'foo-template')]), + createCreateIndexStream({ client, stats, log }), + ]); + + sinon.assert.calledOnce(client.indices.putIndexTemplate as sinon.SinonSpy); + sinon.assert.calledOnce(client.indices.createDataStream as sinon.SinonSpy); + }); }); describe('deleteKibanaIndices', () => { diff --git a/packages/kbn-es-archiver/src/lib/indices/create_index_stream.ts b/packages/kbn-es-archiver/src/lib/indices/create_index_stream.ts index 2ab53a2ca012c0..38f4bed755262d 100644 --- a/packages/kbn-es-archiver/src/lib/indices/create_index_stream.ts +++ b/packages/kbn-es-archiver/src/lib/indices/create_index_stream.ts @@ -13,15 +13,18 @@ import * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { Client } from '@elastic/elasticsearch'; import { ToolingLog } from '@kbn/tooling-log'; +import { IndicesPutIndexTemplateRequest } from '@elastic/elasticsearch/lib/api/types'; import { Stats } from '../stats'; import { deleteKibanaIndices } from './kibana_index'; import { deleteIndex } from './delete_index'; +import { deleteDataStream } from './delete_data_stream'; import { ES_CLIENT_HEADERS } from '../../client_headers'; interface DocRecord { value: estypes.IndicesIndexState & { index: string; type: string; + template?: IndicesPutIndexTemplateRequest; }; } @@ -54,6 +57,43 @@ export function createCreateIndexStream({ stream.push(record); } + async function handleDataStream(record: DocRecord, attempts = 1) { + if (docsOnly) return; + + const { data_stream: dataStream, template } = record.value as { + data_stream: string; + template: IndicesPutIndexTemplateRequest; + }; + + try { + await client.indices.putIndexTemplate(template, { + headers: ES_CLIENT_HEADERS, + }); + + await client.indices.createDataStream( + { name: dataStream }, + { + headers: ES_CLIENT_HEADERS, + } + ); + stats.createdDataStream(dataStream, template.name, { template }); + } catch (err) { + if (err?.meta?.body?.error?.type !== 'resource_already_exists_exception' || attempts >= 3) { + throw err; + } + + if (skipExisting) { + skipDocsFromIndices.add(dataStream); + stats.skippedIndex(dataStream); + return; + } + + await deleteDataStream(client, dataStream, template.name); + stats.deletedDataStream(dataStream, template.name); + await handleDataStream(record, attempts + 1); + } + } + async function handleIndex(record: DocRecord) { const { index, settings, mappings, aliases } = record.value; const isKibanaTaskManager = index.startsWith('.kibana_task_manager'); @@ -134,6 +174,10 @@ export function createCreateIndexStream({ await handleIndex(record); break; + case 'data_stream': + await handleDataStream(record); + break; + case 'doc': await handleDoc(this, record); break; diff --git a/packages/kbn-es-archiver/src/lib/indices/delete_data_stream.ts b/packages/kbn-es-archiver/src/lib/indices/delete_data_stream.ts new file mode 100644 index 00000000000000..6aa68db4216f49 --- /dev/null +++ b/packages/kbn-es-archiver/src/lib/indices/delete_data_stream.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Client } from '@elastic/elasticsearch'; + +export async function deleteDataStream(client: Client, datastream: string, template: string) { + await client.indices.deleteDataStream({ name: datastream }); + await client.indices.deleteIndexTemplate({ name: template }); +} diff --git a/packages/kbn-es-archiver/src/lib/indices/delete_index_stream.test.ts b/packages/kbn-es-archiver/src/lib/indices/delete_index_stream.test.ts index 241d4a89445460..4917deab542d4a 100644 --- a/packages/kbn-es-archiver/src/lib/indices/delete_index_stream.test.ts +++ b/packages/kbn-es-archiver/src/lib/indices/delete_index_stream.test.ts @@ -16,6 +16,7 @@ import { createStubStats, createStubClient, createStubIndexRecord, + createStubDataStreamRecord, createStubLogger, } from './__mocks__/stubs'; @@ -51,4 +52,25 @@ describe('esArchiver: createDeleteIndexStream()', () => { sinon.assert.calledOnce(client.indices.delete as sinon.SinonSpy); sinon.assert.notCalled(client.indices.exists as sinon.SinonSpy); }); + + it('deletes data streams', async () => { + const stats = createStubStats(); + const client = createStubClient([]); + + await createPromiseFromStreams([ + createListStream([createStubDataStreamRecord('foo-datastream', 'foo-template')]), + createDeleteIndexStream(client, stats, log), + ]); + + sinon.assert.calledOnce(stats.deletedDataStream as sinon.SinonSpy); + sinon.assert.notCalled(client.indices.create as sinon.SinonSpy); + sinon.assert.calledOnce(client.indices.deleteDataStream as sinon.SinonSpy); + sinon.assert.calledWith(client.indices.deleteDataStream as sinon.SinonSpy, { + name: 'foo-datastream', + }); + sinon.assert.calledOnce(client.indices.deleteIndexTemplate as sinon.SinonSpy); + sinon.assert.calledWith(client.indices.deleteIndexTemplate as sinon.SinonSpy, { + name: 'foo-template', + }); + }); }); diff --git a/packages/kbn-es-archiver/src/lib/indices/delete_index_stream.ts b/packages/kbn-es-archiver/src/lib/indices/delete_index_stream.ts index 450d575181529b..c7633465ccc4cb 100644 --- a/packages/kbn-es-archiver/src/lib/indices/delete_index_stream.ts +++ b/packages/kbn-es-archiver/src/lib/indices/delete_index_stream.ts @@ -13,6 +13,7 @@ import { ToolingLog } from '@kbn/tooling-log'; import { Stats } from '../stats'; import { deleteIndex } from './delete_index'; import { cleanKibanaIndices } from './kibana_index'; +import { deleteDataStream } from './delete_data_stream'; export function createDeleteIndexStream(client: Client, stats: Stats, log: ToolingLog) { return new Transform({ @@ -20,7 +21,11 @@ export function createDeleteIndexStream(client: Client, stats: Stats, log: Tooli writableObjectMode: true, async transform(record, enc, callback) { try { - if (!record || record.type === 'index') { + if (!record) { + log.warning(`deleteIndexStream: empty index provided`); + return callback(); + } + if (record.type === 'index') { const { index } = record.value; if (index.startsWith('.kibana')) { @@ -28,6 +33,14 @@ export function createDeleteIndexStream(client: Client, stats: Stats, log: Tooli } else { await deleteIndex({ client, stats, log, index }); } + } else if (record.type === 'data_stream') { + const { + data_stream: dataStream, + template: { name }, + } = record.value; + + await deleteDataStream(client, dataStream, name); + stats.deletedDataStream(dataStream, name); } else { this.push(record); } diff --git a/packages/kbn-es-archiver/src/lib/indices/generate_index_records_stream.test.ts b/packages/kbn-es-archiver/src/lib/indices/generate_index_records_stream.test.ts index fbd351cea63a98..566760b0ddf883 100644 --- a/packages/kbn-es-archiver/src/lib/indices/generate_index_records_stream.test.ts +++ b/packages/kbn-es-archiver/src/lib/indices/generate_index_records_stream.test.ts @@ -9,10 +9,12 @@ import sinon from 'sinon'; import { createListStream, createPromiseFromStreams, createConcatStream } from '@kbn/utils'; -import { createStubClient, createStubStats } from './__mocks__/stubs'; +import { createStubClient, createStubLogger, createStubStats } from './__mocks__/stubs'; import { createGenerateIndexRecordsStream } from './generate_index_records_stream'; +const log = createStubLogger(); + describe('esArchiver: createGenerateIndexRecordsStream()', () => { it('consumes index names and queries for the mapping of each', async () => { const indices = ['index1', 'index2', 'index3', 'index4']; @@ -21,7 +23,7 @@ describe('esArchiver: createGenerateIndexRecordsStream()', () => { await createPromiseFromStreams([ createListStream(indices), - createGenerateIndexRecordsStream({ client, stats }), + createGenerateIndexRecordsStream({ client, stats, log }), ]); expect(stats.getTestSummary()).toEqual({ @@ -40,7 +42,7 @@ describe('esArchiver: createGenerateIndexRecordsStream()', () => { await createPromiseFromStreams([ createListStream(['index1']), - createGenerateIndexRecordsStream({ client, stats }), + createGenerateIndexRecordsStream({ client, stats, log }), ]); const params = (client.indices.get as sinon.SinonSpy).args[0][0]; @@ -58,7 +60,7 @@ describe('esArchiver: createGenerateIndexRecordsStream()', () => { const indexRecords = await createPromiseFromStreams([ createListStream(['index1', 'index2', 'index3']), - createGenerateIndexRecordsStream({ client, stats }), + createGenerateIndexRecordsStream({ client, stats, log }), createConcatStream([]), ]); @@ -83,7 +85,7 @@ describe('esArchiver: createGenerateIndexRecordsStream()', () => { const indexRecords = await createPromiseFromStreams([ createListStream(['index1']), - createGenerateIndexRecordsStream({ client, stats }), + createGenerateIndexRecordsStream({ client, stats, log }), createConcatStream([]), ]); @@ -107,7 +109,7 @@ describe('esArchiver: createGenerateIndexRecordsStream()', () => { const indexRecords = await createPromiseFromStreams([ createListStream(['.kibana_7.16.0_001']), - createGenerateIndexRecordsStream({ client, stats }), + createGenerateIndexRecordsStream({ client, stats, log }), createConcatStream([]), ]); @@ -122,7 +124,7 @@ describe('esArchiver: createGenerateIndexRecordsStream()', () => { const indexRecords = await createPromiseFromStreams([ createListStream(['.foo']), - createGenerateIndexRecordsStream({ client, stats }), + createGenerateIndexRecordsStream({ client, stats, log }), createConcatStream([]), ]); @@ -137,7 +139,7 @@ describe('esArchiver: createGenerateIndexRecordsStream()', () => { const indexRecords = await createPromiseFromStreams([ createListStream(['.kibana_7.16.0_001']), - createGenerateIndexRecordsStream({ client, stats, keepIndexNames: true }), + createGenerateIndexRecordsStream({ client, stats, log, keepIndexNames: true }), createConcatStream([]), ]); diff --git a/packages/kbn-es-archiver/src/lib/indices/generate_index_records_stream.ts b/packages/kbn-es-archiver/src/lib/indices/generate_index_records_stream.ts index e3efaa28516093..de32e93e273989 100644 --- a/packages/kbn-es-archiver/src/lib/indices/generate_index_records_stream.ts +++ b/packages/kbn-es-archiver/src/lib/indices/generate_index_records_stream.ts @@ -8,18 +8,28 @@ import type { Client } from '@elastic/elasticsearch'; import { Transform } from 'stream'; +import { ToolingLog } from '@kbn/tooling-log'; import { Stats } from '../stats'; import { ES_CLIENT_HEADERS } from '../../client_headers'; +import { getIndexTemplate } from '..'; + +const headers = { + headers: ES_CLIENT_HEADERS, +}; export function createGenerateIndexRecordsStream({ client, stats, keepIndexNames, + log, }: { client: Client; stats: Stats; keepIndexNames?: boolean; + log: ToolingLog; }) { + const seenDatastreams = new Set(); + return new Transform({ writableObjectMode: true, readableObjectMode: true, @@ -32,6 +42,7 @@ export function createGenerateIndexRecordsStream({ filter_path: [ '*.settings', '*.mappings', + '*.data_stream', // remove settings that aren't really settings '-*.settings.index.creation_date', '-*.settings.index.uuid', @@ -44,37 +55,58 @@ export function createGenerateIndexRecordsStream({ ], }, { - headers: ES_CLIENT_HEADERS, + ...headers, meta: true, } ) ).body; - for (const [index, { settings, mappings }] of Object.entries(resp)) { - const { - body: { - [index]: { aliases }, - }, - } = await client.indices.getAlias( - { index }, - { - headers: ES_CLIENT_HEADERS, - meta: true, + for (const [index, { data_stream: dataStream, settings, mappings }] of Object.entries( + resp + )) { + if (dataStream) { + log.info(`${index} will be saved as data_stream ${dataStream}`); + + if (seenDatastreams.has(dataStream)) { + log.info(`${dataStream} is already archived`); + continue; } - ); - stats.archivedIndex(index, { settings, mappings }); - this.push({ - type: 'index', - value: { - // if keepIndexNames is false, rewrite the .kibana_* index to .kibana_1 so that - // when it is loaded it can skip migration, if possible - index: index.startsWith('.kibana') && !keepIndexNames ? '.kibana_1' : index, - settings, - mappings, - aliases, - }, - }); + const { data_streams: dataStreams } = await client.indices.getDataStream( + { name: dataStream }, + headers + ); + const template = await getIndexTemplate(client, dataStreams[0].template); + + seenDatastreams.add(dataStream); + stats.archivedIndex(dataStream, { template }); + this.push({ + type: 'data_stream', + value: { + data_stream: dataStream, + template, + }, + }); + } else { + const { + body: { + [index]: { aliases }, + }, + } = await client.indices.getAlias({ index }, { ...headers, meta: true }); + + stats.archivedIndex(index, { settings, mappings }); + this.push({ + type: 'index', + value: { + // if keepIndexNames is false, rewrite the .kibana_* index to .kibana_1 so that + // when it is loaded it can skip migration, if possible + index: index.startsWith('.kibana') && !keepIndexNames ? '.kibana_1' : index, + settings, + mappings, + aliases, + }, + }); + } } callback(); diff --git a/packages/kbn-es-archiver/src/lib/records/filter_records_stream.test.ts b/packages/kbn-es-archiver/src/lib/records/filter_records_stream.test.ts index 506507ba0b9e6d..901664988d165d 100644 --- a/packages/kbn-es-archiver/src/lib/records/filter_records_stream.test.ts +++ b/packages/kbn-es-archiver/src/lib/records/filter_records_stream.test.ts @@ -26,7 +26,7 @@ describe('esArchiver: createFilterRecordsStream()', () => { }, chance.bool(), ]), - createFilterRecordsStream('type'), + createFilterRecordsStream((record) => record.type === 'type'), createConcatStream([]), ]); @@ -45,7 +45,7 @@ describe('esArchiver: createFilterRecordsStream()', () => { { type: chance.word({ length: 10 }), value: {} }, { type: chance.word({ length: 10 }), value: {} }, ]), - createFilterRecordsStream(type1), + createFilterRecordsStream((record) => record.type === type1), createConcatStream([]), ]); diff --git a/packages/kbn-es-archiver/src/lib/records/filter_records_stream.ts b/packages/kbn-es-archiver/src/lib/records/filter_records_stream.ts index 69ab06454f93b4..9ded38a6f2b58b 100644 --- a/packages/kbn-es-archiver/src/lib/records/filter_records_stream.ts +++ b/packages/kbn-es-archiver/src/lib/records/filter_records_stream.ts @@ -8,13 +8,13 @@ import { Transform } from 'stream'; -export function createFilterRecordsStream(type: string) { +export function createFilterRecordsStream(fn: (record: any) => boolean) { return new Transform({ writableObjectMode: true, readableObjectMode: true, transform(record, enc, callback) { - if (record && record.type === type) { + if (record && fn(record)) { callback(undefined, record); } else { callback(); diff --git a/packages/kbn-es-archiver/src/lib/stats.ts b/packages/kbn-es-archiver/src/lib/stats.ts index 9ff16d57b8661c..1b533a18acadef 100644 --- a/packages/kbn-es-archiver/src/lib/stats.ts +++ b/packages/kbn-es-archiver/src/lib/stats.ts @@ -83,6 +83,15 @@ export function createStats(name: string, log: ToolingLog) { info('Deleted existing index %j', index); } + /** + * Record that a data stream was deleted + * @param index + */ + public deletedDataStream(stream: string, template: string) { + getOrCreate(stream).deleted = true; + info('Deleted existing data stream %j with index template %j', stream, template); + } + /** * Record that an index was created * @param index @@ -95,6 +104,18 @@ export function createStats(name: string, log: ToolingLog) { }); } + /** + * Record that a data stream was created + * @param index + */ + public createdDataStream(stream: string, template: string, metadata: Record = {}) { + getOrCreate(stream).created = true; + info('Created data stream %j with index template %j', stream, template); + Object.keys(metadata).forEach((key) => { + debug('%j %s %j', stream, key, metadata[key]); + }); + } + /** * Record that an index was written to the archives * @param index diff --git a/packages/kbn-es/BUILD.bazel b/packages/kbn-es/BUILD.bazel index 2ea9c32858dd3e..892cd43244de77 100644 --- a/packages/kbn-es/BUILD.bazel +++ b/packages/kbn-es/BUILD.bazel @@ -24,7 +24,6 @@ filegroup( NPM_MODULE_EXTRA_FILES = [ "package.json", - "README.md", ] RUNTIME_DEPS = [ diff --git a/packages/kbn-es/README.md b/packages/kbn-es/README.mdx similarity index 70% rename from packages/kbn-es/README.md rename to packages/kbn-es/README.mdx index 80850c9e6a09cc..a5392504490fec 100644 --- a/packages/kbn-es/README.md +++ b/packages/kbn-es/README.mdx @@ -1,6 +1,13 @@ -# @kbn/es +--- +id: kibDevDocsOpsEs +slug: /kibana-dev-docs/ops/es +title: "@kbn/es" +description: A cli package for running elasticsearch or building snapshot artifacts +date: 2022-05-24 +tags: ['kibana', 'dev', 'contributor', 'operations', 'es'] +--- -> A command line utility for running elasticsearch from source or archive. +> A command line utility for running elasticsearch from snapshot, source, archive or even building snapshot artifacts. ## Getting started If running elasticsearch from source, elasticsearch needs to be cloned to a sibling directory of Kibana. @@ -71,41 +78,20 @@ To use these steps you'll need to setup the google-cloud-sdk, which can be insta 1. Clone the elasticsearch repo somewhere 2. Checkout the branch you want to build - 3. Run the following to delete old distributables + 3. Build the new artifacts ``` - find distribution/archives -type f \( -name 'elasticsearch-*-*.tar.gz' -o -name 'elasticsearch-*-*.zip' \) -not -path *no-jdk* -exec rm {} \; + node scripts/es build_snapshots --output=~/Downloads/tmp-artifacts --source-path=/path/to/es/repo ``` - 4. Build the new artifacts - - ``` - ./gradlew -p distribution/archives assemble --parallel - ``` - - 4. Copy new artifacts to your `~/Downloads/tmp-artifacts` - - ``` - rm -rf ~/Downloads/tmp-artifacts - mkdir ~/Downloads/tmp-artifacts - find distribution/archives -type f \( -name 'elasticsearch-*-*.tar.gz' -o -name 'elasticsearch-*-*.zip' \) -not -path *no-jdk* -exec cp {} ~/Downloads/tmp-artifacts \; - ``` - - 5. Calculate shasums of the uploads - - ``` - cd ~/Downloads/tmp-artifacts - find * -exec bash -c "shasum -a 512 {} > {}.sha512" \; - ``` - - 6. Check that the files in `~/Downloads/tmp-artifacts` look reasonable - 7. Upload the files to GCS + 4. Check that the files in `~/Downloads/tmp-artifacts` look reasonable + 5. Upload the files to GCS ``` gsutil -m rsync . gs://kibana-ci-tmp-artifacts/ ``` - 8. Once the artifacts are uploaded, modify `packages/kbn-es/src/custom_snapshots.js` in a PR to use a URL formatted like: + 6. Once the artifacts are uploaded, modify `packages/kbn-es/src/custom_snapshots.js` in a PR to use a URL formatted like: ``` // force use of manually created snapshots until ReindexPutMappings fix diff --git a/packages/kbn-es/src/cli_commands/build_snapshots.js b/packages/kbn-es/src/cli_commands/build_snapshots.js index 070f11b8b5f84d..b4a15a0645ccea 100644 --- a/packages/kbn-es/src/cli_commands/build_snapshots.js +++ b/packages/kbn-es/src/cli_commands/build_snapshots.js @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +const dedent = require('dedent'); const { resolve, basename } = require('path'); const { createHash } = require('crypto'); const { promisify } = require('util'); @@ -21,7 +22,16 @@ const pipelineAsync = promisify(pipeline); exports.description = 'Build and collect ES snapshots'; -exports.help = () => ``; +exports.help = () => dedent` + Options: + + --output Path to create the built elasticsearch snapshots + --source-path Path where the elasticsearch repository is checked out + + Example: + + es build_snapshots --source-path=/path/to/es/checked/repo --output=/tmp/es-built-snapshots + `; exports.run = async (defaults = {}) => { const argv = process.argv.slice(2); diff --git a/src/plugins/data_views/common/data_views/data_views.ts b/src/plugins/data_views/common/data_views/data_views.ts index d0f2dc936b68e9..18142ab19070b4 100644 --- a/src/plugins/data_views/common/data_views/data_views.ts +++ b/src/plugins/data_views/common/data_views/data_views.ts @@ -753,7 +753,6 @@ export class DataViewsService { * Get an index pattern by id, cache optimized. * @param id */ - get = async (id: string): Promise => { const indexPatternPromise = this.dataViewCache.get(id) || this.dataViewCache.set(id, this.getSavedObjectAndInit(id)); diff --git a/src/plugins/data_views/server/mocks.ts b/src/plugins/data_views/server/mocks.ts index 42d1a30f0d50f1..82595f7dc51a1b 100644 --- a/src/plugins/data_views/server/mocks.ts +++ b/src/plugins/data_views/server/mocks.ts @@ -28,4 +28,5 @@ export const dataViewsService = { getDefaultId: jest.fn(), updateSavedObject: jest.fn(), refreshFields: jest.fn(), + getIdsWithTitle: jest.fn(), } as unknown as jest.Mocked; diff --git a/src/plugins/data_views/server/rest_api_routes/get_data_views.test.ts b/src/plugins/data_views/server/rest_api_routes/get_data_views.test.ts new file mode 100644 index 00000000000000..216fe58693965a --- /dev/null +++ b/src/plugins/data_views/server/rest_api_routes/get_data_views.test.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getDataViews } from './get_data_views'; +import { dataViewsService } from '../mocks'; +import { getUsageCollection } from './test_utils'; + +describe('get all data views', () => { + it('call usageCollection', () => { + const usageCollection = getUsageCollection(); + getDataViews({ + dataViewsService, + counterName: 'GET /path', + usageCollection, + }); + expect(usageCollection.incrementCounter).toBeCalledTimes(1); + }); +}); diff --git a/src/plugins/data_views/server/rest_api_routes/get_data_views.ts b/src/plugins/data_views/server/rest_api_routes/get_data_views.ts new file mode 100644 index 00000000000000..f7a77d7e5c8d18 --- /dev/null +++ b/src/plugins/data_views/server/rest_api_routes/get_data_views.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { UsageCounter } from '@kbn/usage-collection-plugin/server'; +import { IRouter, StartServicesAccessor } from '@kbn/core/server'; +import { DataViewsService } from '../../common'; +import { handleErrors } from './util/handle_errors'; +import type { DataViewsServerPluginStartDependencies, DataViewsServerPluginStart } from '../types'; +import { SERVICE_KEY, SERVICE_PATH } from '../constants'; + +interface GetDataViewsArgs { + dataViewsService: DataViewsService; + usageCollection?: UsageCounter; + counterName: string; +} + +export const getDataViews = async ({ + dataViewsService, + usageCollection, + counterName, +}: GetDataViewsArgs) => { + usageCollection?.incrementCounter({ counterName }); + return dataViewsService.getIdsWithTitle(); +}; + +const getDataViewsRouteFactory = + (path: string, serviceKey: string) => + ( + router: IRouter, + getStartServices: StartServicesAccessor< + DataViewsServerPluginStartDependencies, + DataViewsServerPluginStart + >, + usageCollection?: UsageCounter + ) => { + router.get( + { + path, + validate: {}, + }, + router.handleLegacyErrors( + handleErrors(async (ctx, req, res) => { + const core = await ctx.core; + const savedObjectsClient = core.savedObjects.client; + const elasticsearchClient = core.elasticsearch.client.asCurrentUser; + const [, , { dataViewsServiceFactory }] = await getStartServices(); + const dataViewsService = await dataViewsServiceFactory( + savedObjectsClient, + elasticsearchClient, + req + ); + + const dataViews = await getDataViews({ + dataViewsService, + usageCollection, + counterName: `${req.route.method} ${path}`, + }); + + return res.ok({ + headers: { + 'content-type': 'application/json', + }, + body: { + [serviceKey]: dataViews, + }, + }); + }) + ) + ); + }; + +export const registerGetDataViewsRoute = getDataViewsRouteFactory(SERVICE_PATH, SERVICE_KEY); diff --git a/src/plugins/data_views/server/rest_api_routes/index.ts b/src/plugins/data_views/server/rest_api_routes/index.ts index 3ed0ac6608e1ea..812cda62ac1efc 100644 --- a/src/plugins/data_views/server/rest_api_routes/index.ts +++ b/src/plugins/data_views/server/rest_api_routes/index.ts @@ -14,6 +14,7 @@ import * as createRoutes from './create_data_view'; import * as defaultRoutes from './default_data_view'; import * as deleteRoutes from './delete_data_view'; import * as getRoutes from './get_data_view'; +import * as getAllRoutes from './get_data_views'; import * as hasRoutes from './has_user_data_view'; import * as updateRoutes from './update_data_view'; @@ -38,6 +39,7 @@ const routes = [ deleteRoutes.registerDeleteDataViewRouteLegacy, getRoutes.registerGetDataViewRoute, getRoutes.registerGetDataViewRouteLegacy, + getAllRoutes.registerGetDataViewsRoute, hasRoutes.registerHasUserDataViewRoute, hasRoutes.registerHasUserDataViewRouteLegacy, updateRoutes.registerUpdateDataViewRoute, diff --git a/src/plugins/visualizations/public/vis.scss b/src/plugins/visualizations/public/vis.scss index 42b59b8de93cd0..42ffab11a1edaa 100644 --- a/src/plugins/visualizations/public/vis.scss +++ b/src/plugins/visualizations/public/vis.scss @@ -8,7 +8,6 @@ // SASSTODO: Too risky to change to BEM naming .visualization { display: flex; - flex-direction: column; width: 100%; height: 100%; overflow: auto; diff --git a/test/api_integration/apis/index_patterns/constants.ts b/test/api_integration/apis/index_patterns/constants.ts index 8194966bcdcb81..eaec583f72d59a 100644 --- a/test/api_integration/apis/index_patterns/constants.ts +++ b/test/api_integration/apis/index_patterns/constants.ts @@ -22,7 +22,7 @@ const legacyConfig = { serviceKey: SERVICE_KEY_LEGACY, }; -const dataViewConfig = { +export const dataViewConfig = { name: 'data view api', path: DATA_VIEW_PATH, basePath: SERVICE_PATH, diff --git a/test/api_integration/apis/index_patterns/index_pattern_crud/get_data_views/index.ts b/test/api_integration/apis/index_patterns/index_pattern_crud/get_data_views/index.ts new file mode 100644 index 00000000000000..60c5ae1dc09353 --- /dev/null +++ b/test/api_integration/apis/index_patterns/index_pattern_crud/get_data_views/index.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { FtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('get_data_views', () => { + loadTestFile(require.resolve('./main')); + }); +} diff --git a/test/api_integration/apis/index_patterns/index_pattern_crud/get_data_views/main.ts b/test/api_integration/apis/index_patterns/index_pattern_crud/get_data_views/main.ts new file mode 100644 index 00000000000000..cce2da6cd89f90 --- /dev/null +++ b/test/api_integration/apis/index_patterns/index_pattern_crud/get_data_views/main.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { dataViewConfig } from '../../constants'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + + describe('main', () => { + describe('get data views api', () => { + it('returns list of data views', async () => { + const response = await supertest.get(dataViewConfig.basePath); + expect(response.status).to.be(200); + expect(response.body).to.have.property(dataViewConfig.serviceKey); + expect(response.body[dataViewConfig.serviceKey]).to.be.an('array'); + }); + }); + }); +} diff --git a/test/api_integration/apis/index_patterns/index_pattern_crud/index.ts b/test/api_integration/apis/index_patterns/index_pattern_crud/index.ts index 81d605d217b54b..158fe3087bcbe5 100644 --- a/test/api_integration/apis/index_patterns/index_pattern_crud/index.ts +++ b/test/api_integration/apis/index_patterns/index_pattern_crud/index.ts @@ -14,5 +14,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./get_index_pattern')); loadTestFile(require.resolve('./delete_index_pattern')); loadTestFile(require.resolve('./update_index_pattern')); + loadTestFile(require.resolve('./get_data_views')); }); } diff --git a/x-pack/plugins/cases/README.md b/x-pack/plugins/cases/README.md index 908428673c30d7..98eb504ff7094f 100644 --- a/x-pack/plugins/cases/README.md +++ b/x-pack/plugins/cases/README.md @@ -16,17 +16,12 @@ This plugin provides cases management in Kibana ## Table of Contents - [Cases API](#cases-api) -- [Cases Client API](#cases-client-api) - [Cases UI](#cases-ui) ## Cases API [**Explore the API docs »**](https://www.elastic.co/guide/en/security/current/cases-api-overview.html) -## Cases Client API - -[**Cases Client API docs**][cases-client-api-docs] - ## Cases UI ### Embed Cases UI components in any Kibana plugin @@ -133,9 +128,79 @@ An array of: | id | The ID of the case | string | | title | The title of the case | string | -### ui +#### `find` -#### `getCases` +Retrieves a paginated subset of cases. + +Arguments + +| Property | Description | Type | +| -------- | ---------------------- | --------------------- | +| query | The request parameters | object | +| signal | The abort signal | Optional, AbortSignal | + +`query` + +| Property | Description | Type | +| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ | +| defaultSearchOperator | The default operator to use for the `simple_query_string`. Defaults to `OR`. | Optional, string | +| fields | The fields in the entity to return in the response. | Optional, array of strings | +| from | Returns only cases that were created after a specific date. The date must be specified as a KQL data range or date match expression. | Optional, string | +| owner | A filter to limit the retrieved cases to a specific set of applications. Valid values are: `cases`, `observability`, and `securitySolution`. If this parameter is omitted, the response contains all cases that the user has access to read. | +| page | The page number to return. Defaults to `1` . | Optional, integer | +| perPage | The number of rules to return per page. Defaults to `20` . | Optional, integer | +| reporters | Filters the returned cases by the reporter's `username. | Optional, string or array of strings | +| search | `simple_query_string` query that filters the objects in the response. | Optional, string | +| searchFields | The fields to perform the `simple_query_string` parsed query against. | Optional, string or array of strings | +| severity | The severity of the case. Valid values are: `critical`, `high`, `low`, and `medium`. | Optional, string | +| sortField | Determines which field is used to sort the results,`createdAt` or `updatedAt`. Defaults to `createdAt`. | Optional, string | +| sortOrder | Determines the sort order, which can be `desc` or `asc`. Defaults to `desc`. | Optional, string | +| status | Filters the returned cases by state, which can be `open`, `in-progress`, or `closed`. | Optional, string | +| tags | Filters the returned cases by tags. | Optional, string or array of strings | +| to | Returns only cases that were created before a specific date. The date must be specified as a KQL data range or date match expression. | Optional, string | + +#### `getCasesStatus` + +Returns the number of cases that are open, closed, and in progress. + +Arguments + +| Property | Description | Type | +| -------- | ---------------------- | --------------------- | +| query | The request parameters | object | +| signal | The abort signal | Optional, AbortSignal | + +`query` + +| Property | Description | Type | +| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | +| from | Returns only cases that were created after a specific date. The date must be specified as a KQL data range or date match expression. | Optional, string | +| owner | A filter to limit the retrieved cases to a specific set of applications. Valid values are: `cases`, `observability`, and `securitySolution`. If this parameter is omitted, the response contains all cases that the user has access to read. | +| to | Returns only cases that were created before a specific date. The date must be specified as a KQL data range or date match expression. | Optional, string | + + +#### `getCasesMetrics` + +Returns the number of cases that are open, closed, and in progress. + +Arguments + +| Property | Description | Type | +| -------- | ---------------------- | --------------------- | +| query | The request parameters | object | +| signal | The abort signal | Optional, AbortSignal | + +`query` + +| Property | Description | Type | +| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | +| features | The metrics to retrieve. | Optional, array of strings | +| from | Returns only cases that were created after a specific date. The date must be specified as a KQL data range or date match expression. | Optional, string | +| owner | A filter to limit the retrieved cases to a specific set of applications. Valid values are: `cases`, `observability`, and `securitySolution`. If this parameter is omitted, the response contains all cases that the user has access to read. | +| to | Returns only cases that were created before a specific date. The date must be specified as a KQL data range or date match expression. | Optional, string | + + +### ui Arguments: @@ -160,6 +225,7 @@ Arguments: | timelineIntegration?.hooks.useInsertTimeline | `(value: string, onChange: (newValue: string) => void): UseInsertTimelineReturn` | | timelineIntegration?.ui?.renderInvestigateInTimelineActionComponent? | `(alertIds: string[]) => JSX.Element;` space to render `InvestigateInTimelineActionComponent` | | timelineIntegration?.ui?renderTimelineDetailsPanel? | `() => JSX.Element;` space to render `TimelineDetailsPanel` | +#### `getCases` UI component: ![All Cases Component][all-cases-img] @@ -284,4 +350,3 @@ Arguments: [all-cases-modal-img]: images/all_cases_selector_modal.png [recent-cases-img]: images/recent_cases.png [case-view-img]: images/case_view.png -[cases-client-api-docs]: docs/cases_client/README.md diff --git a/x-pack/plugins/cases/docs/openapi/bundled.json b/x-pack/plugins/cases/docs/openapi/bundled.json index ddb8ba7d89ea3a..100aec3566d442 100644 --- a/x-pack/plugins/cases/docs/openapi/bundled.json +++ b/x-pack/plugins/cases/docs/openapi/bundled.json @@ -120,25 +120,28 @@ "type": "string" } }, - "required": [ - "fields", - "id", - "name", - "type" - ] + "example": null }, "id": { "description": "The identifier for the connector. To create a case without a connector, use `none`.", - "type": "string" + "type": "string", + "example": "none" }, "name": { "description": "The name of the connector. To create a case without a connector, use `none`.", - "type": "string" + "type": "string", + "example": "none" }, "type": { "$ref": "#/components/schemas/connector_types" } - } + }, + "required": [ + "fields", + "id", + "name", + "type" + ] }, "description": { "description": "The description for the case.", @@ -298,20 +301,17 @@ "type": "string" } }, - "required": [ - "fields", - "id", - "name", - "type" - ] + "example": null }, "id": { "description": "The identifier for the connector. To create a case without a connector, use `none`.", - "type": "string" + "type": "string", + "example": "none" }, "name": { "description": "The name of the connector. To create a case without a connector, use `none`.", - "type": "string" + "type": "string", + "example": "none" }, "type": { "$ref": "#/components/schemas/connector_types" @@ -328,15 +328,15 @@ "properties": { "email": { "type": "string", - "example": "ahunley@imf.usa.gov" + "example": null }, "full_name": { "type": "string", - "example": "Alan Hunley" + "example": null }, "username": { "type": "string", - "example": "ahunley" + "example": "elastic" } } }, @@ -604,25 +604,28 @@ "type": "string" } }, - "required": [ - "fields", - "id", - "name", - "type" - ] + "example": null }, "id": { "description": "The identifier for the connector. To create a case without a connector, use `none`.", - "type": "string" + "type": "string", + "example": "none" }, "name": { "description": "The name of the connector. To create a case without a connector, use `none`.", - "type": "string" + "type": "string", + "example": "none" }, "type": { "$ref": "#/components/schemas/connector_types" } - } + }, + "required": [ + "fields", + "id", + "name", + "type" + ] }, "description": { "description": "The description for the case.", @@ -789,20 +792,17 @@ "type": "string" } }, - "required": [ - "fields", - "id", - "name", - "type" - ] + "example": null }, "id": { "description": "The identifier for the connector. To create a case without a connector, use `none`.", - "type": "string" + "type": "string", + "example": "none" }, "name": { "description": "The name of the connector. To create a case without a connector, use `none`.", - "type": "string" + "type": "string", + "example": "none" }, "type": { "$ref": "#/components/schemas/connector_types" @@ -819,15 +819,15 @@ "properties": { "email": { "type": "string", - "example": "ahunley@imf.usa.gov" + "example": null }, "full_name": { "type": "string", - "example": "Alan Hunley" + "example": null }, "username": { "type": "string", - "example": "ahunley" + "example": "elastic" } } }, @@ -1011,7 +1011,7 @@ "type": "string" }, "example": "now-1d", - "x-preview": true + "x-technical-preview": true }, { "$ref": "#/components/parameters/owner" @@ -1153,7 +1153,7 @@ "type": "string" }, "example": "now%2B1d", - "x-preview": true + "x-technical-preview": true } ], "responses": { @@ -1270,20 +1270,17 @@ "type": "string" } }, - "required": [ - "fields", - "id", - "name", - "type" - ] + "example": null }, "id": { "description": "The identifier for the connector. To create a case without a connector, use `none`.", - "type": "string" + "type": "string", + "example": "none" }, "name": { "description": "The name of the connector. To create a case without a connector, use `none`.", - "type": "string" + "type": "string", + "example": "none" }, "type": { "$ref": "#/components/schemas/connector_types" @@ -1300,15 +1297,15 @@ "properties": { "email": { "type": "string", - "example": "ahunley@imf.usa.gov" + "example": null }, "full_name": { "type": "string", - "example": "Alan Hunley" + "example": null }, "username": { "type": "string", - "example": "ahunley" + "example": "elastic" } } }, @@ -1476,19 +1473,282 @@ } ] }, - "/s/{spaceId}/api/cases": { - "post": { - "description": "Creates a case. You must have all privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the case you're creating.\n", + "/api/cases/alerts/{alertId}": { + "get": { + "description": "Returns the cases associated with a specific alert. You must have read privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the cases you're seeking.\n", + "x-technical-preview": true, "tags": [ "cases", "kibana" ], "parameters": [ { - "$ref": "#/components/parameters/kbn_xsrf" + "$ref": "#/components/parameters/alert_id" }, { - "$ref": "#/components/parameters/space_id" + "$ref": "#/components/parameters/owner" + } + ], + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json; charset=utf-8": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The case identifier." + }, + "title": { + "type": "string", + "description": "The case title." + } + } + }, + "example": [ + { + "id": "06116b80-e1c3-11ec-be9b-9b1838238ee6", + "title": "security_case" + } + ] + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/api/cases/configure": { + "get": { + "description": "Retrieves external connection details, such as the closure type and default connector for cases. You must have read privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the case configuration.\n", + "tags": [ + "cases", + "kibana" + ], + "parameters": [ + { + "$ref": "#/components/parameters/owner" + } + ], + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json; charset=utf-8": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "closure_type": { + "$ref": "#/components/schemas/closure_types" + }, + "connector": { + "type": "object", + "properties": { + "fields": { + "description": "An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value.", + "nullable": true, + "type": "object", + "properties": { + "caseId": { + "description": "The case identifier for Swimlane connectors.", + "type": "string" + }, + "category": { + "description": "The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors.", + "type": "string" + }, + "destIp": { + "description": "A comma-separated list of destination IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "impact": { + "description": "The effect an incident had on business for ServiceNow ITSM connectors.", + "type": "string" + }, + "issueType": { + "description": "The type of issue for Jira connectors.", + "type": "string" + }, + "issueTypes": { + "description": "The type of incident for IBM Resilient connectors.", + "type": "array", + "items": { + "type": "number" + } + }, + "malwareHash": { + "description": "A comma-separated list of malware hashes for ServiceNow SecOps connectors.", + "type": "string" + }, + "malwareUrl": { + "description": "A comma-separated list of malware URLs for ServiceNow SecOps connectors.", + "type": "string" + }, + "parent": { + "description": "The key of the parent issue, when the issue type is sub-task for Jira connectors.", + "type": "string" + }, + "priority": { + "description": "The priority of the issue for Jira and ServiceNow SecOps connectors.", + "type": "string" + }, + "severity": { + "description": "The severity of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "severityCode": { + "description": "The severity code of the incident for IBM Resilient connectors.", + "type": "number" + }, + "sourceIp": { + "description": "A comma-separated list of source IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "subcategory": { + "description": "The subcategory of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "urgency": { + "description": "The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors.", + "type": "string" + } + }, + "example": null + }, + "id": { + "description": "The identifier for the connector. To create a case without a connector, use `none`.", + "type": "string", + "example": "none" + }, + "name": { + "description": "The name of the connector. To create a case without a connector, use `none`.", + "type": "string", + "example": "none" + }, + "type": { + "$ref": "#/components/schemas/connector_types" + } + } + }, + "created_at": { + "type": "string", + "format": "date-time", + "example": "2022-06-01T17:07:17.767Z" + }, + "created_by": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": null + }, + "full_name": { + "type": "string", + "example": null + }, + "username": { + "type": "string", + "example": "elastic" + } + } + }, + "error": { + "type": "string", + "example": null + }, + "id": { + "type": "string", + "example": "4a97a440-e1cd-11ec-be9b-9b1838238ee6" + }, + "mappings": { + "type": "array", + "items": { + "type": "object", + "properties": { + "action_type": { + "type": "string", + "example": "overwrite" + }, + "source": { + "type": "string", + "example": "title" + }, + "target": { + "type": "string", + "example": "summary" + } + } + } + }, + "owner": { + "$ref": "#/components/schemas/owners" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "example": "2022-06-01T19:58:48.169Z" + }, + "updated_by": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": null + }, + "full_name": { + "type": "string", + "example": null + }, + "username": { + "type": "string", + "example": "elastic" + } + }, + "nullable": true + }, + "version": { + "type": "string", + "example": "WzIwNzMsMV0=" + } + } + } + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "post": { + "description": "Sets external connection details, such as the closure type and default connector for cases. You must have all privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the case configuration. Connectors are used to interface with external systems. You must create a connector before you can use it in your cases. Refer to the add connectors API. If you set a default connector, it is automatically selected when you create cases in Kibana. If you use the create case API, however, you must still specify all of the connector details.\n", + "tags": [ + "cases", + "kibana" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" } ], "requestBody": { @@ -1497,6 +1757,9 @@ "schema": { "type": "object", "properties": { + "closure_type": { + "$ref": "#/components/schemas/closure_types" + }, "connector": { "description": "An object that contains the connector configuration.", "type": "object", @@ -1570,29 +1833,28 @@ "type": "string" } }, - "required": [ - "fields", - "id", - "name", - "type" - ] + "example": null }, "id": { "description": "The identifier for the connector. To create a case without a connector, use `none`.", - "type": "string" + "type": "string", + "example": "none" }, "name": { "description": "The name of the connector. To create a case without a connector, use `none`.", - "type": "string" + "type": "string", + "example": "none" }, "type": { "$ref": "#/components/schemas/connector_types" } - } - }, - "description": { - "description": "The description for the case.", - "type": "string" + }, + "required": [ + "fields", + "id", + "name", + "type" + ] }, "owner": { "$ref": "#/components/schemas/owners" @@ -1603,38 +1865,20 @@ "properties": { "syncAlerts": { "description": "Turns alert syncing on or off.", - "type": "boolean" + "type": "boolean", + "example": true } - } - }, - "severity": { - "$ref": "#/components/schemas/severity_property" - }, - "tags": { - "description": "The words and phrases that help categorize cases. It can be an empty array.", - "type": "array", - "items": { - "type": "string" - } - }, - "title": { - "description": "A title for the case.", - "type": "string" + }, + "required": [ + "syncAlerts" + ] } }, "required": [ + "closure_type", "connector", - "description", - "owner", - "settings", - "tags", - "title" + "owner" ] - }, - "examples": { - "createCaseRequest": { - "$ref": "#/components/examples/create_case_request" - } } } } @@ -1645,277 +1889,185 @@ "content": { "application/json; charset=utf-8": { "schema": { - "type": "object", - "properties": { - "closed_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "example": null - }, - "closed_by": { - "type": "object", - "properties": { - "email": { - "type": "string" - }, - "full_name": { - "type": "string" - }, - "username": { - "type": "string" - } - }, - "nullable": true, - "example": null - }, - "comments": { - "type": "array", - "items": { - "type": "string" + "type": "array", + "items": { + "type": "object", + "properties": { + "closure_type": { + "$ref": "#/components/schemas/closure_types" }, - "example": [] - }, - "connector": { - "type": "object", - "properties": { - "fields": { - "description": "An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value.", - "nullable": true, - "type": "object", - "properties": { - "caseId": { - "description": "The case identifier for Swimlane connectors.", - "type": "string" - }, - "category": { - "description": "The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors.", - "type": "string" - }, - "destIp": { - "description": "A comma-separated list of destination IPs for ServiceNow SecOps connectors.", - "type": "string" - }, - "impact": { - "description": "The effect an incident had on business for ServiceNow ITSM connectors.", - "type": "string" - }, - "issueType": { - "description": "The type of issue for Jira connectors.", - "type": "string" - }, - "issueTypes": { - "description": "The type of incident for IBM Resilient connectors.", - "type": "array", - "items": { + "connector": { + "type": "object", + "properties": { + "fields": { + "description": "An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value.", + "nullable": true, + "type": "object", + "properties": { + "caseId": { + "description": "The case identifier for Swimlane connectors.", + "type": "string" + }, + "category": { + "description": "The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors.", + "type": "string" + }, + "destIp": { + "description": "A comma-separated list of destination IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "impact": { + "description": "The effect an incident had on business for ServiceNow ITSM connectors.", + "type": "string" + }, + "issueType": { + "description": "The type of issue for Jira connectors.", + "type": "string" + }, + "issueTypes": { + "description": "The type of incident for IBM Resilient connectors.", + "type": "array", + "items": { + "type": "number" + } + }, + "malwareHash": { + "description": "A comma-separated list of malware hashes for ServiceNow SecOps connectors.", + "type": "string" + }, + "malwareUrl": { + "description": "A comma-separated list of malware URLs for ServiceNow SecOps connectors.", + "type": "string" + }, + "parent": { + "description": "The key of the parent issue, when the issue type is sub-task for Jira connectors.", + "type": "string" + }, + "priority": { + "description": "The priority of the issue for Jira and ServiceNow SecOps connectors.", + "type": "string" + }, + "severity": { + "description": "The severity of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "severityCode": { + "description": "The severity code of the incident for IBM Resilient connectors.", "type": "number" + }, + "sourceIp": { + "description": "A comma-separated list of source IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "subcategory": { + "description": "The subcategory of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "urgency": { + "description": "The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors.", + "type": "string" } }, - "malwareHash": { - "description": "A comma-separated list of malware hashes for ServiceNow SecOps connectors.", - "type": "string" - }, - "malwareUrl": { - "description": "A comma-separated list of malware URLs for ServiceNow SecOps connectors.", - "type": "string" - }, - "parent": { - "description": "The key of the parent issue, when the issue type is sub-task for Jira connectors.", - "type": "string" - }, - "priority": { - "description": "The priority of the issue for Jira and ServiceNow SecOps connectors.", - "type": "string" - }, - "severity": { - "description": "The severity of the incident for ServiceNow ITSM connectors.", - "type": "string" - }, - "severityCode": { - "description": "The severity code of the incident for IBM Resilient connectors.", - "type": "number" - }, - "sourceIp": { - "description": "A comma-separated list of source IPs for ServiceNow SecOps connectors.", - "type": "string" - }, - "subcategory": { - "description": "The subcategory of the incident for ServiceNow ITSM connectors.", - "type": "string" - }, - "urgency": { - "description": "The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors.", - "type": "string" - } + "example": null }, - "required": [ - "fields", - "id", - "name", - "type" - ] - }, - "id": { - "description": "The identifier for the connector. To create a case without a connector, use `none`.", - "type": "string" - }, - "name": { - "description": "The name of the connector. To create a case without a connector, use `none`.", - "type": "string" - }, - "type": { - "$ref": "#/components/schemas/connector_types" + "id": { + "description": "The identifier for the connector. To create a case without a connector, use `none`.", + "type": "string", + "example": "none" + }, + "name": { + "description": "The name of the connector. To create a case without a connector, use `none`.", + "type": "string", + "example": "none" + }, + "type": { + "$ref": "#/components/schemas/connector_types" + } } - } - }, - "created_at": { - "type": "string", - "format": "date-time", - "example": "2022-05-13T09:16:17.416Z" - }, - "created_by": { - "type": "object", - "properties": { - "email": { - "type": "string", - "example": "ahunley@imf.usa.gov" - }, - "full_name": { - "type": "string", - "example": "Alan Hunley" - }, - "username": { - "type": "string", - "example": "ahunley" + }, + "created_at": { + "type": "string", + "format": "date-time", + "example": "2022-06-01T17:07:17.767Z" + }, + "created_by": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": null + }, + "full_name": { + "type": "string", + "example": null + }, + "username": { + "type": "string", + "example": "elastic" + } } - } - }, - "description": { - "type": "string", - "example": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active" - }, - "duration": { - "type": "integer", - "description": "The elapsed time from the creation of the case to its closure (in seconds). If the case has not been closed, the duration is set to null.", - "example": 120 - }, - "external_service": { - "type": "object", - "properties": { - "connector_id": { - "type": "string" - }, - "connector_name": { - "type": "string" - }, - "external_id": { - "type": "string" - }, - "external_title": { - "type": "string" - }, - "external_url": { - "type": "string" - }, - "pushed_at": { - "type": "string", - "format": "date-time" - }, - "pushed_by": { + }, + "error": { + "type": "string", + "example": null + }, + "id": { + "type": "string", + "example": "4a97a440-e1cd-11ec-be9b-9b1838238ee6" + }, + "mappings": { + "type": "array", + "items": { "type": "object", "properties": { - "email": { - "type": "string" + "action_type": { + "type": "string", + "example": "overwrite" }, - "full_name": { - "type": "string" + "source": { + "type": "string", + "example": "title" }, - "username": { - "type": "string" + "target": { + "type": "string", + "example": "summary" } - }, - "nullable": true, - "example": null + } } + }, + "owner": { + "$ref": "#/components/schemas/owners" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "example": "2022-06-01T19:58:48.169Z" + }, + "updated_by": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": null + }, + "full_name": { + "type": "string", + "example": null + }, + "username": { + "type": "string", + "example": "elastic" + } + }, + "nullable": true + }, + "version": { + "type": "string", + "example": "WzIwNzMsMV0=" } - }, - "id": { - "type": "string", - "example": "66b9aa00-94fa-11ea-9f74-e7e108796192" - }, - "owner": { - "$ref": "#/components/schemas/owners" - }, - "settings": { - "type": "object", - "properties": { - "syncAlerts": { - "type": "boolean", - "example": true - } - } - }, - "severity": { - "$ref": "#/components/schemas/severity_property" - }, - "status": { - "$ref": "#/components/schemas/status" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "phishing", - "social engineering", - "bubblegum" - ] - }, - "title": { - "type": "string", - "example": "This case will self-destruct in 5 seconds" - }, - "totalAlerts": { - "type": "integer", - "example": 0 - }, - "totalComment": { - "type": "integer", - "example": 0 - }, - "updated_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "example": null - }, - "updated_by": { - "type": "object", - "properties": { - "email": { - "type": "string" - }, - "full_name": { - "type": "string" - }, - "username": { - "type": "string" - } - }, - "nullable": true, - "example": null - }, - "version": { - "type": "string", - "example": "WzUzMiwxXQ==" } } - }, - "examples": { - "createCaseResponse": { - "$ref": "#/components/examples/create_case_response" - } } } } @@ -1927,43 +2079,15 @@ } ] }, - "delete": { - "description": "Deletes one or more cases. You must have all privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the cases you're deleting.\n", - "tags": [ - "cases", - "kibana" - ], - "parameters": [ - { - "$ref": "#/components/parameters/kbn_xsrf" - }, - { - "$ref": "#/components/parameters/space_id" - }, - { - "name": "ids", - "description": "The cases that you want to removed. All non-ASCII characters must be URL encoded.", - "in": "query", - "required": true, - "schema": { - "type": "string" - }, - "example": "d4e7abb0-b462-11ec-9a8d-698504725a43" - } - ], - "responses": { - "204": { - "description": "Indicates a successful call." - } - }, - "servers": [ - { - "url": "https://localhost:5601" - } - ] - }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/api/cases/configure/{configurationId}": { "patch": { - "description": "Updates one or more cases. You must have all privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the case you're updating.\n", + "description": "Updates external connection details, such as the closure type and default connector for cases. You must have all privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the case configuration. Connectors are used to interface with external systems. You must create a connector before you can it in your cases. Refer to the add connectors API.\n", "tags": [ "cases", "kibana" @@ -1973,7 +2097,7 @@ "$ref": "#/components/parameters/kbn_xsrf" }, { - "$ref": "#/components/parameters/space_id" + "$ref": "#/components/parameters/configuration_id" } ], "requestBody": { @@ -1982,156 +2106,114 @@ "schema": { "type": "object", "properties": { - "cases": { - "type": "array", - "items": { - "type": "object", - "properties": { - "connector": { - "description": "An object that contains the connector configuration.", - "type": "object", - "properties": { - "fields": { - "description": "An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value.", - "nullable": true, - "type": "object", - "properties": { - "caseId": { - "description": "The case identifier for Swimlane connectors.", - "type": "string" - }, - "category": { - "description": "The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors.", - "type": "string" - }, - "destIp": { - "description": "A comma-separated list of destination IPs for ServiceNow SecOps connectors.", - "type": "string" - }, - "impact": { - "description": "The effect an incident had on business for ServiceNow ITSM connectors.", - "type": "string" - }, - "issueType": { - "description": "The type of issue for Jira connectors.", - "type": "string" - }, - "issueTypes": { - "description": "The type of incident for IBM Resilient connectors.", - "type": "array", - "items": { - "type": "number" - } - }, - "malwareHash": { - "description": "A comma-separated list of malware hashes for ServiceNow SecOps connectors.", - "type": "string" - }, - "malwareUrl": { - "description": "A comma-separated list of malware URLs for ServiceNow SecOps connectors.", - "type": "string" - }, - "parent": { - "description": "The key of the parent issue, when the issue type is sub-task for Jira connectors.", - "type": "string" - }, - "priority": { - "description": "The priority of the issue for Jira and ServiceNow SecOps connectors.", - "type": "string" - }, - "severity": { - "description": "The severity of the incident for ServiceNow ITSM connectors.", - "type": "string" - }, - "severityCode": { - "description": "The severity code of the incident for IBM Resilient connectors.", - "type": "number" - }, - "sourceIp": { - "description": "A comma-separated list of source IPs for ServiceNow SecOps connectors.", - "type": "string" - }, - "subcategory": { - "description": "The subcategory of the incident for ServiceNow ITSM connectors.", - "type": "string" - }, - "urgency": { - "description": "The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors.", - "type": "string" - } - }, - "required": [ - "fields", - "id", - "name", - "type" - ] - }, - "id": { - "description": "The identifier for the connector. To create a case without a connector, use `none`.", - "type": "string" - }, - "name": { - "description": "The name of the connector. To create a case without a connector, use `none`.", - "type": "string" - }, - "type": { - "$ref": "#/components/schemas/connector_types" - } - } - }, - "description": { - "description": "The description for the case.", - "type": "string" - }, - "id": { - "description": "The identifier for the case.", - "type": "string" - }, - "settings": { - "description": "An object that contains the case settings.", - "type": "object", - "properties": { - "syncAlerts": { - "description": "Turns alert syncing on or off.", - "type": "boolean" - } - } - }, - "severity": { - "$ref": "#/components/schemas/severity_property" - }, - "status": { - "$ref": "#/components/schemas/status" - }, - "tags": { - "description": "The words and phrases that help categorize cases.", - "type": "array", - "items": { + "closure_type": { + "$ref": "#/components/schemas/closure_types" + }, + "connector": { + "description": "An object that contains the connector configuration.", + "type": "object", + "properties": { + "fields": { + "description": "An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value.", + "nullable": true, + "type": "object", + "properties": { + "caseId": { + "description": "The case identifier for Swimlane connectors.", "type": "string" - } - }, - "title": { - "description": "A title for the case.", - "type": "string" - }, - "version": { - "description": "The current version of the case.", - "type": "string" - } + }, + "category": { + "description": "The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors.", + "type": "string" + }, + "destIp": { + "description": "A comma-separated list of destination IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "impact": { + "description": "The effect an incident had on business for ServiceNow ITSM connectors.", + "type": "string" + }, + "issueType": { + "description": "The type of issue for Jira connectors.", + "type": "string" + }, + "issueTypes": { + "description": "The type of incident for IBM Resilient connectors.", + "type": "array", + "items": { + "type": "number" + } + }, + "malwareHash": { + "description": "A comma-separated list of malware hashes for ServiceNow SecOps connectors.", + "type": "string" + }, + "malwareUrl": { + "description": "A comma-separated list of malware URLs for ServiceNow SecOps connectors.", + "type": "string" + }, + "parent": { + "description": "The key of the parent issue, when the issue type is sub-task for Jira connectors.", + "type": "string" + }, + "priority": { + "description": "The priority of the issue for Jira and ServiceNow SecOps connectors.", + "type": "string" + }, + "severity": { + "description": "The severity of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "severityCode": { + "description": "The severity code of the incident for IBM Resilient connectors.", + "type": "number" + }, + "sourceIp": { + "description": "A comma-separated list of source IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "subcategory": { + "description": "The subcategory of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "urgency": { + "description": "The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors.", + "type": "string" + } + }, + "example": null }, - "required": [ - "id", - "version" - ] - } + "id": { + "description": "The identifier for the connector. To create a case without a connector, use `none`.", + "type": "string", + "example": "none" + }, + "name": { + "description": "The name of the connector. To create a case without a connector, use `none`.", + "type": "string", + "example": "none" + }, + "type": { + "$ref": "#/components/schemas/connector_types" + } + }, + "required": [ + "fields", + "id", + "name", + "type" + ] + }, + "version": { + "description": "The version of the connector. To retrieve the version value, use the get configuration API.\n", + "type": "string", + "example": "WzIwMiwxXQ==" } - } - }, - "examples": { - "updateCaseRequest": { - "$ref": "#/components/examples/update_case_request" - } + }, + "required": [ + "version" + ] } } } @@ -2142,277 +2224,185 @@ "content": { "application/json; charset=utf-8": { "schema": { - "type": "object", - "properties": { - "closed_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "example": null - }, - "closed_by": { - "type": "object", - "properties": { - "email": { - "type": "string" - }, - "full_name": { - "type": "string" - }, - "username": { - "type": "string" + "type": "array", + "items": { + "type": "object", + "properties": { + "closure_type": { + "$ref": "#/components/schemas/closure_types" + }, + "connector": { + "type": "object", + "properties": { + "fields": { + "description": "An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value.", + "nullable": true, + "type": "object", + "properties": { + "caseId": { + "description": "The case identifier for Swimlane connectors.", + "type": "string" + }, + "category": { + "description": "The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors.", + "type": "string" + }, + "destIp": { + "description": "A comma-separated list of destination IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "impact": { + "description": "The effect an incident had on business for ServiceNow ITSM connectors.", + "type": "string" + }, + "issueType": { + "description": "The type of issue for Jira connectors.", + "type": "string" + }, + "issueTypes": { + "description": "The type of incident for IBM Resilient connectors.", + "type": "array", + "items": { + "type": "number" + } + }, + "malwareHash": { + "description": "A comma-separated list of malware hashes for ServiceNow SecOps connectors.", + "type": "string" + }, + "malwareUrl": { + "description": "A comma-separated list of malware URLs for ServiceNow SecOps connectors.", + "type": "string" + }, + "parent": { + "description": "The key of the parent issue, when the issue type is sub-task for Jira connectors.", + "type": "string" + }, + "priority": { + "description": "The priority of the issue for Jira and ServiceNow SecOps connectors.", + "type": "string" + }, + "severity": { + "description": "The severity of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "severityCode": { + "description": "The severity code of the incident for IBM Resilient connectors.", + "type": "number" + }, + "sourceIp": { + "description": "A comma-separated list of source IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "subcategory": { + "description": "The subcategory of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "urgency": { + "description": "The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors.", + "type": "string" + } + }, + "example": null + }, + "id": { + "description": "The identifier for the connector. To create a case without a connector, use `none`.", + "type": "string", + "example": "none" + }, + "name": { + "description": "The name of the connector. To create a case without a connector, use `none`.", + "type": "string", + "example": "none" + }, + "type": { + "$ref": "#/components/schemas/connector_types" + } } }, - "nullable": true, - "example": null - }, - "comments": { - "type": "array", - "items": { - "type": "string" + "created_at": { + "type": "string", + "format": "date-time", + "example": "2022-06-01T17:07:17.767Z" }, - "example": [] - }, - "connector": { - "type": "object", - "properties": { - "fields": { - "description": "An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value.", - "nullable": true, + "created_by": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": null + }, + "full_name": { + "type": "string", + "example": null + }, + "username": { + "type": "string", + "example": "elastic" + } + } + }, + "error": { + "type": "string", + "example": null + }, + "id": { + "type": "string", + "example": "4a97a440-e1cd-11ec-be9b-9b1838238ee6" + }, + "mappings": { + "type": "array", + "items": { "type": "object", "properties": { - "caseId": { - "description": "The case identifier for Swimlane connectors.", - "type": "string" - }, - "category": { - "description": "The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors.", - "type": "string" - }, - "destIp": { - "description": "A comma-separated list of destination IPs for ServiceNow SecOps connectors.", - "type": "string" - }, - "impact": { - "description": "The effect an incident had on business for ServiceNow ITSM connectors.", - "type": "string" - }, - "issueType": { - "description": "The type of issue for Jira connectors.", - "type": "string" - }, - "issueTypes": { - "description": "The type of incident for IBM Resilient connectors.", - "type": "array", - "items": { - "type": "number" - } - }, - "malwareHash": { - "description": "A comma-separated list of malware hashes for ServiceNow SecOps connectors.", - "type": "string" - }, - "malwareUrl": { - "description": "A comma-separated list of malware URLs for ServiceNow SecOps connectors.", - "type": "string" - }, - "parent": { - "description": "The key of the parent issue, when the issue type is sub-task for Jira connectors.", - "type": "string" + "action_type": { + "type": "string", + "example": "overwrite" }, - "priority": { - "description": "The priority of the issue for Jira and ServiceNow SecOps connectors.", - "type": "string" - }, - "severity": { - "description": "The severity of the incident for ServiceNow ITSM connectors.", - "type": "string" - }, - "severityCode": { - "description": "The severity code of the incident for IBM Resilient connectors.", - "type": "number" - }, - "sourceIp": { - "description": "A comma-separated list of source IPs for ServiceNow SecOps connectors.", - "type": "string" - }, - "subcategory": { - "description": "The subcategory of the incident for ServiceNow ITSM connectors.", - "type": "string" + "source": { + "type": "string", + "example": "title" }, - "urgency": { - "description": "The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors.", - "type": "string" + "target": { + "type": "string", + "example": "summary" } + } + } + }, + "owner": { + "$ref": "#/components/schemas/owners" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "example": "2022-06-01T19:58:48.169Z" + }, + "updated_by": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": null }, - "required": [ - "fields", - "id", - "name", - "type" - ] - }, - "id": { - "description": "The identifier for the connector. To create a case without a connector, use `none`.", - "type": "string" - }, - "name": { - "description": "The name of the connector. To create a case without a connector, use `none`.", - "type": "string" - }, - "type": { - "$ref": "#/components/schemas/connector_types" - } - } - }, - "created_at": { - "type": "string", - "format": "date-time", - "example": "2022-05-13T09:16:17.416Z" - }, - "created_by": { - "type": "object", - "properties": { - "email": { - "type": "string", - "example": "ahunley@imf.usa.gov" - }, - "full_name": { - "type": "string", - "example": "Alan Hunley" - }, - "username": { - "type": "string", - "example": "ahunley" - } - } - }, - "description": { - "type": "string", - "example": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active" - }, - "duration": { - "type": "integer", - "description": "The elapsed time from the creation of the case to its closure (in seconds). If the case has not been closed, the duration is set to null.", - "example": 120 - }, - "external_service": { - "type": "object", - "properties": { - "connector_id": { - "type": "string" - }, - "connector_name": { - "type": "string" - }, - "external_id": { - "type": "string" - }, - "external_title": { - "type": "string" - }, - "external_url": { - "type": "string" - }, - "pushed_at": { - "type": "string", - "format": "date-time" - }, - "pushed_by": { - "type": "object", - "properties": { - "email": { - "type": "string" - }, - "full_name": { - "type": "string" - }, - "username": { - "type": "string" - } + "full_name": { + "type": "string", + "example": null }, - "nullable": true, - "example": null - } - } - }, - "id": { - "type": "string", - "example": "66b9aa00-94fa-11ea-9f74-e7e108796192" - }, - "owner": { - "$ref": "#/components/schemas/owners" - }, - "settings": { - "type": "object", - "properties": { - "syncAlerts": { - "type": "boolean", - "example": true - } - } - }, - "severity": { - "$ref": "#/components/schemas/severity_property" - }, - "status": { - "$ref": "#/components/schemas/status" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "phishing", - "social engineering", - "bubblegum" - ] - }, - "title": { - "type": "string", - "example": "This case will self-destruct in 5 seconds" - }, - "totalAlerts": { - "type": "integer", - "example": 0 - }, - "totalComment": { - "type": "integer", - "example": 0 - }, - "updated_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "example": null - }, - "updated_by": { - "type": "object", - "properties": { - "email": { - "type": "string" - }, - "full_name": { - "type": "string" + "username": { + "type": "string", + "example": "elastic" + } }, - "username": { - "type": "string" - } + "nullable": true }, - "nullable": true, - "example": null - }, - "version": { - "type": "string", - "example": "WzUzMiwxXQ==" + "version": { + "type": "string", + "example": "WzIwNzMsMV0=" + } } } - }, - "examples": { - "updateCaseResponse": { - "$ref": "#/components/examples/update_case_response" - } } } } @@ -2430,492 +2420,2402 @@ } ] }, - "/s/{spaceId}/api/cases/_find": { - "get": { - "description": "Retrieves a paginated subset of cases. You must have read privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the cases you're seeking.\n", + "/s/{spaceId}/api/cases": { + "post": { + "description": "Creates a case. You must have all privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the case you're creating.\n", "tags": [ "cases", "kibana" ], "parameters": [ { - "$ref": "#/components/parameters/space_id" - }, - { - "name": "defaultSearchOperator", - "in": "query", - "description": "The default operator to use for the simple_query_string.", - "schema": { - "type": "string", - "default": "OR" - }, - "example": "OR" - }, - { - "name": "fields", - "in": "query", - "description": "The fields in the entity to return in the response.", - "schema": { - "type": "array", - "items": { - "type": "string" - } - } - }, - { - "name": "from", - "in": "query", - "description": "[preview] Returns only cases that were created after a specific date. The date must be specified as a KQL data range or date match expression. This functionality is in technical preview and may be changed or removed in a future release. Elastic will apply best effort to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.\n", - "schema": { - "type": "string" - }, - "example": "now-1d" - }, - { - "$ref": "#/components/parameters/owner" - }, - { - "name": "page", - "in": "query", - "description": "The page number to return.", - "schema": { - "type": "integer", - "default": 1 - }, - "example": 1 - }, - { - "name": "perPage", - "in": "query", - "description": "The number of rules to return per page.", - "schema": { - "type": "integer", - "default": 20 - }, - "example": 20 - }, - { - "name": "reporters", - "in": "query", - "description": "Filters the returned cases by the user name of the reporter.", - "schema": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "type": "string" - } - } - ] - }, - "example": "elastic" - }, - { - "name": "search", - "in": "query", - "description": "An Elasticsearch simple_query_string query that filters the objects in the response.", - "schema": { - "type": "string" - } + "$ref": "#/components/parameters/kbn_xsrf" }, { - "name": "searchFields", - "in": "query", - "description": "The fields to perform the simple_query_string parsed query against.", + "$ref": "#/components/parameters/space_id" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "connector": { + "description": "An object that contains the connector configuration.", + "type": "object", + "properties": { + "fields": { + "description": "An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value.", + "nullable": true, + "type": "object", + "properties": { + "caseId": { + "description": "The case identifier for Swimlane connectors.", + "type": "string" + }, + "category": { + "description": "The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors.", + "type": "string" + }, + "destIp": { + "description": "A comma-separated list of destination IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "impact": { + "description": "The effect an incident had on business for ServiceNow ITSM connectors.", + "type": "string" + }, + "issueType": { + "description": "The type of issue for Jira connectors.", + "type": "string" + }, + "issueTypes": { + "description": "The type of incident for IBM Resilient connectors.", + "type": "array", + "items": { + "type": "number" + } + }, + "malwareHash": { + "description": "A comma-separated list of malware hashes for ServiceNow SecOps connectors.", + "type": "string" + }, + "malwareUrl": { + "description": "A comma-separated list of malware URLs for ServiceNow SecOps connectors.", + "type": "string" + }, + "parent": { + "description": "The key of the parent issue, when the issue type is sub-task for Jira connectors.", + "type": "string" + }, + "priority": { + "description": "The priority of the issue for Jira and ServiceNow SecOps connectors.", + "type": "string" + }, + "severity": { + "description": "The severity of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "severityCode": { + "description": "The severity code of the incident for IBM Resilient connectors.", + "type": "number" + }, + "sourceIp": { + "description": "A comma-separated list of source IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "subcategory": { + "description": "The subcategory of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "urgency": { + "description": "The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors.", + "type": "string" + } + }, + "example": null + }, + "id": { + "description": "The identifier for the connector. To create a case without a connector, use `none`.", + "type": "string", + "example": "none" + }, + "name": { + "description": "The name of the connector. To create a case without a connector, use `none`.", + "type": "string", + "example": "none" + }, + "type": { + "$ref": "#/components/schemas/connector_types" + } + }, + "required": [ + "fields", + "id", + "name", + "type" + ] + }, + "description": { + "description": "The description for the case.", + "type": "string" + }, + "owner": { + "$ref": "#/components/schemas/owners" + }, + "settings": { + "description": "An object that contains the case settings.", + "type": "object", + "properties": { + "syncAlerts": { + "description": "Turns alert syncing on or off.", + "type": "boolean" + } + } + }, + "severity": { + "$ref": "#/components/schemas/severity_property" + }, + "tags": { + "description": "The words and phrases that help categorize cases. It can be an empty array.", + "type": "array", + "items": { + "type": "string" + } + }, + "title": { + "description": "A title for the case.", + "type": "string" + } + }, + "required": [ + "connector", + "description", + "owner", + "settings", + "tags", + "title" + ] + }, + "examples": { + "createCaseRequest": { + "$ref": "#/components/examples/create_case_request" + } + } + } + } + }, + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json; charset=utf-8": { + "schema": { + "type": "object", + "properties": { + "closed_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "example": null + }, + "closed_by": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "nullable": true, + "example": null + }, + "comments": { + "type": "array", + "items": { + "type": "string" + }, + "example": [] + }, + "connector": { + "type": "object", + "properties": { + "fields": { + "description": "An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value.", + "nullable": true, + "type": "object", + "properties": { + "caseId": { + "description": "The case identifier for Swimlane connectors.", + "type": "string" + }, + "category": { + "description": "The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors.", + "type": "string" + }, + "destIp": { + "description": "A comma-separated list of destination IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "impact": { + "description": "The effect an incident had on business for ServiceNow ITSM connectors.", + "type": "string" + }, + "issueType": { + "description": "The type of issue for Jira connectors.", + "type": "string" + }, + "issueTypes": { + "description": "The type of incident for IBM Resilient connectors.", + "type": "array", + "items": { + "type": "number" + } + }, + "malwareHash": { + "description": "A comma-separated list of malware hashes for ServiceNow SecOps connectors.", + "type": "string" + }, + "malwareUrl": { + "description": "A comma-separated list of malware URLs for ServiceNow SecOps connectors.", + "type": "string" + }, + "parent": { + "description": "The key of the parent issue, when the issue type is sub-task for Jira connectors.", + "type": "string" + }, + "priority": { + "description": "The priority of the issue for Jira and ServiceNow SecOps connectors.", + "type": "string" + }, + "severity": { + "description": "The severity of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "severityCode": { + "description": "The severity code of the incident for IBM Resilient connectors.", + "type": "number" + }, + "sourceIp": { + "description": "A comma-separated list of source IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "subcategory": { + "description": "The subcategory of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "urgency": { + "description": "The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors.", + "type": "string" + } + }, + "example": null + }, + "id": { + "description": "The identifier for the connector. To create a case without a connector, use `none`.", + "type": "string", + "example": "none" + }, + "name": { + "description": "The name of the connector. To create a case without a connector, use `none`.", + "type": "string", + "example": "none" + }, + "type": { + "$ref": "#/components/schemas/connector_types" + } + } + }, + "created_at": { + "type": "string", + "format": "date-time", + "example": "2022-05-13T09:16:17.416Z" + }, + "created_by": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": null + }, + "full_name": { + "type": "string", + "example": null + }, + "username": { + "type": "string", + "example": "elastic" + } + } + }, + "description": { + "type": "string", + "example": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active" + }, + "duration": { + "type": "integer", + "description": "The elapsed time from the creation of the case to its closure (in seconds). If the case has not been closed, the duration is set to null.", + "example": 120 + }, + "external_service": { + "type": "object", + "properties": { + "connector_id": { + "type": "string" + }, + "connector_name": { + "type": "string" + }, + "external_id": { + "type": "string" + }, + "external_title": { + "type": "string" + }, + "external_url": { + "type": "string" + }, + "pushed_at": { + "type": "string", + "format": "date-time" + }, + "pushed_by": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "nullable": true, + "example": null + } + } + }, + "id": { + "type": "string", + "example": "66b9aa00-94fa-11ea-9f74-e7e108796192" + }, + "owner": { + "$ref": "#/components/schemas/owners" + }, + "settings": { + "type": "object", + "properties": { + "syncAlerts": { + "type": "boolean", + "example": true + } + } + }, + "severity": { + "$ref": "#/components/schemas/severity_property" + }, + "status": { + "$ref": "#/components/schemas/status" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "phishing", + "social engineering", + "bubblegum" + ] + }, + "title": { + "type": "string", + "example": "This case will self-destruct in 5 seconds" + }, + "totalAlerts": { + "type": "integer", + "example": 0 + }, + "totalComment": { + "type": "integer", + "example": 0 + }, + "updated_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "example": null + }, + "updated_by": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "nullable": true, + "example": null + }, + "version": { + "type": "string", + "example": "WzUzMiwxXQ==" + } + } + }, + "examples": { + "createCaseResponse": { + "$ref": "#/components/examples/create_case_response" + } + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "delete": { + "description": "Deletes one or more cases. You must have all privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the cases you're deleting.\n", + "tags": [ + "cases", + "kibana" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/space_id" + }, + { + "name": "ids", + "description": "The cases that you want to removed. All non-ASCII characters must be URL encoded.", + "in": "query", + "required": true, + "schema": { + "type": "string" + }, + "example": "d4e7abb0-b462-11ec-9a8d-698504725a43" + } + ], + "responses": { + "204": { + "description": "Indicates a successful call." + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "patch": { + "description": "Updates one or more cases. You must have all privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the case you're updating.\n", + "tags": [ + "cases", + "kibana" + ], + "parameters": [ + { + "$ref": "#/components/parameters/kbn_xsrf" + }, + { + "$ref": "#/components/parameters/space_id" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "cases": { + "type": "array", + "items": { + "type": "object", + "properties": { + "connector": { + "description": "An object that contains the connector configuration.", + "type": "object", + "properties": { + "fields": { + "description": "An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value.", + "nullable": true, + "type": "object", + "properties": { + "caseId": { + "description": "The case identifier for Swimlane connectors.", + "type": "string" + }, + "category": { + "description": "The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors.", + "type": "string" + }, + "destIp": { + "description": "A comma-separated list of destination IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "impact": { + "description": "The effect an incident had on business for ServiceNow ITSM connectors.", + "type": "string" + }, + "issueType": { + "description": "The type of issue for Jira connectors.", + "type": "string" + }, + "issueTypes": { + "description": "The type of incident for IBM Resilient connectors.", + "type": "array", + "items": { + "type": "number" + } + }, + "malwareHash": { + "description": "A comma-separated list of malware hashes for ServiceNow SecOps connectors.", + "type": "string" + }, + "malwareUrl": { + "description": "A comma-separated list of malware URLs for ServiceNow SecOps connectors.", + "type": "string" + }, + "parent": { + "description": "The key of the parent issue, when the issue type is sub-task for Jira connectors.", + "type": "string" + }, + "priority": { + "description": "The priority of the issue for Jira and ServiceNow SecOps connectors.", + "type": "string" + }, + "severity": { + "description": "The severity of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "severityCode": { + "description": "The severity code of the incident for IBM Resilient connectors.", + "type": "number" + }, + "sourceIp": { + "description": "A comma-separated list of source IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "subcategory": { + "description": "The subcategory of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "urgency": { + "description": "The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors.", + "type": "string" + } + }, + "example": null + }, + "id": { + "description": "The identifier for the connector. To create a case without a connector, use `none`.", + "type": "string", + "example": "none" + }, + "name": { + "description": "The name of the connector. To create a case without a connector, use `none`.", + "type": "string", + "example": "none" + }, + "type": { + "$ref": "#/components/schemas/connector_types" + } + }, + "required": [ + "fields", + "id", + "name", + "type" + ] + }, + "description": { + "description": "The description for the case.", + "type": "string" + }, + "id": { + "description": "The identifier for the case.", + "type": "string" + }, + "settings": { + "description": "An object that contains the case settings.", + "type": "object", + "properties": { + "syncAlerts": { + "description": "Turns alert syncing on or off.", + "type": "boolean" + } + } + }, + "severity": { + "$ref": "#/components/schemas/severity_property" + }, + "status": { + "$ref": "#/components/schemas/status" + }, + "tags": { + "description": "The words and phrases that help categorize cases.", + "type": "array", + "items": { + "type": "string" + } + }, + "title": { + "description": "A title for the case.", + "type": "string" + }, + "version": { + "description": "The current version of the case.", + "type": "string" + } + }, + "required": [ + "id", + "version" + ] + } + } + } + }, + "examples": { + "updateCaseRequest": { + "$ref": "#/components/examples/update_case_request" + } + } + } + } + }, + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json; charset=utf-8": { + "schema": { + "type": "object", + "properties": { + "closed_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "example": null + }, + "closed_by": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "nullable": true, + "example": null + }, + "comments": { + "type": "array", + "items": { + "type": "string" + }, + "example": [] + }, + "connector": { + "type": "object", + "properties": { + "fields": { + "description": "An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value.", + "nullable": true, + "type": "object", + "properties": { + "caseId": { + "description": "The case identifier for Swimlane connectors.", + "type": "string" + }, + "category": { + "description": "The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors.", + "type": "string" + }, + "destIp": { + "description": "A comma-separated list of destination IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "impact": { + "description": "The effect an incident had on business for ServiceNow ITSM connectors.", + "type": "string" + }, + "issueType": { + "description": "The type of issue for Jira connectors.", + "type": "string" + }, + "issueTypes": { + "description": "The type of incident for IBM Resilient connectors.", + "type": "array", + "items": { + "type": "number" + } + }, + "malwareHash": { + "description": "A comma-separated list of malware hashes for ServiceNow SecOps connectors.", + "type": "string" + }, + "malwareUrl": { + "description": "A comma-separated list of malware URLs for ServiceNow SecOps connectors.", + "type": "string" + }, + "parent": { + "description": "The key of the parent issue, when the issue type is sub-task for Jira connectors.", + "type": "string" + }, + "priority": { + "description": "The priority of the issue for Jira and ServiceNow SecOps connectors.", + "type": "string" + }, + "severity": { + "description": "The severity of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "severityCode": { + "description": "The severity code of the incident for IBM Resilient connectors.", + "type": "number" + }, + "sourceIp": { + "description": "A comma-separated list of source IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "subcategory": { + "description": "The subcategory of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "urgency": { + "description": "The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors.", + "type": "string" + } + }, + "example": null + }, + "id": { + "description": "The identifier for the connector. To create a case without a connector, use `none`.", + "type": "string", + "example": "none" + }, + "name": { + "description": "The name of the connector. To create a case without a connector, use `none`.", + "type": "string", + "example": "none" + }, + "type": { + "$ref": "#/components/schemas/connector_types" + } + } + }, + "created_at": { + "type": "string", + "format": "date-time", + "example": "2022-05-13T09:16:17.416Z" + }, + "created_by": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": null + }, + "full_name": { + "type": "string", + "example": null + }, + "username": { + "type": "string", + "example": "elastic" + } + } + }, + "description": { + "type": "string", + "example": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active" + }, + "duration": { + "type": "integer", + "description": "The elapsed time from the creation of the case to its closure (in seconds). If the case has not been closed, the duration is set to null.", + "example": 120 + }, + "external_service": { + "type": "object", + "properties": { + "connector_id": { + "type": "string" + }, + "connector_name": { + "type": "string" + }, + "external_id": { + "type": "string" + }, + "external_title": { + "type": "string" + }, + "external_url": { + "type": "string" + }, + "pushed_at": { + "type": "string", + "format": "date-time" + }, + "pushed_by": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "nullable": true, + "example": null + } + } + }, + "id": { + "type": "string", + "example": "66b9aa00-94fa-11ea-9f74-e7e108796192" + }, + "owner": { + "$ref": "#/components/schemas/owners" + }, + "settings": { + "type": "object", + "properties": { + "syncAlerts": { + "type": "boolean", + "example": true + } + } + }, + "severity": { + "$ref": "#/components/schemas/severity_property" + }, + "status": { + "$ref": "#/components/schemas/status" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "phishing", + "social engineering", + "bubblegum" + ] + }, + "title": { + "type": "string", + "example": "This case will self-destruct in 5 seconds" + }, + "totalAlerts": { + "type": "integer", + "example": 0 + }, + "totalComment": { + "type": "integer", + "example": 0 + }, + "updated_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "example": null + }, + "updated_by": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "nullable": true, + "example": null + }, + "version": { + "type": "string", + "example": "WzUzMiwxXQ==" + } + } + }, + "examples": { + "updateCaseResponse": { + "$ref": "#/components/examples/update_case_response" + } + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/s/{spaceId}/api/cases/_find": { + "get": { + "description": "Retrieves a paginated subset of cases. You must have read privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the cases you're seeking.\n", + "tags": [ + "cases", + "kibana" + ], + "parameters": [ + { + "$ref": "#/components/parameters/space_id" + }, + { + "name": "defaultSearchOperator", + "in": "query", + "description": "The default operator to use for the simple_query_string.", + "schema": { + "type": "string", + "default": "OR" + }, + "example": "OR" + }, + { + "name": "fields", + "in": "query", + "description": "The fields in the entity to return in the response.", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + { + "name": "from", + "in": "query", + "description": "[preview] Returns only cases that were created after a specific date. The date must be specified as a KQL data range or date match expression. This functionality is in technical preview and may be changed or removed in a future release. Elastic will apply best effort to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.\n", + "schema": { + "type": "string" + }, + "example": "now-1d" + }, + { + "$ref": "#/components/parameters/owner" + }, + { + "name": "page", + "in": "query", + "description": "The page number to return.", + "schema": { + "type": "integer", + "default": 1 + }, + "example": 1 + }, + { + "name": "perPage", + "in": "query", + "description": "The number of rules to return per page.", + "schema": { + "type": "integer", + "default": 20 + }, + "example": 20 + }, + { + "name": "reporters", + "in": "query", + "description": "Filters the returned cases by the user name of the reporter.", + "schema": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "example": "elastic" + }, + { + "name": "search", + "in": "query", + "description": "An Elasticsearch simple_query_string query that filters the objects in the response.", + "schema": { + "type": "string" + } + }, + { + "name": "searchFields", + "in": "query", + "description": "The fields to perform the simple_query_string parsed query against.", + "schema": { + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + } + }, + { + "$ref": "#/components/parameters/severity" + }, + { + "name": "sortField", + "in": "query", + "description": "Determines which field is used to sort the results.", + "schema": { + "type": "string", + "enum": [ + "createdAt", + "updatedAt" + ], + "default": "createdAt" + }, + "example": "updatedAt" + }, + { + "name": "sortOrder", + "in": "query", + "description": "Determines the sort order.", + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "default": "desc" + }, + "example": "asc" + }, + { + "name": "status", + "in": "query", + "description": "Filters the returned cases by state.", + "schema": { + "type": "string", + "enum": [ + "closed", + "in-progress", + "open" + ] + }, + "example": "open" + }, + { + "name": "tags", + "in": "query", + "description": "Filters the returned cases by tags.", "schema": { "oneOf": [ { "type": "string" }, - { + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, + "example": "phishing" + }, + { + "name": "to", + "in": "query", + "description": "[preview] Returns only cases that were created before a specific date. The date must be specified as a KQL data range or date match expression. This functionality is in technical preview and may be changed or removed in a future release. Elastic will apply best effort to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.\n", + "schema": { + "type": "string" + }, + "example": "now+1d" + } + ], + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json; charset=utf-8": { + "schema": { + "type": "object", + "properties": { + "cases": { + "type": "array", + "items": { + "type": "object", + "properties": { + "closed_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "example": null + }, + "closed_by": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "nullable": true, + "example": null + }, + "comments": { + "type": "array", + "items": { + "type": "string" + }, + "example": [] + }, + "connector": { + "type": "object", + "properties": { + "fields": { + "description": "An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value.", + "nullable": true, + "type": "object", + "properties": { + "caseId": { + "description": "The case identifier for Swimlane connectors.", + "type": "string" + }, + "category": { + "description": "The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors.", + "type": "string" + }, + "destIp": { + "description": "A comma-separated list of destination IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "impact": { + "description": "The effect an incident had on business for ServiceNow ITSM connectors.", + "type": "string" + }, + "issueType": { + "description": "The type of issue for Jira connectors.", + "type": "string" + }, + "issueTypes": { + "description": "The type of incident for IBM Resilient connectors.", + "type": "array", + "items": { + "type": "number" + } + }, + "malwareHash": { + "description": "A comma-separated list of malware hashes for ServiceNow SecOps connectors.", + "type": "string" + }, + "malwareUrl": { + "description": "A comma-separated list of malware URLs for ServiceNow SecOps connectors.", + "type": "string" + }, + "parent": { + "description": "The key of the parent issue, when the issue type is sub-task for Jira connectors.", + "type": "string" + }, + "priority": { + "description": "The priority of the issue for Jira and ServiceNow SecOps connectors.", + "type": "string" + }, + "severity": { + "description": "The severity of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "severityCode": { + "description": "The severity code of the incident for IBM Resilient connectors.", + "type": "number" + }, + "sourceIp": { + "description": "A comma-separated list of source IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "subcategory": { + "description": "The subcategory of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "urgency": { + "description": "The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors.", + "type": "string" + } + }, + "example": null + }, + "id": { + "description": "The identifier for the connector. To create a case without a connector, use `none`.", + "type": "string", + "example": "none" + }, + "name": { + "description": "The name of the connector. To create a case without a connector, use `none`.", + "type": "string", + "example": "none" + }, + "type": { + "$ref": "#/components/schemas/connector_types" + } + } + }, + "created_at": { + "type": "string", + "format": "date-time", + "example": "2022-05-13T09:16:17.416Z" + }, + "created_by": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": null + }, + "full_name": { + "type": "string", + "example": null + }, + "username": { + "type": "string", + "example": "elastic" + } + } + }, + "description": { + "type": "string", + "example": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active" + }, + "duration": { + "type": "integer", + "description": "The elapsed time from the creation of the case to its closure (in seconds). If the case has not been closed, the duration is set to null.", + "example": 120 + }, + "external_service": { + "type": "object", + "properties": { + "connector_id": { + "type": "string" + }, + "connector_name": { + "type": "string" + }, + "external_id": { + "type": "string" + }, + "external_title": { + "type": "string" + }, + "external_url": { + "type": "string" + }, + "pushed_at": { + "type": "string", + "format": "date-time" + }, + "pushed_by": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "nullable": true, + "example": null + } + } + }, + "id": { + "type": "string", + "example": "66b9aa00-94fa-11ea-9f74-e7e108796192" + }, + "owner": { + "$ref": "#/components/schemas/owners" + }, + "settings": { + "type": "object", + "properties": { + "syncAlerts": { + "type": "boolean", + "example": true + } + } + }, + "severity": { + "$ref": "#/components/schemas/severity_property" + }, + "status": { + "$ref": "#/components/schemas/status" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "example": [ + "phishing", + "social engineering", + "bubblegum" + ] + }, + "title": { + "type": "string", + "example": "This case will self-destruct in 5 seconds" + }, + "totalAlerts": { + "type": "integer", + "example": 0 + }, + "totalComment": { + "type": "integer", + "example": 0 + }, + "updated_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "example": null + }, + "updated_by": { + "type": "object", + "properties": { + "email": { + "type": "string" + }, + "full_name": { + "type": "string" + }, + "username": { + "type": "string" + } + }, + "nullable": true, + "example": null + }, + "version": { + "type": "string", + "example": "WzUzMiwxXQ==" + } + } + } + }, + "count_closed_cases": { + "type": "integer" + }, + "count_in_progress_cases": { + "type": "integer" + }, + "count_open_cases": { + "type": "integer" + }, + "page": { + "type": "integer" + }, + "per_page": { + "type": "integer" + }, + "total": { + "type": "integer" + } + } + }, + "examples": { + "findCaseResponse": { + "$ref": "#/components/examples/find_case_response" + } + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/s/{spaceId}/api/cases/alerts/{alertId}": { + "get": { + "description": "Returns the cases associated with a specific alert. You must have read privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the cases you're seeking.\n", + "x-technical-preview": true, + "tags": [ + "cases", + "kibana" + ], + "parameters": [ + { + "$ref": "#/components/parameters/alert_id" + }, + { + "$ref": "#/components/parameters/space_id" + }, + { + "$ref": "#/components/parameters/owner" + } + ], + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json; charset=utf-8": { + "schema": { "type": "array", "items": { - "type": "string" + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The case identifier." + }, + "title": { + "type": "string", + "description": "The case title." + } + } + }, + "example": [ + { + "id": "06116b80-e1c3-11ec-be9b-9b1838238ee6", + "title": "security_case" + } + ] + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/s/{spaceId}/api/cases/configure": { + "get": { + "description": "Retrieves external connection details, such as the closure type and default connector for cases. You must have read privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the case configuration.\n", + "tags": [ + "cases", + "kibana" + ], + "parameters": [ + { + "$ref": "#/components/parameters/space_id" + }, + { + "$ref": "#/components/parameters/owner" + } + ], + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json; charset=utf-8": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "closure_type": { + "$ref": "#/components/schemas/closure_types" + }, + "connector": { + "type": "object", + "properties": { + "fields": { + "description": "An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value.", + "nullable": true, + "type": "object", + "properties": { + "caseId": { + "description": "The case identifier for Swimlane connectors.", + "type": "string" + }, + "category": { + "description": "The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors.", + "type": "string" + }, + "destIp": { + "description": "A comma-separated list of destination IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "impact": { + "description": "The effect an incident had on business for ServiceNow ITSM connectors.", + "type": "string" + }, + "issueType": { + "description": "The type of issue for Jira connectors.", + "type": "string" + }, + "issueTypes": { + "description": "The type of incident for IBM Resilient connectors.", + "type": "array", + "items": { + "type": "number" + } + }, + "malwareHash": { + "description": "A comma-separated list of malware hashes for ServiceNow SecOps connectors.", + "type": "string" + }, + "malwareUrl": { + "description": "A comma-separated list of malware URLs for ServiceNow SecOps connectors.", + "type": "string" + }, + "parent": { + "description": "The key of the parent issue, when the issue type is sub-task for Jira connectors.", + "type": "string" + }, + "priority": { + "description": "The priority of the issue for Jira and ServiceNow SecOps connectors.", + "type": "string" + }, + "severity": { + "description": "The severity of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "severityCode": { + "description": "The severity code of the incident for IBM Resilient connectors.", + "type": "number" + }, + "sourceIp": { + "description": "A comma-separated list of source IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "subcategory": { + "description": "The subcategory of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "urgency": { + "description": "The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors.", + "type": "string" + } + }, + "example": null + }, + "id": { + "description": "The identifier for the connector. To create a case without a connector, use `none`.", + "type": "string", + "example": "none" + }, + "name": { + "description": "The name of the connector. To create a case without a connector, use `none`.", + "type": "string", + "example": "none" + }, + "type": { + "$ref": "#/components/schemas/connector_types" + } + } + }, + "created_at": { + "type": "string", + "format": "date-time", + "example": "2022-06-01T17:07:17.767Z" + }, + "created_by": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": null + }, + "full_name": { + "type": "string", + "example": null + }, + "username": { + "type": "string", + "example": "elastic" + } + } + }, + "error": { + "type": "string", + "example": null + }, + "id": { + "type": "string", + "example": "4a97a440-e1cd-11ec-be9b-9b1838238ee6" + }, + "mappings": { + "type": "array", + "items": { + "type": "object", + "properties": { + "action_type": { + "type": "string", + "example": "overwrite" + }, + "source": { + "type": "string", + "example": "title" + }, + "target": { + "type": "string", + "example": "summary" + } + } + } + }, + "owner": { + "$ref": "#/components/schemas/owners" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "example": "2022-06-01T19:58:48.169Z" + }, + "updated_by": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": null + }, + "full_name": { + "type": "string", + "example": null + }, + "username": { + "type": "string", + "example": "elastic" + } + }, + "nullable": true + }, + "version": { + "type": "string", + "example": "WzIwNzMsMV0=" + } + } } } - ] + } } - }, + } + }, + "servers": [ { - "$ref": "#/components/parameters/severity" - }, + "url": "https://localhost:5601" + } + ] + }, + "post": { + "description": "Sets external connection details, such as the closure type and default connector for cases. You must have all privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the case configuration. Connectors are used to interface with external systems. You must create a connector before you can use it in your cases. Refer to the add connectors API. If you set a default connector, it is automatically selected when you create cases in Kibana. If you use the create case API, however, you must still specify all of the connector details.\n", + "tags": [ + "cases", + "kibana" + ], + "parameters": [ { - "name": "sortField", - "in": "query", - "description": "Determines which field is used to sort the results.", - "schema": { - "type": "string", - "enum": [ - "createdAt", - "updatedAt" - ], - "default": "createdAt" - }, - "example": "updatedAt" + "$ref": "#/components/parameters/kbn_xsrf" }, { - "name": "sortOrder", - "in": "query", - "description": "Determines the sort order.", - "schema": { - "type": "string", - "enum": [ - "asc", - "desc" - ], - "default": "desc" - }, - "example": "asc" - }, + "$ref": "#/components/parameters/space_id" + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "closure_type": { + "$ref": "#/components/schemas/closure_types" + }, + "connector": { + "description": "An object that contains the connector configuration.", + "type": "object", + "properties": { + "fields": { + "description": "An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value.", + "nullable": true, + "type": "object", + "properties": { + "caseId": { + "description": "The case identifier for Swimlane connectors.", + "type": "string" + }, + "category": { + "description": "The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors.", + "type": "string" + }, + "destIp": { + "description": "A comma-separated list of destination IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "impact": { + "description": "The effect an incident had on business for ServiceNow ITSM connectors.", + "type": "string" + }, + "issueType": { + "description": "The type of issue for Jira connectors.", + "type": "string" + }, + "issueTypes": { + "description": "The type of incident for IBM Resilient connectors.", + "type": "array", + "items": { + "type": "number" + } + }, + "malwareHash": { + "description": "A comma-separated list of malware hashes for ServiceNow SecOps connectors.", + "type": "string" + }, + "malwareUrl": { + "description": "A comma-separated list of malware URLs for ServiceNow SecOps connectors.", + "type": "string" + }, + "parent": { + "description": "The key of the parent issue, when the issue type is sub-task for Jira connectors.", + "type": "string" + }, + "priority": { + "description": "The priority of the issue for Jira and ServiceNow SecOps connectors.", + "type": "string" + }, + "severity": { + "description": "The severity of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "severityCode": { + "description": "The severity code of the incident for IBM Resilient connectors.", + "type": "number" + }, + "sourceIp": { + "description": "A comma-separated list of source IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "subcategory": { + "description": "The subcategory of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "urgency": { + "description": "The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors.", + "type": "string" + } + }, + "example": null + }, + "id": { + "description": "The identifier for the connector. To create a case without a connector, use `none`.", + "type": "string", + "example": "none" + }, + "name": { + "description": "The name of the connector. To create a case without a connector, use `none`.", + "type": "string", + "example": "none" + }, + "type": { + "$ref": "#/components/schemas/connector_types" + } + }, + "required": [ + "fields", + "id", + "name", + "type" + ] + }, + "owner": { + "$ref": "#/components/schemas/owners" + }, + "settings": { + "description": "An object that contains the case settings.", + "type": "object", + "properties": { + "syncAlerts": { + "description": "Turns alert syncing on or off.", + "type": "boolean", + "example": true + } + }, + "required": [ + "syncAlerts" + ] + } + }, + "required": [ + "closure_type", + "connector", + "owner" + ] + } + } + } + }, + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json; charset=utf-8": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "closure_type": { + "$ref": "#/components/schemas/closure_types" + }, + "connector": { + "type": "object", + "properties": { + "fields": { + "description": "An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value.", + "nullable": true, + "type": "object", + "properties": { + "caseId": { + "description": "The case identifier for Swimlane connectors.", + "type": "string" + }, + "category": { + "description": "The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors.", + "type": "string" + }, + "destIp": { + "description": "A comma-separated list of destination IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "impact": { + "description": "The effect an incident had on business for ServiceNow ITSM connectors.", + "type": "string" + }, + "issueType": { + "description": "The type of issue for Jira connectors.", + "type": "string" + }, + "issueTypes": { + "description": "The type of incident for IBM Resilient connectors.", + "type": "array", + "items": { + "type": "number" + } + }, + "malwareHash": { + "description": "A comma-separated list of malware hashes for ServiceNow SecOps connectors.", + "type": "string" + }, + "malwareUrl": { + "description": "A comma-separated list of malware URLs for ServiceNow SecOps connectors.", + "type": "string" + }, + "parent": { + "description": "The key of the parent issue, when the issue type is sub-task for Jira connectors.", + "type": "string" + }, + "priority": { + "description": "The priority of the issue for Jira and ServiceNow SecOps connectors.", + "type": "string" + }, + "severity": { + "description": "The severity of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "severityCode": { + "description": "The severity code of the incident for IBM Resilient connectors.", + "type": "number" + }, + "sourceIp": { + "description": "A comma-separated list of source IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "subcategory": { + "description": "The subcategory of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "urgency": { + "description": "The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors.", + "type": "string" + } + }, + "example": null + }, + "id": { + "description": "The identifier for the connector. To create a case without a connector, use `none`.", + "type": "string", + "example": "none" + }, + "name": { + "description": "The name of the connector. To create a case without a connector, use `none`.", + "type": "string", + "example": "none" + }, + "type": { + "$ref": "#/components/schemas/connector_types" + } + } + }, + "created_at": { + "type": "string", + "format": "date-time", + "example": "2022-06-01T17:07:17.767Z" + }, + "created_by": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": null + }, + "full_name": { + "type": "string", + "example": null + }, + "username": { + "type": "string", + "example": "elastic" + } + } + }, + "error": { + "type": "string", + "example": null + }, + "id": { + "type": "string", + "example": "4a97a440-e1cd-11ec-be9b-9b1838238ee6" + }, + "mappings": { + "type": "array", + "items": { + "type": "object", + "properties": { + "action_type": { + "type": "string", + "example": "overwrite" + }, + "source": { + "type": "string", + "example": "title" + }, + "target": { + "type": "string", + "example": "summary" + } + } + } + }, + "owner": { + "$ref": "#/components/schemas/owners" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "example": "2022-06-01T19:58:48.169Z" + }, + "updated_by": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": null + }, + "full_name": { + "type": "string", + "example": null + }, + "username": { + "type": "string", + "example": "elastic" + } + }, + "nullable": true + }, + "version": { + "type": "string", + "example": "WzIwNzMsMV0=" + } + } + } + } + } + } + } + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "servers": [ + { + "url": "https://localhost:5601" + } + ] + }, + "/s/{spaceId}/api/cases/configure/{configurationId}": { + "patch": { + "description": "Updates external connection details, such as the closure type and default connector for cases. You must have all privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the owner of the case configuration. Connectors are used to interface with external systems. You must create a connector before you can it in your cases. Refer to the add connectors API.\n", + "tags": [ + "cases", + "kibana" + ], + "parameters": [ { - "name": "status", - "in": "query", - "description": "Filters the returned cases by state.", - "schema": { - "type": "string", - "enum": [ - "closed", - "in-progress", - "open" - ] - }, - "example": "open" + "$ref": "#/components/parameters/kbn_xsrf" }, { - "name": "tags", - "in": "query", - "description": "Filters the returned cases by tags.", - "schema": { - "oneOf": [ - { - "type": "string" - }, - { - "type": "array", - "items": { - "type": "string" - } - } - ] - }, - "example": "phishing" + "$ref": "#/components/parameters/configuration_id" }, { - "name": "to", - "in": "query", - "description": "[preview] Returns only cases that were created before a specific date. The date must be specified as a KQL data range or date match expression. This functionality is in technical preview and may be changed or removed in a future release. Elastic will apply best effort to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.\n", - "schema": { - "type": "string" - }, - "example": "now+1d" + "$ref": "#/components/parameters/space_id" } ], - "responses": { - "200": { - "description": "Indicates a successful call.", - "content": { - "application/json; charset=utf-8": { - "schema": { - "type": "object", - "properties": { - "cases": { - "type": "array", - "items": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "closure_type": { + "$ref": "#/components/schemas/closure_types" + }, + "connector": { + "description": "An object that contains the connector configuration.", + "type": "object", + "properties": { + "fields": { + "description": "An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value.", + "nullable": true, "type": "object", "properties": { - "closed_at": { - "type": "string", - "format": "date-time", - "nullable": true, - "example": null + "caseId": { + "description": "The case identifier for Swimlane connectors.", + "type": "string" }, - "closed_by": { - "type": "object", - "properties": { - "email": { - "type": "string" - }, - "full_name": { - "type": "string" - }, - "username": { - "type": "string" - } - }, - "nullable": true, - "example": null + "category": { + "description": "The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors.", + "type": "string" }, - "comments": { + "destIp": { + "description": "A comma-separated list of destination IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "impact": { + "description": "The effect an incident had on business for ServiceNow ITSM connectors.", + "type": "string" + }, + "issueType": { + "description": "The type of issue for Jira connectors.", + "type": "string" + }, + "issueTypes": { + "description": "The type of incident for IBM Resilient connectors.", "type": "array", "items": { - "type": "string" - }, - "example": [] - }, - "connector": { - "type": "object", - "properties": { - "fields": { - "description": "An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value.", - "nullable": true, - "type": "object", - "properties": { - "caseId": { - "description": "The case identifier for Swimlane connectors.", - "type": "string" - }, - "category": { - "description": "The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors.", - "type": "string" - }, - "destIp": { - "description": "A comma-separated list of destination IPs for ServiceNow SecOps connectors.", - "type": "string" - }, - "impact": { - "description": "The effect an incident had on business for ServiceNow ITSM connectors.", - "type": "string" - }, - "issueType": { - "description": "The type of issue for Jira connectors.", - "type": "string" - }, - "issueTypes": { - "description": "The type of incident for IBM Resilient connectors.", - "type": "array", - "items": { - "type": "number" - } - }, - "malwareHash": { - "description": "A comma-separated list of malware hashes for ServiceNow SecOps connectors.", - "type": "string" - }, - "malwareUrl": { - "description": "A comma-separated list of malware URLs for ServiceNow SecOps connectors.", - "type": "string" - }, - "parent": { - "description": "The key of the parent issue, when the issue type is sub-task for Jira connectors.", - "type": "string" - }, - "priority": { - "description": "The priority of the issue for Jira and ServiceNow SecOps connectors.", - "type": "string" - }, - "severity": { - "description": "The severity of the incident for ServiceNow ITSM connectors.", - "type": "string" - }, - "severityCode": { - "description": "The severity code of the incident for IBM Resilient connectors.", - "type": "number" - }, - "sourceIp": { - "description": "A comma-separated list of source IPs for ServiceNow SecOps connectors.", - "type": "string" - }, - "subcategory": { - "description": "The subcategory of the incident for ServiceNow ITSM connectors.", - "type": "string" - }, - "urgency": { - "description": "The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors.", - "type": "string" - } - }, - "required": [ - "fields", - "id", - "name", - "type" - ] - }, - "id": { - "description": "The identifier for the connector. To create a case without a connector, use `none`.", - "type": "string" - }, - "name": { - "description": "The name of the connector. To create a case without a connector, use `none`.", - "type": "string" - }, - "type": { - "$ref": "#/components/schemas/connector_types" - } + "type": "number" } }, - "created_at": { - "type": "string", - "format": "date-time", - "example": "2022-05-13T09:16:17.416Z" + "malwareHash": { + "description": "A comma-separated list of malware hashes for ServiceNow SecOps connectors.", + "type": "string" + }, + "malwareUrl": { + "description": "A comma-separated list of malware URLs for ServiceNow SecOps connectors.", + "type": "string" + }, + "parent": { + "description": "The key of the parent issue, when the issue type is sub-task for Jira connectors.", + "type": "string" + }, + "priority": { + "description": "The priority of the issue for Jira and ServiceNow SecOps connectors.", + "type": "string" + }, + "severity": { + "description": "The severity of the incident for ServiceNow ITSM connectors.", + "type": "string" }, - "created_by": { - "type": "object", - "properties": { - "email": { - "type": "string", - "example": "ahunley@imf.usa.gov" - }, - "full_name": { - "type": "string", - "example": "Alan Hunley" - }, - "username": { - "type": "string", - "example": "ahunley" - } - } + "severityCode": { + "description": "The severity code of the incident for IBM Resilient connectors.", + "type": "number" }, - "description": { - "type": "string", - "example": "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active" + "sourceIp": { + "description": "A comma-separated list of source IPs for ServiceNow SecOps connectors.", + "type": "string" }, - "duration": { - "type": "integer", - "description": "The elapsed time from the creation of the case to its closure (in seconds). If the case has not been closed, the duration is set to null.", - "example": 120 + "subcategory": { + "description": "The subcategory of the incident for ServiceNow ITSM connectors.", + "type": "string" }, - "external_service": { + "urgency": { + "description": "The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors.", + "type": "string" + } + }, + "example": null + }, + "id": { + "description": "The identifier for the connector. To create a case without a connector, use `none`.", + "type": "string", + "example": "none" + }, + "name": { + "description": "The name of the connector. To create a case without a connector, use `none`.", + "type": "string", + "example": "none" + }, + "type": { + "$ref": "#/components/schemas/connector_types" + } + }, + "required": [ + "fields", + "id", + "name", + "type" + ] + }, + "version": { + "description": "The version of the connector. To retrieve the version value, use the get configuration API.\n", + "type": "string", + "example": "WzIwMiwxXQ==" + } + }, + "required": [ + "version" + ] + } + } + } + }, + "responses": { + "200": { + "description": "Indicates a successful call.", + "content": { + "application/json; charset=utf-8": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "closure_type": { + "$ref": "#/components/schemas/closure_types" + }, + "connector": { + "type": "object", + "properties": { + "fields": { + "description": "An object containing the connector fields. To create a case without a connector, specify null. If you want to omit any individual field, specify null as its value.", + "nullable": true, "type": "object", "properties": { - "connector_id": { + "caseId": { + "description": "The case identifier for Swimlane connectors.", "type": "string" }, - "connector_name": { + "category": { + "description": "The category of the incident for ServiceNow ITSM and ServiceNow SecOps connectors.", "type": "string" }, - "external_id": { + "destIp": { + "description": "A comma-separated list of destination IPs for ServiceNow SecOps connectors.", "type": "string" }, - "external_title": { + "impact": { + "description": "The effect an incident had on business for ServiceNow ITSM connectors.", "type": "string" }, - "external_url": { + "issueType": { + "description": "The type of issue for Jira connectors.", "type": "string" }, - "pushed_at": { - "type": "string", - "format": "date-time" + "issueTypes": { + "description": "The type of incident for IBM Resilient connectors.", + "type": "array", + "items": { + "type": "number" + } }, - "pushed_by": { - "type": "object", - "properties": { - "email": { - "type": "string" - }, - "full_name": { - "type": "string" - }, - "username": { - "type": "string" - } - }, - "nullable": true, - "example": null + "malwareHash": { + "description": "A comma-separated list of malware hashes for ServiceNow SecOps connectors.", + "type": "string" + }, + "malwareUrl": { + "description": "A comma-separated list of malware URLs for ServiceNow SecOps connectors.", + "type": "string" + }, + "parent": { + "description": "The key of the parent issue, when the issue type is sub-task for Jira connectors.", + "type": "string" + }, + "priority": { + "description": "The priority of the issue for Jira and ServiceNow SecOps connectors.", + "type": "string" + }, + "severity": { + "description": "The severity of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "severityCode": { + "description": "The severity code of the incident for IBM Resilient connectors.", + "type": "number" + }, + "sourceIp": { + "description": "A comma-separated list of source IPs for ServiceNow SecOps connectors.", + "type": "string" + }, + "subcategory": { + "description": "The subcategory of the incident for ServiceNow ITSM connectors.", + "type": "string" + }, + "urgency": { + "description": "The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors.", + "type": "string" } - } + }, + "example": null }, "id": { + "description": "The identifier for the connector. To create a case without a connector, use `none`.", "type": "string", - "example": "66b9aa00-94fa-11ea-9f74-e7e108796192" - }, - "owner": { - "$ref": "#/components/schemas/owners" - }, - "settings": { - "type": "object", - "properties": { - "syncAlerts": { - "type": "boolean", - "example": true - } - } - }, - "severity": { - "$ref": "#/components/schemas/severity_property" - }, - "status": { - "$ref": "#/components/schemas/status" - }, - "tags": { - "type": "array", - "items": { - "type": "string" - }, - "example": [ - "phishing", - "social engineering", - "bubblegum" - ] + "example": "none" }, - "title": { + "name": { + "description": "The name of the connector. To create a case without a connector, use `none`.", "type": "string", - "example": "This case will self-destruct in 5 seconds" - }, - "totalAlerts": { - "type": "integer", - "example": 0 + "example": "none" }, - "totalComment": { - "type": "integer", - "example": 0 + "type": { + "$ref": "#/components/schemas/connector_types" + } + } + }, + "created_at": { + "type": "string", + "format": "date-time", + "example": "2022-06-01T17:07:17.767Z" + }, + "created_by": { + "type": "object", + "properties": { + "email": { + "type": "string", + "example": null }, - "updated_at": { + "full_name": { "type": "string", - "format": "date-time", - "nullable": true, "example": null }, - "updated_by": { - "type": "object", - "properties": { - "email": { - "type": "string" - }, - "full_name": { - "type": "string" - }, - "username": { - "type": "string" - } + "username": { + "type": "string", + "example": "elastic" + } + } + }, + "error": { + "type": "string", + "example": null + }, + "id": { + "type": "string", + "example": "4a97a440-e1cd-11ec-be9b-9b1838238ee6" + }, + "mappings": { + "type": "array", + "items": { + "type": "object", + "properties": { + "action_type": { + "type": "string", + "example": "overwrite" }, - "nullable": true, + "source": { + "type": "string", + "example": "title" + }, + "target": { + "type": "string", + "example": "summary" + } + } + } + }, + "owner": { + "$ref": "#/components/schemas/owners" + }, + "updated_at": { + "type": "string", + "format": "date-time", + "nullable": true, + "example": "2022-06-01T19:58:48.169Z" + }, + "updated_by": { + "type": "object", + "properties": { + "email": { + "type": "string", "example": null }, - "version": { + "full_name": { "type": "string", - "example": "WzUzMiwxXQ==" + "example": null + }, + "username": { + "type": "string", + "example": "elastic" } - } + }, + "nullable": true + }, + "version": { + "type": "string", + "example": "WzIwNzMsMV0=" } - }, - "count_closed_cases": { - "type": "integer" - }, - "count_in_progress_cases": { - "type": "integer" - }, - "count_open_cases": { - "type": "integer" - }, - "page": { - "type": "integer" - }, - "per_page": { - "type": "integer" - }, - "total": { - "type": "integer" } } - }, - "examples": { - "findCaseResponse": { - "$ref": "#/components/examples/find_case_response" - } } } } @@ -2958,7 +4858,7 @@ "owner": { "in": "query", "name": "owner", - "description": "A filter to limit the response to a specific set of applications. If this parameter is omitted, the response contains information about all the cases that the user has access to read.", + "description": "A filter to limit the response to a specific set of applications. If this parameter is omitted, the response contains information about all the cases that the user has access to read.\n", "schema": { "oneOf": [ { @@ -2988,6 +4888,26 @@ ] } }, + "alert_id": { + "in": "path", + "name": "alertId", + "description": "An identifier for the alert.", + "required": true, + "schema": { + "type": "string", + "example": "09f0c261e39e36351d75995b78bb83673774d1bc2cca9df2d15f0e5c0a99a540" + } + }, + "configuration_id": { + "in": "path", + "name": "configurationId", + "description": "An identifier for the configuration.", + "required": true, + "schema": { + "type": "string", + "example": "3297a0f0-b5ec-11ec-b141-0fdb20a7f9a9" + } + }, "space_id": { "in": "path", "name": "spaceId", @@ -3010,16 +4930,18 @@ ".servicenow", ".servicenow-sir", ".swimlane" - ] + ], + "example": ".none" }, "owners": { "type": "string", - "description": "Owner apps", + "description": "The application that owns the cases: Stack Management, Observability, or Elastic Security.\n", "enum": [ "cases", "observability", "securitySolution" - ] + ], + "example": "cases" }, "severity_property": { "type": "string", @@ -3040,6 +4962,15 @@ "in-progress", "open" ] + }, + "closure_types": { + "type": "string", + "description": "Indicates whether a case is automatically closed when it is pushed to external systems (`close-by-pushing`) or not automatically closed (`close-by-user`).", + "enum": [ + "close-by-pushing", + "close-by-user" + ], + "example": "close-by-user" } }, "examples": { diff --git a/x-pack/plugins/cases/docs/openapi/bundled.yaml b/x-pack/plugins/cases/docs/openapi/bundled.yaml index dbf11fc42e8924..16a584a40877b9 100644 --- a/x-pack/plugins/cases/docs/openapi/bundled.yaml +++ b/x-pack/plugins/cases/docs/openapi/bundled.yaml @@ -118,23 +118,26 @@ paths: The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors. type: string - required: - - fields - - id - - name - - type + example: null id: description: >- The identifier for the connector. To create a case without a connector, use `none`. type: string + example: none name: description: >- The name of the connector. To create a case without a connector, use `none`. type: string + example: none type: $ref: '#/components/schemas/connector_types' + required: + - fields + - id + - name + - type description: description: The description for the case. type: string @@ -280,21 +283,19 @@ paths: The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors. type: string - required: - - fields - - id - - name - - type + example: null id: description: >- The identifier for the connector. To create a case without a connector, use `none`. type: string + example: none name: description: >- The name of the connector. To create a case without a connector, use `none`. type: string + example: none type: $ref: '#/components/schemas/connector_types' created_at: @@ -306,13 +307,13 @@ paths: properties: email: type: string - example: ahunley@imf.usa.gov + example: null full_name: type: string - example: Alan Hunley + example: null username: type: string - example: ahunley + example: elastic description: type: string example: >- @@ -543,23 +544,26 @@ paths: The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors. type: string - required: - - fields - - id - - name - - type + example: null id: description: >- The identifier for the connector. To create a case without a connector, use `none`. type: string + example: none name: description: >- The name of the connector. To create a case without a connector, use `none`. type: string + example: none type: $ref: '#/components/schemas/connector_types' + required: + - fields + - id + - name + - type description: description: The description for the case. type: string @@ -705,21 +709,19 @@ paths: The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors. type: string - required: - - fields - - id - - name - - type + example: null id: description: >- The identifier for the connector. To create a case without a connector, use `none`. type: string + example: none name: description: >- The name of the connector. To create a case without a connector, use `none`. type: string + example: none type: $ref: '#/components/schemas/connector_types' created_at: @@ -731,13 +733,13 @@ paths: properties: email: type: string - example: ahunley@imf.usa.gov + example: null full_name: type: string - example: Alan Hunley + example: null username: type: string - example: ahunley + example: elastic description: type: string example: >- @@ -874,7 +876,7 @@ paths: schema: type: string example: now-1d - x-preview: true + x-technical-preview: true - $ref: '#/components/parameters/owner' - name: page in: query @@ -965,7 +967,7 @@ paths: schema: type: string example: now%2B1d - x-preview: true + x-technical-preview: true responses: '200': description: Indicates a successful call. @@ -1086,21 +1088,19 @@ paths: can be delayed for ServiceNow ITSM connectors. type: string - required: - - fields - - id - - name - - type + example: null id: description: >- The identifier for the connector. To create a case without a connector, use `none`. type: string + example: none name: description: >- The name of the connector. To create a case without a connector, use `none`. type: string + example: none type: $ref: '#/components/schemas/connector_types' created_at: @@ -1112,13 +1112,13 @@ paths: properties: email: type: string - example: ahunley@imf.usa.gov + example: null full_name: type: string - example: Alan Hunley + example: null username: type: string - example: ahunley + example: elastic description: type: string example: >- @@ -1230,25 +1230,254 @@ paths: - url: https://localhost:5601 servers: - url: https://localhost:5601 - /s/{spaceId}/api/cases: + /api/cases/alerts/{alertId}: + get: + description: > + Returns the cases associated with a specific alert. You must have read + privileges for the **Cases** feature in the **Management**, + **Observability**, or **Security** section of the Kibana feature + privileges, depending on the owner of the cases you're seeking. + x-technical-preview: true + tags: + - cases + - kibana + parameters: + - $ref: '#/components/parameters/alert_id' + - $ref: '#/components/parameters/owner' + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: array + items: + type: object + properties: + id: + type: string + description: The case identifier. + title: + type: string + description: The case title. + example: + - id: 06116b80-e1c3-11ec-be9b-9b1838238ee6 + title: security_case + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 + /api/cases/configure: + get: + description: > + Retrieves external connection details, such as the closure type and + default connector for cases. You must have read privileges for the + **Cases** feature in the **Management**, **Observability**, or + **Security** section of the Kibana feature privileges, depending on the + owner of the case configuration. + tags: + - cases + - kibana + parameters: + - $ref: '#/components/parameters/owner' + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: array + items: + type: object + properties: + closure_type: + $ref: '#/components/schemas/closure_types' + connector: + type: object + properties: + fields: + description: >- + An object containing the connector fields. To create + a case without a connector, specify null. If you + want to omit any individual field, specify null as + its value. + nullable: true + type: object + properties: + caseId: + description: The case identifier for Swimlane connectors. + type: string + category: + description: >- + The category of the incident for ServiceNow ITSM + and ServiceNow SecOps connectors. + type: string + destIp: + description: >- + A comma-separated list of destination IPs for + ServiceNow SecOps connectors. + type: string + impact: + description: >- + The effect an incident had on business for + ServiceNow ITSM connectors. + type: string + issueType: + description: The type of issue for Jira connectors. + type: string + issueTypes: + description: >- + The type of incident for IBM Resilient + connectors. + type: array + items: + type: number + malwareHash: + description: >- + A comma-separated list of malware hashes for + ServiceNow SecOps connectors. + type: string + malwareUrl: + description: >- + A comma-separated list of malware URLs for + ServiceNow SecOps connectors. + type: string + parent: + description: >- + The key of the parent issue, when the issue type + is sub-task for Jira connectors. + type: string + priority: + description: >- + The priority of the issue for Jira and + ServiceNow SecOps connectors. + type: string + severity: + description: >- + The severity of the incident for ServiceNow ITSM + connectors. + type: string + severityCode: + description: >- + The severity code of the incident for IBM + Resilient connectors. + type: number + sourceIp: + description: >- + A comma-separated list of source IPs for + ServiceNow SecOps connectors. + type: string + subcategory: + description: >- + The subcategory of the incident for ServiceNow + ITSM connectors. + type: string + urgency: + description: >- + The extent to which the incident resolution can + be delayed for ServiceNow ITSM connectors. + type: string + example: null + id: + description: >- + The identifier for the connector. To create a case + without a connector, use `none`. + type: string + example: none + name: + description: >- + The name of the connector. To create a case without + a connector, use `none`. + type: string + example: none + type: + $ref: '#/components/schemas/connector_types' + created_at: + type: string + format: date-time + example: '2022-06-01T17:07:17.767Z' + created_by: + type: object + properties: + email: + type: string + example: null + full_name: + type: string + example: null + username: + type: string + example: elastic + error: + type: string + example: null + id: + type: string + example: 4a97a440-e1cd-11ec-be9b-9b1838238ee6 + mappings: + type: array + items: + type: object + properties: + action_type: + type: string + example: overwrite + source: + type: string + example: title + target: + type: string + example: summary + owner: + $ref: '#/components/schemas/owners' + updated_at: + type: string + format: date-time + nullable: true + example: '2022-06-01T19:58:48.169Z' + updated_by: + type: object + properties: + email: + type: string + example: null + full_name: + type: string + example: null + username: + type: string + example: elastic + nullable: true + version: + type: string + example: WzIwNzMsMV0= + servers: + - url: https://localhost:5601 post: description: > - Creates a case. You must have all privileges for the **Cases** feature - in the **Management**, **Observability**, or **Security** section of the - Kibana feature privileges, depending on the owner of the case you're - creating. + Sets external connection details, such as the closure type and default + connector for cases. You must have all privileges for the **Cases** + feature in the **Management**, **Observability**, or **Security** + section of the Kibana feature privileges, depending on the owner of the + case configuration. Connectors are used to interface with external + systems. You must create a connector before you can use it in your + cases. Refer to the add connectors API. If you set a default connector, + it is automatically selected when you create cases in Kibana. If you use + the create case API, however, you must still specify all of the + connector details. tags: - cases - kibana parameters: - $ref: '#/components/parameters/kbn_xsrf' - - $ref: '#/components/parameters/space_id' requestBody: content: application/json: schema: type: object properties: + closure_type: + $ref: '#/components/schemas/closure_types' connector: description: An object that contains the connector configuration. type: object @@ -1332,26 +1561,26 @@ paths: The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors. type: string - required: - - fields - - id - - name - - type + example: null id: description: >- The identifier for the connector. To create a case without a connector, use `none`. type: string + example: none name: description: >- The name of the connector. To create a case without a connector, use `none`. type: string + example: none type: $ref: '#/components/schemas/connector_types' - description: - description: The description for the case. - type: string + required: + - fields + - id + - name + - type owner: $ref: '#/components/schemas/owners' settings: @@ -1361,1090 +1590,2569 @@ paths: syncAlerts: description: Turns alert syncing on or off. type: boolean - severity: - $ref: '#/components/schemas/severity_property' - tags: - description: >- - The words and phrases that help categorize cases. It can be - an empty array. - type: array - items: - type: string - title: - description: A title for the case. - type: string + example: true + required: + - syncAlerts required: + - closure_type - connector - - description - owner - - settings - - tags - - title - examples: - createCaseRequest: - $ref: '#/components/examples/create_case_request' responses: '200': description: Indicates a successful call. content: application/json; charset=utf-8: schema: - type: object - properties: - closed_at: - type: string - format: date-time - nullable: true - example: null - closed_by: - type: object - properties: - email: - type: string - full_name: - type: string - username: - type: string - nullable: true - example: null - comments: - type: array - items: + type: array + items: + type: object + properties: + closure_type: + $ref: '#/components/schemas/closure_types' + connector: + type: object + properties: + fields: + description: >- + An object containing the connector fields. To create + a case without a connector, specify null. If you + want to omit any individual field, specify null as + its value. + nullable: true + type: object + properties: + caseId: + description: The case identifier for Swimlane connectors. + type: string + category: + description: >- + The category of the incident for ServiceNow ITSM + and ServiceNow SecOps connectors. + type: string + destIp: + description: >- + A comma-separated list of destination IPs for + ServiceNow SecOps connectors. + type: string + impact: + description: >- + The effect an incident had on business for + ServiceNow ITSM connectors. + type: string + issueType: + description: The type of issue for Jira connectors. + type: string + issueTypes: + description: >- + The type of incident for IBM Resilient + connectors. + type: array + items: + type: number + malwareHash: + description: >- + A comma-separated list of malware hashes for + ServiceNow SecOps connectors. + type: string + malwareUrl: + description: >- + A comma-separated list of malware URLs for + ServiceNow SecOps connectors. + type: string + parent: + description: >- + The key of the parent issue, when the issue type + is sub-task for Jira connectors. + type: string + priority: + description: >- + The priority of the issue for Jira and + ServiceNow SecOps connectors. + type: string + severity: + description: >- + The severity of the incident for ServiceNow ITSM + connectors. + type: string + severityCode: + description: >- + The severity code of the incident for IBM + Resilient connectors. + type: number + sourceIp: + description: >- + A comma-separated list of source IPs for + ServiceNow SecOps connectors. + type: string + subcategory: + description: >- + The subcategory of the incident for ServiceNow + ITSM connectors. + type: string + urgency: + description: >- + The extent to which the incident resolution can + be delayed for ServiceNow ITSM connectors. + type: string + example: null + id: + description: >- + The identifier for the connector. To create a case + without a connector, use `none`. + type: string + example: none + name: + description: >- + The name of the connector. To create a case without + a connector, use `none`. + type: string + example: none + type: + $ref: '#/components/schemas/connector_types' + created_at: type: string - example: [] - connector: - type: object - properties: - fields: - description: >- - An object containing the connector fields. To create a - case without a connector, specify null. If you want to - omit any individual field, specify null as its value. - nullable: true + format: date-time + example: '2022-06-01T17:07:17.767Z' + created_by: + type: object + properties: + email: + type: string + example: null + full_name: + type: string + example: null + username: + type: string + example: elastic + error: + type: string + example: null + id: + type: string + example: 4a97a440-e1cd-11ec-be9b-9b1838238ee6 + mappings: + type: array + items: type: object properties: - caseId: - description: The case identifier for Swimlane connectors. + action_type: type: string - category: - description: >- - The category of the incident for ServiceNow ITSM - and ServiceNow SecOps connectors. + example: overwrite + source: type: string - destIp: - description: >- - A comma-separated list of destination IPs for - ServiceNow SecOps connectors. + example: title + target: type: string - impact: - description: >- - The effect an incident had on business for - ServiceNow ITSM connectors. - type: string - issueType: - description: The type of issue for Jira connectors. - type: string - issueTypes: - description: The type of incident for IBM Resilient connectors. - type: array - items: - type: number - malwareHash: - description: >- - A comma-separated list of malware hashes for - ServiceNow SecOps connectors. - type: string - malwareUrl: - description: >- - A comma-separated list of malware URLs for - ServiceNow SecOps connectors. - type: string - parent: - description: >- - The key of the parent issue, when the issue type - is sub-task for Jira connectors. - type: string - priority: - description: >- - The priority of the issue for Jira and ServiceNow - SecOps connectors. - type: string - severity: - description: >- - The severity of the incident for ServiceNow ITSM - connectors. - type: string - severityCode: - description: >- - The severity code of the incident for IBM - Resilient connectors. - type: number - sourceIp: - description: >- - A comma-separated list of source IPs for - ServiceNow SecOps connectors. - type: string - subcategory: - description: >- - The subcategory of the incident for ServiceNow - ITSM connectors. - type: string - urgency: - description: >- - The extent to which the incident resolution can be - delayed for ServiceNow ITSM connectors. - type: string - required: - - fields - - id - - name - - type - id: - description: >- - The identifier for the connector. To create a case - without a connector, use `none`. - type: string - name: - description: >- - The name of the connector. To create a case without a - connector, use `none`. - type: string - type: - $ref: '#/components/schemas/connector_types' - created_at: - type: string - format: date-time - example: '2022-05-13T09:16:17.416Z' - created_by: - type: object - properties: - email: - type: string - example: ahunley@imf.usa.gov - full_name: - type: string - example: Alan Hunley - username: - type: string - example: ahunley - description: - type: string - example: >- - James Bond clicked on a highly suspicious email banner - advertising cheap holidays for underpaid civil servants. - Operation bubblegum is active. Repeat - operation - bubblegum is now active - duration: - type: integer - description: >- - The elapsed time from the creation of the case to its - closure (in seconds). If the case has not been closed, the - duration is set to null. - example: 120 - external_service: - type: object - properties: - connector_id: - type: string - connector_name: - type: string - external_id: - type: string - external_title: - type: string - external_url: - type: string - pushed_at: - type: string - format: date-time - pushed_by: - type: object - properties: - email: - type: string - full_name: - type: string - username: - type: string - nullable: true - example: null - id: - type: string - example: 66b9aa00-94fa-11ea-9f74-e7e108796192 - owner: - $ref: '#/components/schemas/owners' - settings: - type: object - properties: - syncAlerts: - type: boolean - example: true - severity: - $ref: '#/components/schemas/severity_property' - status: - $ref: '#/components/schemas/status' - tags: - type: array - items: + example: summary + owner: + $ref: '#/components/schemas/owners' + updated_at: type: string - example: - - phishing - - social engineering - - bubblegum - title: - type: string - example: This case will self-destruct in 5 seconds - totalAlerts: - type: integer - example: 0 - totalComment: - type: integer - example: 0 - updated_at: - type: string - format: date-time - nullable: true - example: null - updated_by: - type: object - properties: - email: - type: string - full_name: - type: string - username: - type: string - nullable: true - example: null - version: - type: string - example: WzUzMiwxXQ== - examples: - createCaseResponse: - $ref: '#/components/examples/create_case_response' - servers: - - url: https://localhost:5601 - delete: - description: > - Deletes one or more cases. You must have all privileges for the - **Cases** feature in the **Management**, **Observability**, or - **Security** section of the Kibana feature privileges, depending on the - owner of the cases you're deleting. - tags: - - cases - - kibana - parameters: - - $ref: '#/components/parameters/kbn_xsrf' - - $ref: '#/components/parameters/space_id' - - name: ids - description: >- - The cases that you want to removed. All non-ASCII characters must be - URL encoded. - in: query - required: true - schema: - type: string - example: d4e7abb0-b462-11ec-9a8d-698504725a43 - responses: - '204': - description: Indicates a successful call. + format: date-time + nullable: true + example: '2022-06-01T19:58:48.169Z' + updated_by: + type: object + properties: + email: + type: string + example: null + full_name: + type: string + example: null + username: + type: string + example: elastic + nullable: true + version: + type: string + example: WzIwNzMsMV0= servers: - url: https://localhost:5601 + servers: + - url: https://localhost:5601 + /api/cases/configure/{configurationId}: patch: description: > - Updates one or more cases. You must have all privileges for the + Updates external connection details, such as the closure type and + default connector for cases. You must have all privileges for the **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the - owner of the case you're updating. + owner of the case configuration. Connectors are used to interface with + external systems. You must create a connector before you can it in your + cases. Refer to the add connectors API. tags: - cases - kibana parameters: - $ref: '#/components/parameters/kbn_xsrf' - - $ref: '#/components/parameters/space_id' + - $ref: '#/components/parameters/configuration_id' requestBody: content: application/json: schema: type: object properties: - cases: - type: array - items: - type: object - properties: - connector: - description: An object that contains the connector configuration. - type: object - properties: - fields: - description: >- - An object containing the connector fields. To - create a case without a connector, specify null. - If you want to omit any individual field, specify - null as its value. - nullable: true - type: object - properties: - caseId: - description: The case identifier for Swimlane connectors. - type: string - category: - description: >- - The category of the incident for ServiceNow - ITSM and ServiceNow SecOps connectors. - type: string - destIp: - description: >- - A comma-separated list of destination IPs for - ServiceNow SecOps connectors. - type: string - impact: - description: >- - The effect an incident had on business for - ServiceNow ITSM connectors. - type: string - issueType: - description: The type of issue for Jira connectors. - type: string - issueTypes: - description: >- - The type of incident for IBM Resilient - connectors. - type: array - items: - type: number - malwareHash: - description: >- - A comma-separated list of malware hashes for - ServiceNow SecOps connectors. - type: string - malwareUrl: - description: >- - A comma-separated list of malware URLs for - ServiceNow SecOps connectors. - type: string - parent: - description: >- - The key of the parent issue, when the issue - type is sub-task for Jira connectors. - type: string - priority: - description: >- - The priority of the issue for Jira and - ServiceNow SecOps connectors. - type: string - severity: - description: >- - The severity of the incident for ServiceNow - ITSM connectors. - type: string - severityCode: - description: >- - The severity code of the incident for IBM - Resilient connectors. + closure_type: + $ref: '#/components/schemas/closure_types' + connector: + description: An object that contains the connector configuration. + type: object + properties: + fields: + description: >- + An object containing the connector fields. To create a + case without a connector, specify null. If you want to + omit any individual field, specify null as its value. + nullable: true + type: object + properties: + caseId: + description: The case identifier for Swimlane connectors. + type: string + category: + description: >- + The category of the incident for ServiceNow ITSM and + ServiceNow SecOps connectors. + type: string + destIp: + description: >- + A comma-separated list of destination IPs for + ServiceNow SecOps connectors. + type: string + impact: + description: >- + The effect an incident had on business for + ServiceNow ITSM connectors. + type: string + issueType: + description: The type of issue for Jira connectors. + type: string + issueTypes: + description: The type of incident for IBM Resilient connectors. + type: array + items: + type: number + malwareHash: + description: >- + A comma-separated list of malware hashes for + ServiceNow SecOps connectors. + type: string + malwareUrl: + description: >- + A comma-separated list of malware URLs for + ServiceNow SecOps connectors. + type: string + parent: + description: >- + The key of the parent issue, when the issue type is + sub-task for Jira connectors. + type: string + priority: + description: >- + The priority of the issue for Jira and ServiceNow + SecOps connectors. + type: string + severity: + description: >- + The severity of the incident for ServiceNow ITSM + connectors. + type: string + severityCode: + description: >- + The severity code of the incident for IBM Resilient + connectors. + type: number + sourceIp: + description: >- + A comma-separated list of source IPs for ServiceNow + SecOps connectors. + type: string + subcategory: + description: >- + The subcategory of the incident for ServiceNow ITSM + connectors. + type: string + urgency: + description: >- + The extent to which the incident resolution can be + delayed for ServiceNow ITSM connectors. + type: string + example: null + id: + description: >- + The identifier for the connector. To create a case + without a connector, use `none`. + type: string + example: none + name: + description: >- + The name of the connector. To create a case without a + connector, use `none`. + type: string + example: none + type: + $ref: '#/components/schemas/connector_types' + required: + - fields + - id + - name + - type + version: + description: > + The version of the connector. To retrieve the version value, + use the get configuration API. + type: string + example: WzIwMiwxXQ== + required: + - version + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: array + items: + type: object + properties: + closure_type: + $ref: '#/components/schemas/closure_types' + connector: + type: object + properties: + fields: + description: >- + An object containing the connector fields. To create + a case without a connector, specify null. If you + want to omit any individual field, specify null as + its value. + nullable: true + type: object + properties: + caseId: + description: The case identifier for Swimlane connectors. + type: string + category: + description: >- + The category of the incident for ServiceNow ITSM + and ServiceNow SecOps connectors. + type: string + destIp: + description: >- + A comma-separated list of destination IPs for + ServiceNow SecOps connectors. + type: string + impact: + description: >- + The effect an incident had on business for + ServiceNow ITSM connectors. + type: string + issueType: + description: The type of issue for Jira connectors. + type: string + issueTypes: + description: >- + The type of incident for IBM Resilient + connectors. + type: array + items: type: number - sourceIp: - description: >- - A comma-separated list of source IPs for - ServiceNow SecOps connectors. - type: string - subcategory: - description: >- - The subcategory of the incident for ServiceNow - ITSM connectors. - type: string - urgency: - description: >- - The extent to which the incident resolution - can be delayed for ServiceNow ITSM connectors. - type: string - required: - - fields - - id - - name - - type - id: - description: >- - The identifier for the connector. To create a case - without a connector, use `none`. - type: string - name: - description: >- - The name of the connector. To create a case - without a connector, use `none`. - type: string - type: - $ref: '#/components/schemas/connector_types' - description: - description: The description for the case. - type: string - id: - description: The identifier for the case. - type: string - settings: - description: An object that contains the case settings. + malwareHash: + description: >- + A comma-separated list of malware hashes for + ServiceNow SecOps connectors. + type: string + malwareUrl: + description: >- + A comma-separated list of malware URLs for + ServiceNow SecOps connectors. + type: string + parent: + description: >- + The key of the parent issue, when the issue type + is sub-task for Jira connectors. + type: string + priority: + description: >- + The priority of the issue for Jira and + ServiceNow SecOps connectors. + type: string + severity: + description: >- + The severity of the incident for ServiceNow ITSM + connectors. + type: string + severityCode: + description: >- + The severity code of the incident for IBM + Resilient connectors. + type: number + sourceIp: + description: >- + A comma-separated list of source IPs for + ServiceNow SecOps connectors. + type: string + subcategory: + description: >- + The subcategory of the incident for ServiceNow + ITSM connectors. + type: string + urgency: + description: >- + The extent to which the incident resolution can + be delayed for ServiceNow ITSM connectors. + type: string + example: null + id: + description: >- + The identifier for the connector. To create a case + without a connector, use `none`. + type: string + example: none + name: + description: >- + The name of the connector. To create a case without + a connector, use `none`. + type: string + example: none + type: + $ref: '#/components/schemas/connector_types' + created_at: + type: string + format: date-time + example: '2022-06-01T17:07:17.767Z' + created_by: + type: object + properties: + email: + type: string + example: null + full_name: + type: string + example: null + username: + type: string + example: elastic + error: + type: string + example: null + id: + type: string + example: 4a97a440-e1cd-11ec-be9b-9b1838238ee6 + mappings: + type: array + items: type: object properties: - syncAlerts: - description: Turns alert syncing on or off. - type: boolean - severity: - $ref: '#/components/schemas/severity_property' - status: - $ref: '#/components/schemas/status' - tags: - description: The words and phrases that help categorize cases. - type: array - items: + action_type: + type: string + example: overwrite + source: + type: string + example: title + target: + type: string + example: summary + owner: + $ref: '#/components/schemas/owners' + updated_at: + type: string + format: date-time + nullable: true + example: '2022-06-01T19:58:48.169Z' + updated_by: + type: object + properties: + email: + type: string + example: null + full_name: + type: string + example: null + username: + type: string + example: elastic + nullable: true + version: + type: string + example: WzIwNzMsMV0= + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 + /s/{spaceId}/api/cases: + post: + description: > + Creates a case. You must have all privileges for the **Cases** feature + in the **Management**, **Observability**, or **Security** section of the + Kibana feature privileges, depending on the owner of the case you're + creating. + tags: + - cases + - kibana + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/space_id' + requestBody: + content: + application/json: + schema: + type: object + properties: + connector: + description: An object that contains the connector configuration. + type: object + properties: + fields: + description: >- + An object containing the connector fields. To create a + case without a connector, specify null. If you want to + omit any individual field, specify null as its value. + nullable: true + type: object + properties: + caseId: + description: The case identifier for Swimlane connectors. + type: string + category: + description: >- + The category of the incident for ServiceNow ITSM and + ServiceNow SecOps connectors. + type: string + destIp: + description: >- + A comma-separated list of destination IPs for + ServiceNow SecOps connectors. + type: string + impact: + description: >- + The effect an incident had on business for + ServiceNow ITSM connectors. + type: string + issueType: + description: The type of issue for Jira connectors. + type: string + issueTypes: + description: The type of incident for IBM Resilient connectors. + type: array + items: + type: number + malwareHash: + description: >- + A comma-separated list of malware hashes for + ServiceNow SecOps connectors. + type: string + malwareUrl: + description: >- + A comma-separated list of malware URLs for + ServiceNow SecOps connectors. + type: string + parent: + description: >- + The key of the parent issue, when the issue type is + sub-task for Jira connectors. + type: string + priority: + description: >- + The priority of the issue for Jira and ServiceNow + SecOps connectors. + type: string + severity: + description: >- + The severity of the incident for ServiceNow ITSM + connectors. + type: string + severityCode: + description: >- + The severity code of the incident for IBM Resilient + connectors. + type: number + sourceIp: + description: >- + A comma-separated list of source IPs for ServiceNow + SecOps connectors. + type: string + subcategory: + description: >- + The subcategory of the incident for ServiceNow ITSM + connectors. + type: string + urgency: + description: >- + The extent to which the incident resolution can be + delayed for ServiceNow ITSM connectors. + type: string + example: null + id: + description: >- + The identifier for the connector. To create a case + without a connector, use `none`. + type: string + example: none + name: + description: >- + The name of the connector. To create a case without a + connector, use `none`. + type: string + example: none + type: + $ref: '#/components/schemas/connector_types' + required: + - fields + - id + - name + - type + description: + description: The description for the case. + type: string + owner: + $ref: '#/components/schemas/owners' + settings: + description: An object that contains the case settings. + type: object + properties: + syncAlerts: + description: Turns alert syncing on or off. + type: boolean + severity: + $ref: '#/components/schemas/severity_property' + tags: + description: >- + The words and phrases that help categorize cases. It can be + an empty array. + type: array + items: + type: string + title: + description: A title for the case. + type: string + required: + - connector + - description + - owner + - settings + - tags + - title + examples: + createCaseRequest: + $ref: '#/components/examples/create_case_request' + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: object + properties: + closed_at: + type: string + format: date-time + nullable: true + example: null + closed_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null + comments: + type: array + items: + type: string + example: [] + connector: + type: object + properties: + fields: + description: >- + An object containing the connector fields. To create a + case without a connector, specify null. If you want to + omit any individual field, specify null as its value. + nullable: true + type: object + properties: + caseId: + description: The case identifier for Swimlane connectors. + type: string + category: + description: >- + The category of the incident for ServiceNow ITSM + and ServiceNow SecOps connectors. + type: string + destIp: + description: >- + A comma-separated list of destination IPs for + ServiceNow SecOps connectors. + type: string + impact: + description: >- + The effect an incident had on business for + ServiceNow ITSM connectors. + type: string + issueType: + description: The type of issue for Jira connectors. + type: string + issueTypes: + description: The type of incident for IBM Resilient connectors. + type: array + items: + type: number + malwareHash: + description: >- + A comma-separated list of malware hashes for + ServiceNow SecOps connectors. + type: string + malwareUrl: + description: >- + A comma-separated list of malware URLs for + ServiceNow SecOps connectors. + type: string + parent: + description: >- + The key of the parent issue, when the issue type + is sub-task for Jira connectors. + type: string + priority: + description: >- + The priority of the issue for Jira and ServiceNow + SecOps connectors. + type: string + severity: + description: >- + The severity of the incident for ServiceNow ITSM + connectors. + type: string + severityCode: + description: >- + The severity code of the incident for IBM + Resilient connectors. + type: number + sourceIp: + description: >- + A comma-separated list of source IPs for + ServiceNow SecOps connectors. + type: string + subcategory: + description: >- + The subcategory of the incident for ServiceNow + ITSM connectors. + type: string + urgency: + description: >- + The extent to which the incident resolution can be + delayed for ServiceNow ITSM connectors. + type: string + example: null + id: + description: >- + The identifier for the connector. To create a case + without a connector, use `none`. + type: string + example: none + name: + description: >- + The name of the connector. To create a case without a + connector, use `none`. + type: string + example: none + type: + $ref: '#/components/schemas/connector_types' + created_at: + type: string + format: date-time + example: '2022-05-13T09:16:17.416Z' + created_by: + type: object + properties: + email: + type: string + example: null + full_name: + type: string + example: null + username: + type: string + example: elastic + description: + type: string + example: >- + James Bond clicked on a highly suspicious email banner + advertising cheap holidays for underpaid civil servants. + Operation bubblegum is active. Repeat - operation + bubblegum is now active + duration: + type: integer + description: >- + The elapsed time from the creation of the case to its + closure (in seconds). If the case has not been closed, the + duration is set to null. + example: 120 + external_service: + type: object + properties: + connector_id: + type: string + connector_name: + type: string + external_id: + type: string + external_title: + type: string + external_url: + type: string + pushed_at: + type: string + format: date-time + pushed_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null + id: + type: string + example: 66b9aa00-94fa-11ea-9f74-e7e108796192 + owner: + $ref: '#/components/schemas/owners' + settings: + type: object + properties: + syncAlerts: + type: boolean + example: true + severity: + $ref: '#/components/schemas/severity_property' + status: + $ref: '#/components/schemas/status' + tags: + type: array + items: + type: string + example: + - phishing + - social engineering + - bubblegum + title: + type: string + example: This case will self-destruct in 5 seconds + totalAlerts: + type: integer + example: 0 + totalComment: + type: integer + example: 0 + updated_at: + type: string + format: date-time + nullable: true + example: null + updated_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null + version: + type: string + example: WzUzMiwxXQ== + examples: + createCaseResponse: + $ref: '#/components/examples/create_case_response' + servers: + - url: https://localhost:5601 + delete: + description: > + Deletes one or more cases. You must have all privileges for the + **Cases** feature in the **Management**, **Observability**, or + **Security** section of the Kibana feature privileges, depending on the + owner of the cases you're deleting. + tags: + - cases + - kibana + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/space_id' + - name: ids + description: >- + The cases that you want to removed. All non-ASCII characters must be + URL encoded. + in: query + required: true + schema: + type: string + example: d4e7abb0-b462-11ec-9a8d-698504725a43 + responses: + '204': + description: Indicates a successful call. + servers: + - url: https://localhost:5601 + patch: + description: > + Updates one or more cases. You must have all privileges for the + **Cases** feature in the **Management**, **Observability**, or + **Security** section of the Kibana feature privileges, depending on the + owner of the case you're updating. + tags: + - cases + - kibana + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/space_id' + requestBody: + content: + application/json: + schema: + type: object + properties: + cases: + type: array + items: + type: object + properties: + connector: + description: An object that contains the connector configuration. + type: object + properties: + fields: + description: >- + An object containing the connector fields. To + create a case without a connector, specify null. + If you want to omit any individual field, specify + null as its value. + nullable: true + type: object + properties: + caseId: + description: The case identifier for Swimlane connectors. + type: string + category: + description: >- + The category of the incident for ServiceNow + ITSM and ServiceNow SecOps connectors. + type: string + destIp: + description: >- + A comma-separated list of destination IPs for + ServiceNow SecOps connectors. + type: string + impact: + description: >- + The effect an incident had on business for + ServiceNow ITSM connectors. + type: string + issueType: + description: The type of issue for Jira connectors. + type: string + issueTypes: + description: >- + The type of incident for IBM Resilient + connectors. + type: array + items: + type: number + malwareHash: + description: >- + A comma-separated list of malware hashes for + ServiceNow SecOps connectors. + type: string + malwareUrl: + description: >- + A comma-separated list of malware URLs for + ServiceNow SecOps connectors. + type: string + parent: + description: >- + The key of the parent issue, when the issue + type is sub-task for Jira connectors. + type: string + priority: + description: >- + The priority of the issue for Jira and + ServiceNow SecOps connectors. + type: string + severity: + description: >- + The severity of the incident for ServiceNow + ITSM connectors. + type: string + severityCode: + description: >- + The severity code of the incident for IBM + Resilient connectors. + type: number + sourceIp: + description: >- + A comma-separated list of source IPs for + ServiceNow SecOps connectors. + type: string + subcategory: + description: >- + The subcategory of the incident for ServiceNow + ITSM connectors. + type: string + urgency: + description: >- + The extent to which the incident resolution + can be delayed for ServiceNow ITSM connectors. + type: string + example: null + id: + description: >- + The identifier for the connector. To create a case + without a connector, use `none`. + type: string + example: none + name: + description: >- + The name of the connector. To create a case + without a connector, use `none`. + type: string + example: none + type: + $ref: '#/components/schemas/connector_types' + required: + - fields + - id + - name + - type + description: + description: The description for the case. + type: string + id: + description: The identifier for the case. + type: string + settings: + description: An object that contains the case settings. + type: object + properties: + syncAlerts: + description: Turns alert syncing on or off. + type: boolean + severity: + $ref: '#/components/schemas/severity_property' + status: + $ref: '#/components/schemas/status' + tags: + description: The words and phrases that help categorize cases. + type: array + items: + type: string + title: + description: A title for the case. + type: string + version: + description: The current version of the case. + type: string + required: + - id + - version + examples: + updateCaseRequest: + $ref: '#/components/examples/update_case_request' + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: object + properties: + closed_at: + type: string + format: date-time + nullable: true + example: null + closed_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null + comments: + type: array + items: + type: string + example: [] + connector: + type: object + properties: + fields: + description: >- + An object containing the connector fields. To create a + case without a connector, specify null. If you want to + omit any individual field, specify null as its value. + nullable: true + type: object + properties: + caseId: + description: The case identifier for Swimlane connectors. + type: string + category: + description: >- + The category of the incident for ServiceNow ITSM + and ServiceNow SecOps connectors. + type: string + destIp: + description: >- + A comma-separated list of destination IPs for + ServiceNow SecOps connectors. + type: string + impact: + description: >- + The effect an incident had on business for + ServiceNow ITSM connectors. + type: string + issueType: + description: The type of issue for Jira connectors. + type: string + issueTypes: + description: The type of incident for IBM Resilient connectors. + type: array + items: + type: number + malwareHash: + description: >- + A comma-separated list of malware hashes for + ServiceNow SecOps connectors. + type: string + malwareUrl: + description: >- + A comma-separated list of malware URLs for + ServiceNow SecOps connectors. + type: string + parent: + description: >- + The key of the parent issue, when the issue type + is sub-task for Jira connectors. + type: string + priority: + description: >- + The priority of the issue for Jira and ServiceNow + SecOps connectors. + type: string + severity: + description: >- + The severity of the incident for ServiceNow ITSM + connectors. + type: string + severityCode: + description: >- + The severity code of the incident for IBM + Resilient connectors. + type: number + sourceIp: + description: >- + A comma-separated list of source IPs for + ServiceNow SecOps connectors. + type: string + subcategory: + description: >- + The subcategory of the incident for ServiceNow + ITSM connectors. + type: string + urgency: + description: >- + The extent to which the incident resolution can be + delayed for ServiceNow ITSM connectors. + type: string + example: null + id: + description: >- + The identifier for the connector. To create a case + without a connector, use `none`. + type: string + example: none + name: + description: >- + The name of the connector. To create a case without a + connector, use `none`. + type: string + example: none + type: + $ref: '#/components/schemas/connector_types' + created_at: + type: string + format: date-time + example: '2022-05-13T09:16:17.416Z' + created_by: + type: object + properties: + email: + type: string + example: null + full_name: + type: string + example: null + username: + type: string + example: elastic + description: + type: string + example: >- + James Bond clicked on a highly suspicious email banner + advertising cheap holidays for underpaid civil servants. + Operation bubblegum is active. Repeat - operation + bubblegum is now active + duration: + type: integer + description: >- + The elapsed time from the creation of the case to its + closure (in seconds). If the case has not been closed, the + duration is set to null. + example: 120 + external_service: + type: object + properties: + connector_id: + type: string + connector_name: + type: string + external_id: + type: string + external_title: + type: string + external_url: + type: string + pushed_at: + type: string + format: date-time + pushed_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null + id: + type: string + example: 66b9aa00-94fa-11ea-9f74-e7e108796192 + owner: + $ref: '#/components/schemas/owners' + settings: + type: object + properties: + syncAlerts: + type: boolean + example: true + severity: + $ref: '#/components/schemas/severity_property' + status: + $ref: '#/components/schemas/status' + tags: + type: array + items: + type: string + example: + - phishing + - social engineering + - bubblegum + title: + type: string + example: This case will self-destruct in 5 seconds + totalAlerts: + type: integer + example: 0 + totalComment: + type: integer + example: 0 + updated_at: + type: string + format: date-time + nullable: true + example: null + updated_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null + version: + type: string + example: WzUzMiwxXQ== + examples: + updateCaseResponse: + $ref: '#/components/examples/update_case_response' + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 + /s/{spaceId}/api/cases/_find: + get: + description: > + Retrieves a paginated subset of cases. You must have read privileges for + the **Cases** feature in the **Management**, **Observability**, or + **Security** section of the Kibana feature privileges, depending on the + owner of the cases you're seeking. + tags: + - cases + - kibana + parameters: + - $ref: '#/components/parameters/space_id' + - name: defaultSearchOperator + in: query + description: The default operator to use for the simple_query_string. + schema: + type: string + default: OR + example: OR + - name: fields + in: query + description: The fields in the entity to return in the response. + schema: + type: array + items: + type: string + - name: from + in: query + description: > + [preview] Returns only cases that were created after a specific + date. The date must be specified as a KQL data range or date match + expression. This functionality is in technical preview and may be + changed or removed in a future release. Elastic will apply best + effort to fix any issues, but features in technical preview are not + subject to the support SLA of official GA features. + schema: + type: string + example: now-1d + - $ref: '#/components/parameters/owner' + - name: page + in: query + description: The page number to return. + schema: + type: integer + default: 1 + example: 1 + - name: perPage + in: query + description: The number of rules to return per page. + schema: + type: integer + default: 20 + example: 20 + - name: reporters + in: query + description: Filters the returned cases by the user name of the reporter. + schema: + oneOf: + - type: string + - type: array + items: + type: string + example: elastic + - name: search + in: query + description: >- + An Elasticsearch simple_query_string query that filters the objects + in the response. + schema: + type: string + - name: searchFields + in: query + description: The fields to perform the simple_query_string parsed query against. + schema: + oneOf: + - type: string + - type: array + items: + type: string + - $ref: '#/components/parameters/severity' + - name: sortField + in: query + description: Determines which field is used to sort the results. + schema: + type: string + enum: + - createdAt + - updatedAt + default: createdAt + example: updatedAt + - name: sortOrder + in: query + description: Determines the sort order. + schema: + type: string + enum: + - asc + - desc + default: desc + example: asc + - name: status + in: query + description: Filters the returned cases by state. + schema: + type: string + enum: + - closed + - in-progress + - open + example: open + - name: tags + in: query + description: Filters the returned cases by tags. + schema: + oneOf: + - type: string + - type: array + items: + type: string + example: phishing + - name: to + in: query + description: > + [preview] Returns only cases that were created before a specific + date. The date must be specified as a KQL data range or date match + expression. This functionality is in technical preview and may be + changed or removed in a future release. Elastic will apply best + effort to fix any issues, but features in technical preview are not + subject to the support SLA of official GA features. + schema: + type: string + example: now+1d + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: object + properties: + cases: + type: array + items: + type: object + properties: + closed_at: + type: string + format: date-time + nullable: true + example: null + closed_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null + comments: + type: array + items: + type: string + example: [] + connector: + type: object + properties: + fields: + description: >- + An object containing the connector fields. To + create a case without a connector, specify null. + If you want to omit any individual field, + specify null as its value. + nullable: true + type: object + properties: + caseId: + description: The case identifier for Swimlane connectors. + type: string + category: + description: >- + The category of the incident for ServiceNow + ITSM and ServiceNow SecOps connectors. + type: string + destIp: + description: >- + A comma-separated list of destination IPs + for ServiceNow SecOps connectors. + type: string + impact: + description: >- + The effect an incident had on business for + ServiceNow ITSM connectors. + type: string + issueType: + description: The type of issue for Jira connectors. + type: string + issueTypes: + description: >- + The type of incident for IBM Resilient + connectors. + type: array + items: + type: number + malwareHash: + description: >- + A comma-separated list of malware hashes for + ServiceNow SecOps connectors. + type: string + malwareUrl: + description: >- + A comma-separated list of malware URLs for + ServiceNow SecOps connectors. + type: string + parent: + description: >- + The key of the parent issue, when the issue + type is sub-task for Jira connectors. + type: string + priority: + description: >- + The priority of the issue for Jira and + ServiceNow SecOps connectors. + type: string + severity: + description: >- + The severity of the incident for ServiceNow + ITSM connectors. + type: string + severityCode: + description: >- + The severity code of the incident for IBM + Resilient connectors. + type: number + sourceIp: + description: >- + A comma-separated list of source IPs for + ServiceNow SecOps connectors. + type: string + subcategory: + description: >- + The subcategory of the incident for + ServiceNow ITSM connectors. + type: string + urgency: + description: >- + The extent to which the incident resolution + can be delayed for ServiceNow ITSM + connectors. + type: string + example: null + id: + description: >- + The identifier for the connector. To create a + case without a connector, use `none`. + type: string + example: none + name: + description: >- + The name of the connector. To create a case + without a connector, use `none`. + type: string + example: none + type: + $ref: '#/components/schemas/connector_types' + created_at: + type: string + format: date-time + example: '2022-05-13T09:16:17.416Z' + created_by: + type: object + properties: + email: + type: string + example: null + full_name: + type: string + example: null + username: + type: string + example: elastic + description: + type: string + example: >- + James Bond clicked on a highly suspicious email + banner advertising cheap holidays for underpaid + civil servants. Operation bubblegum is active. + Repeat - operation bubblegum is now active + duration: + type: integer + description: >- + The elapsed time from the creation of the case to + its closure (in seconds). If the case has not been + closed, the duration is set to null. + example: 120 + external_service: + type: object + properties: + connector_id: + type: string + connector_name: + type: string + external_id: + type: string + external_title: + type: string + external_url: + type: string + pushed_at: + type: string + format: date-time + pushed_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null + id: + type: string + example: 66b9aa00-94fa-11ea-9f74-e7e108796192 + owner: + $ref: '#/components/schemas/owners' + settings: + type: object + properties: + syncAlerts: + type: boolean + example: true + severity: + $ref: '#/components/schemas/severity_property' + status: + $ref: '#/components/schemas/status' + tags: + type: array + items: + type: string + example: + - phishing + - social engineering + - bubblegum + title: + type: string + example: This case will self-destruct in 5 seconds + totalAlerts: + type: integer + example: 0 + totalComment: + type: integer + example: 0 + updated_at: + type: string + format: date-time + nullable: true + example: null + updated_by: + type: object + properties: + email: + type: string + full_name: + type: string + username: + type: string + nullable: true + example: null + version: + type: string + example: WzUzMiwxXQ== + count_closed_cases: + type: integer + count_in_progress_cases: + type: integer + count_open_cases: + type: integer + page: + type: integer + per_page: + type: integer + total: + type: integer + examples: + findCaseResponse: + $ref: '#/components/examples/find_case_response' + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 + /s/{spaceId}/api/cases/alerts/{alertId}: + get: + description: > + Returns the cases associated with a specific alert. You must have read + privileges for the **Cases** feature in the **Management**, + **Observability**, or **Security** section of the Kibana feature + privileges, depending on the owner of the cases you're seeking. + x-technical-preview: true + tags: + - cases + - kibana + parameters: + - $ref: '#/components/parameters/alert_id' + - $ref: '#/components/parameters/space_id' + - $ref: '#/components/parameters/owner' + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: array + items: + type: object + properties: + id: + type: string + description: The case identifier. + title: + type: string + description: The case title. + example: + - id: 06116b80-e1c3-11ec-be9b-9b1838238ee6 + title: security_case + servers: + - url: https://localhost:5601 + servers: + - url: https://localhost:5601 + /s/{spaceId}/api/cases/configure: + get: + description: > + Retrieves external connection details, such as the closure type and + default connector for cases. You must have read privileges for the + **Cases** feature in the **Management**, **Observability**, or + **Security** section of the Kibana feature privileges, depending on the + owner of the case configuration. + tags: + - cases + - kibana + parameters: + - $ref: '#/components/parameters/space_id' + - $ref: '#/components/parameters/owner' + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: array + items: + type: object + properties: + closure_type: + $ref: '#/components/schemas/closure_types' + connector: + type: object + properties: + fields: + description: >- + An object containing the connector fields. To create + a case without a connector, specify null. If you + want to omit any individual field, specify null as + its value. + nullable: true + type: object + properties: + caseId: + description: The case identifier for Swimlane connectors. + type: string + category: + description: >- + The category of the incident for ServiceNow ITSM + and ServiceNow SecOps connectors. + type: string + destIp: + description: >- + A comma-separated list of destination IPs for + ServiceNow SecOps connectors. + type: string + impact: + description: >- + The effect an incident had on business for + ServiceNow ITSM connectors. + type: string + issueType: + description: The type of issue for Jira connectors. + type: string + issueTypes: + description: >- + The type of incident for IBM Resilient + connectors. + type: array + items: + type: number + malwareHash: + description: >- + A comma-separated list of malware hashes for + ServiceNow SecOps connectors. + type: string + malwareUrl: + description: >- + A comma-separated list of malware URLs for + ServiceNow SecOps connectors. + type: string + parent: + description: >- + The key of the parent issue, when the issue type + is sub-task for Jira connectors. + type: string + priority: + description: >- + The priority of the issue for Jira and + ServiceNow SecOps connectors. + type: string + severity: + description: >- + The severity of the incident for ServiceNow ITSM + connectors. + type: string + severityCode: + description: >- + The severity code of the incident for IBM + Resilient connectors. + type: number + sourceIp: + description: >- + A comma-separated list of source IPs for + ServiceNow SecOps connectors. + type: string + subcategory: + description: >- + The subcategory of the incident for ServiceNow + ITSM connectors. + type: string + urgency: + description: >- + The extent to which the incident resolution can + be delayed for ServiceNow ITSM connectors. + type: string + example: null + id: + description: >- + The identifier for the connector. To create a case + without a connector, use `none`. + type: string + example: none + name: + description: >- + The name of the connector. To create a case without + a connector, use `none`. + type: string + example: none + type: + $ref: '#/components/schemas/connector_types' + created_at: + type: string + format: date-time + example: '2022-06-01T17:07:17.767Z' + created_by: + type: object + properties: + email: + type: string + example: null + full_name: + type: string + example: null + username: + type: string + example: elastic + error: + type: string + example: null + id: + type: string + example: 4a97a440-e1cd-11ec-be9b-9b1838238ee6 + mappings: + type: array + items: + type: object + properties: + action_type: + type: string + example: overwrite + source: + type: string + example: title + target: + type: string + example: summary + owner: + $ref: '#/components/schemas/owners' + updated_at: + type: string + format: date-time + nullable: true + example: '2022-06-01T19:58:48.169Z' + updated_by: + type: object + properties: + email: + type: string + example: null + full_name: + type: string + example: null + username: + type: string + example: elastic + nullable: true + version: + type: string + example: WzIwNzMsMV0= + servers: + - url: https://localhost:5601 + post: + description: > + Sets external connection details, such as the closure type and default + connector for cases. You must have all privileges for the **Cases** + feature in the **Management**, **Observability**, or **Security** + section of the Kibana feature privileges, depending on the owner of the + case configuration. Connectors are used to interface with external + systems. You must create a connector before you can use it in your + cases. Refer to the add connectors API. If you set a default connector, + it is automatically selected when you create cases in Kibana. If you use + the create case API, however, you must still specify all of the + connector details. + tags: + - cases + - kibana + parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/space_id' + requestBody: + content: + application/json: + schema: + type: object + properties: + closure_type: + $ref: '#/components/schemas/closure_types' + connector: + description: An object that contains the connector configuration. + type: object + properties: + fields: + description: >- + An object containing the connector fields. To create a + case without a connector, specify null. If you want to + omit any individual field, specify null as its value. + nullable: true + type: object + properties: + caseId: + description: The case identifier for Swimlane connectors. + type: string + category: + description: >- + The category of the incident for ServiceNow ITSM and + ServiceNow SecOps connectors. + type: string + destIp: + description: >- + A comma-separated list of destination IPs for + ServiceNow SecOps connectors. + type: string + impact: + description: >- + The effect an incident had on business for + ServiceNow ITSM connectors. + type: string + issueType: + description: The type of issue for Jira connectors. + type: string + issueTypes: + description: The type of incident for IBM Resilient connectors. + type: array + items: + type: number + malwareHash: + description: >- + A comma-separated list of malware hashes for + ServiceNow SecOps connectors. + type: string + malwareUrl: + description: >- + A comma-separated list of malware URLs for + ServiceNow SecOps connectors. + type: string + parent: + description: >- + The key of the parent issue, when the issue type is + sub-task for Jira connectors. + type: string + priority: + description: >- + The priority of the issue for Jira and ServiceNow + SecOps connectors. + type: string + severity: + description: >- + The severity of the incident for ServiceNow ITSM + connectors. + type: string + severityCode: + description: >- + The severity code of the incident for IBM Resilient + connectors. + type: number + sourceIp: + description: >- + A comma-separated list of source IPs for ServiceNow + SecOps connectors. + type: string + subcategory: + description: >- + The subcategory of the incident for ServiceNow ITSM + connectors. + type: string + urgency: + description: >- + The extent to which the incident resolution can be + delayed for ServiceNow ITSM connectors. + type: string + example: null + id: + description: >- + The identifier for the connector. To create a case + without a connector, use `none`. + type: string + example: none + name: + description: >- + The name of the connector. To create a case without a + connector, use `none`. + type: string + example: none + type: + $ref: '#/components/schemas/connector_types' + required: + - fields + - id + - name + - type + owner: + $ref: '#/components/schemas/owners' + settings: + description: An object that contains the case settings. + type: object + properties: + syncAlerts: + description: Turns alert syncing on or off. + type: boolean + example: true + required: + - syncAlerts + required: + - closure_type + - connector + - owner + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: array + items: + type: object + properties: + closure_type: + $ref: '#/components/schemas/closure_types' + connector: + type: object + properties: + fields: + description: >- + An object containing the connector fields. To create + a case without a connector, specify null. If you + want to omit any individual field, specify null as + its value. + nullable: true + type: object + properties: + caseId: + description: The case identifier for Swimlane connectors. + type: string + category: + description: >- + The category of the incident for ServiceNow ITSM + and ServiceNow SecOps connectors. + type: string + destIp: + description: >- + A comma-separated list of destination IPs for + ServiceNow SecOps connectors. + type: string + impact: + description: >- + The effect an incident had on business for + ServiceNow ITSM connectors. + type: string + issueType: + description: The type of issue for Jira connectors. + type: string + issueTypes: + description: >- + The type of incident for IBM Resilient + connectors. + type: array + items: + type: number + malwareHash: + description: >- + A comma-separated list of malware hashes for + ServiceNow SecOps connectors. + type: string + malwareUrl: + description: >- + A comma-separated list of malware URLs for + ServiceNow SecOps connectors. + type: string + parent: + description: >- + The key of the parent issue, when the issue type + is sub-task for Jira connectors. + type: string + priority: + description: >- + The priority of the issue for Jira and + ServiceNow SecOps connectors. + type: string + severity: + description: >- + The severity of the incident for ServiceNow ITSM + connectors. + type: string + severityCode: + description: >- + The severity code of the incident for IBM + Resilient connectors. + type: number + sourceIp: + description: >- + A comma-separated list of source IPs for + ServiceNow SecOps connectors. + type: string + subcategory: + description: >- + The subcategory of the incident for ServiceNow + ITSM connectors. + type: string + urgency: + description: >- + The extent to which the incident resolution can + be delayed for ServiceNow ITSM connectors. + type: string + example: null + id: + description: >- + The identifier for the connector. To create a case + without a connector, use `none`. + type: string + example: none + name: + description: >- + The name of the connector. To create a case without + a connector, use `none`. + type: string + example: none + type: + $ref: '#/components/schemas/connector_types' + created_at: + type: string + format: date-time + example: '2022-06-01T17:07:17.767Z' + created_by: + type: object + properties: + email: type: string - title: - description: A title for the case. - type: string - version: - description: The current version of the case. - type: string - required: - - id - - version - examples: - updateCaseRequest: - $ref: '#/components/examples/update_case_request' - responses: - '200': - description: Indicates a successful call. - content: - application/json; charset=utf-8: - schema: - type: object - properties: - closed_at: - type: string - format: date-time - nullable: true - example: null - closed_by: - type: object - properties: - email: - type: string - full_name: - type: string - username: - type: string - nullable: true - example: null - comments: - type: array - items: + example: null + full_name: + type: string + example: null + username: + type: string + example: elastic + error: type: string - example: [] - connector: - type: object - properties: - fields: - description: >- - An object containing the connector fields. To create a - case without a connector, specify null. If you want to - omit any individual field, specify null as its value. - nullable: true - type: object - properties: - caseId: - description: The case identifier for Swimlane connectors. - type: string - category: - description: >- - The category of the incident for ServiceNow ITSM - and ServiceNow SecOps connectors. - type: string - destIp: - description: >- - A comma-separated list of destination IPs for - ServiceNow SecOps connectors. - type: string - impact: - description: >- - The effect an incident had on business for - ServiceNow ITSM connectors. - type: string - issueType: - description: The type of issue for Jira connectors. - type: string - issueTypes: - description: The type of incident for IBM Resilient connectors. - type: array - items: - type: number - malwareHash: - description: >- - A comma-separated list of malware hashes for - ServiceNow SecOps connectors. - type: string - malwareUrl: - description: >- - A comma-separated list of malware URLs for - ServiceNow SecOps connectors. - type: string - parent: - description: >- - The key of the parent issue, when the issue type - is sub-task for Jira connectors. - type: string - priority: - description: >- - The priority of the issue for Jira and ServiceNow - SecOps connectors. - type: string - severity: - description: >- - The severity of the incident for ServiceNow ITSM - connectors. - type: string - severityCode: - description: >- - The severity code of the incident for IBM - Resilient connectors. - type: number - sourceIp: - description: >- - A comma-separated list of source IPs for - ServiceNow SecOps connectors. - type: string - subcategory: - description: >- - The subcategory of the incident for ServiceNow - ITSM connectors. - type: string - urgency: - description: >- - The extent to which the incident resolution can be - delayed for ServiceNow ITSM connectors. - type: string - required: - - fields - - id - - name - - type - id: - description: >- - The identifier for the connector. To create a case - without a connector, use `none`. - type: string - name: - description: >- - The name of the connector. To create a case without a - connector, use `none`. - type: string - type: - $ref: '#/components/schemas/connector_types' - created_at: - type: string - format: date-time - example: '2022-05-13T09:16:17.416Z' - created_by: - type: object - properties: - email: - type: string - example: ahunley@imf.usa.gov - full_name: - type: string - example: Alan Hunley - username: - type: string - example: ahunley - description: - type: string - example: >- - James Bond clicked on a highly suspicious email banner - advertising cheap holidays for underpaid civil servants. - Operation bubblegum is active. Repeat - operation - bubblegum is now active - duration: - type: integer - description: >- - The elapsed time from the creation of the case to its - closure (in seconds). If the case has not been closed, the - duration is set to null. - example: 120 - external_service: - type: object - properties: - connector_id: - type: string - connector_name: - type: string - external_id: - type: string - external_title: - type: string - external_url: - type: string - pushed_at: - type: string - format: date-time - pushed_by: + example: null + id: + type: string + example: 4a97a440-e1cd-11ec-be9b-9b1838238ee6 + mappings: + type: array + items: type: object properties: - email: + action_type: type: string - full_name: + example: overwrite + source: type: string - username: + example: title + target: type: string - nullable: true - example: null - id: - type: string - example: 66b9aa00-94fa-11ea-9f74-e7e108796192 - owner: - $ref: '#/components/schemas/owners' - settings: - type: object - properties: - syncAlerts: - type: boolean - example: true - severity: - $ref: '#/components/schemas/severity_property' - status: - $ref: '#/components/schemas/status' - tags: - type: array - items: + example: summary + owner: + $ref: '#/components/schemas/owners' + updated_at: type: string - example: - - phishing - - social engineering - - bubblegum - title: - type: string - example: This case will self-destruct in 5 seconds - totalAlerts: - type: integer - example: 0 - totalComment: - type: integer - example: 0 - updated_at: - type: string - format: date-time - nullable: true - example: null - updated_by: - type: object - properties: - email: - type: string - full_name: - type: string - username: - type: string - nullable: true - example: null - version: - type: string - example: WzUzMiwxXQ== - examples: - updateCaseResponse: - $ref: '#/components/examples/update_case_response' + format: date-time + nullable: true + example: '2022-06-01T19:58:48.169Z' + updated_by: + type: object + properties: + email: + type: string + example: null + full_name: + type: string + example: null + username: + type: string + example: elastic + nullable: true + version: + type: string + example: WzIwNzMsMV0= servers: - url: https://localhost:5601 servers: - url: https://localhost:5601 - /s/{spaceId}/api/cases/_find: - get: + /s/{spaceId}/api/cases/configure/{configurationId}: + patch: description: > - Retrieves a paginated subset of cases. You must have read privileges for - the **Cases** feature in the **Management**, **Observability**, or + Updates external connection details, such as the closure type and + default connector for cases. You must have all privileges for the + **Cases** feature in the **Management**, **Observability**, or **Security** section of the Kibana feature privileges, depending on the - owner of the cases you're seeking. + owner of the case configuration. Connectors are used to interface with + external systems. You must create a connector before you can it in your + cases. Refer to the add connectors API. tags: - cases - kibana parameters: + - $ref: '#/components/parameters/kbn_xsrf' + - $ref: '#/components/parameters/configuration_id' - $ref: '#/components/parameters/space_id' - - name: defaultSearchOperator - in: query - description: The default operator to use for the simple_query_string. - schema: - type: string - default: OR - example: OR - - name: fields - in: query - description: The fields in the entity to return in the response. - schema: - type: array - items: - type: string - - name: from - in: query - description: > - [preview] Returns only cases that were created after a specific - date. The date must be specified as a KQL data range or date match - expression. This functionality is in technical preview and may be - changed or removed in a future release. Elastic will apply best - effort to fix any issues, but features in technical preview are not - subject to the support SLA of official GA features. - schema: - type: string - example: now-1d - - $ref: '#/components/parameters/owner' - - name: page - in: query - description: The page number to return. - schema: - type: integer - default: 1 - example: 1 - - name: perPage - in: query - description: The number of rules to return per page. - schema: - type: integer - default: 20 - example: 20 - - name: reporters - in: query - description: Filters the returned cases by the user name of the reporter. - schema: - oneOf: - - type: string - - type: array - items: - type: string - example: elastic - - name: search - in: query - description: >- - An Elasticsearch simple_query_string query that filters the objects - in the response. - schema: - type: string - - name: searchFields - in: query - description: The fields to perform the simple_query_string parsed query against. - schema: - oneOf: - - type: string - - type: array - items: - type: string - - $ref: '#/components/parameters/severity' - - name: sortField - in: query - description: Determines which field is used to sort the results. - schema: - type: string - enum: - - createdAt - - updatedAt - default: createdAt - example: updatedAt - - name: sortOrder - in: query - description: Determines the sort order. - schema: - type: string - enum: - - asc - - desc - default: desc - example: asc - - name: status - in: query - description: Filters the returned cases by state. - schema: - type: string - enum: - - closed - - in-progress - - open - example: open - - name: tags - in: query - description: Filters the returned cases by tags. - schema: - oneOf: - - type: string - - type: array - items: + requestBody: + content: + application/json: + schema: + type: object + properties: + closure_type: + $ref: '#/components/schemas/closure_types' + connector: + description: An object that contains the connector configuration. + type: object + properties: + fields: + description: >- + An object containing the connector fields. To create a + case without a connector, specify null. If you want to + omit any individual field, specify null as its value. + nullable: true + type: object + properties: + caseId: + description: The case identifier for Swimlane connectors. + type: string + category: + description: >- + The category of the incident for ServiceNow ITSM and + ServiceNow SecOps connectors. + type: string + destIp: + description: >- + A comma-separated list of destination IPs for + ServiceNow SecOps connectors. + type: string + impact: + description: >- + The effect an incident had on business for + ServiceNow ITSM connectors. + type: string + issueType: + description: The type of issue for Jira connectors. + type: string + issueTypes: + description: The type of incident for IBM Resilient connectors. + type: array + items: + type: number + malwareHash: + description: >- + A comma-separated list of malware hashes for + ServiceNow SecOps connectors. + type: string + malwareUrl: + description: >- + A comma-separated list of malware URLs for + ServiceNow SecOps connectors. + type: string + parent: + description: >- + The key of the parent issue, when the issue type is + sub-task for Jira connectors. + type: string + priority: + description: >- + The priority of the issue for Jira and ServiceNow + SecOps connectors. + type: string + severity: + description: >- + The severity of the incident for ServiceNow ITSM + connectors. + type: string + severityCode: + description: >- + The severity code of the incident for IBM Resilient + connectors. + type: number + sourceIp: + description: >- + A comma-separated list of source IPs for ServiceNow + SecOps connectors. + type: string + subcategory: + description: >- + The subcategory of the incident for ServiceNow ITSM + connectors. + type: string + urgency: + description: >- + The extent to which the incident resolution can be + delayed for ServiceNow ITSM connectors. + type: string + example: null + id: + description: >- + The identifier for the connector. To create a case + without a connector, use `none`. + type: string + example: none + name: + description: >- + The name of the connector. To create a case without a + connector, use `none`. + type: string + example: none + type: + $ref: '#/components/schemas/connector_types' + required: + - fields + - id + - name + - type + version: + description: > + The version of the connector. To retrieve the version value, + use the get configuration API. type: string - example: phishing - - name: to - in: query - description: > - [preview] Returns only cases that were created before a specific - date. The date must be specified as a KQL data range or date match - expression. This functionality is in technical preview and may be - changed or removed in a future release. Elastic will apply best - effort to fix any issues, but features in technical preview are not - subject to the support SLA of official GA features. - schema: - type: string - example: now+1d + example: WzIwMiwxXQ== + required: + - version responses: '200': description: Indicates a successful call. content: application/json; charset=utf-8: schema: - type: object - properties: - cases: - type: array - items: + type: array + items: + type: object + properties: + closure_type: + $ref: '#/components/schemas/closure_types' + connector: type: object properties: - closed_at: - type: string - format: date-time - nullable: true - example: null - closed_by: - type: object - properties: - email: - type: string - full_name: - type: string - username: - type: string + fields: + description: >- + An object containing the connector fields. To create + a case without a connector, specify null. If you + want to omit any individual field, specify null as + its value. nullable: true - example: null - comments: - type: array - items: - type: string - example: [] - connector: type: object properties: - fields: + caseId: + description: The case identifier for Swimlane connectors. + type: string + category: description: >- - An object containing the connector fields. To - create a case without a connector, specify null. - If you want to omit any individual field, - specify null as its value. - nullable: true - type: object - properties: - caseId: - description: The case identifier for Swimlane connectors. - type: string - category: - description: >- - The category of the incident for ServiceNow - ITSM and ServiceNow SecOps connectors. - type: string - destIp: - description: >- - A comma-separated list of destination IPs - for ServiceNow SecOps connectors. - type: string - impact: - description: >- - The effect an incident had on business for - ServiceNow ITSM connectors. - type: string - issueType: - description: The type of issue for Jira connectors. - type: string - issueTypes: - description: >- - The type of incident for IBM Resilient - connectors. - type: array - items: - type: number - malwareHash: - description: >- - A comma-separated list of malware hashes for - ServiceNow SecOps connectors. - type: string - malwareUrl: - description: >- - A comma-separated list of malware URLs for - ServiceNow SecOps connectors. - type: string - parent: - description: >- - The key of the parent issue, when the issue - type is sub-task for Jira connectors. - type: string - priority: - description: >- - The priority of the issue for Jira and - ServiceNow SecOps connectors. - type: string - severity: - description: >- - The severity of the incident for ServiceNow - ITSM connectors. - type: string - severityCode: - description: >- - The severity code of the incident for IBM - Resilient connectors. - type: number - sourceIp: - description: >- - A comma-separated list of source IPs for - ServiceNow SecOps connectors. - type: string - subcategory: - description: >- - The subcategory of the incident for - ServiceNow ITSM connectors. - type: string - urgency: - description: >- - The extent to which the incident resolution - can be delayed for ServiceNow ITSM - connectors. - type: string - required: - - fields - - id - - name - - type - id: + The category of the incident for ServiceNow ITSM + and ServiceNow SecOps connectors. + type: string + destIp: description: >- - The identifier for the connector. To create a - case without a connector, use `none`. + A comma-separated list of destination IPs for + ServiceNow SecOps connectors. type: string - name: + impact: description: >- - The name of the connector. To create a case - without a connector, use `none`. + The effect an incident had on business for + ServiceNow ITSM connectors. type: string - type: - $ref: '#/components/schemas/connector_types' - created_at: - type: string - format: date-time - example: '2022-05-13T09:16:17.416Z' - created_by: - type: object - properties: - email: + issueType: + description: The type of issue for Jira connectors. type: string - example: ahunley@imf.usa.gov - full_name: + issueTypes: + description: >- + The type of incident for IBM Resilient + connectors. + type: array + items: + type: number + malwareHash: + description: >- + A comma-separated list of malware hashes for + ServiceNow SecOps connectors. type: string - example: Alan Hunley - username: + malwareUrl: + description: >- + A comma-separated list of malware URLs for + ServiceNow SecOps connectors. type: string - example: ahunley - description: - type: string - example: >- - James Bond clicked on a highly suspicious email - banner advertising cheap holidays for underpaid - civil servants. Operation bubblegum is active. - Repeat - operation bubblegum is now active - duration: - type: integer - description: >- - The elapsed time from the creation of the case to - its closure (in seconds). If the case has not been - closed, the duration is set to null. - example: 120 - external_service: - type: object - properties: - connector_id: + parent: + description: >- + The key of the parent issue, when the issue type + is sub-task for Jira connectors. type: string - connector_name: + priority: + description: >- + The priority of the issue for Jira and + ServiceNow SecOps connectors. type: string - external_id: + severity: + description: >- + The severity of the incident for ServiceNow ITSM + connectors. type: string - external_title: + severityCode: + description: >- + The severity code of the incident for IBM + Resilient connectors. + type: number + sourceIp: + description: >- + A comma-separated list of source IPs for + ServiceNow SecOps connectors. type: string - external_url: + subcategory: + description: >- + The subcategory of the incident for ServiceNow + ITSM connectors. type: string - pushed_at: + urgency: + description: >- + The extent to which the incident resolution can + be delayed for ServiceNow ITSM connectors. type: string - format: date-time - pushed_by: - type: object - properties: - email: - type: string - full_name: - type: string - username: - type: string - nullable: true - example: null + example: null id: + description: >- + The identifier for the connector. To create a case + without a connector, use `none`. type: string - example: 66b9aa00-94fa-11ea-9f74-e7e108796192 - owner: - $ref: '#/components/schemas/owners' - settings: - type: object - properties: - syncAlerts: - type: boolean - example: true - severity: - $ref: '#/components/schemas/severity_property' - status: - $ref: '#/components/schemas/status' - tags: - type: array - items: - type: string - example: - - phishing - - social engineering - - bubblegum - title: + example: none + name: + description: >- + The name of the connector. To create a case without + a connector, use `none`. type: string - example: This case will self-destruct in 5 seconds - totalAlerts: - type: integer - example: 0 - totalComment: - type: integer - example: 0 - updated_at: + example: none + type: + $ref: '#/components/schemas/connector_types' + created_at: + type: string + format: date-time + example: '2022-06-01T17:07:17.767Z' + created_by: + type: object + properties: + email: type: string - format: date-time - nullable: true example: null - updated_by: - type: object - properties: - email: - type: string - full_name: - type: string - username: - type: string - nullable: true + full_name: + type: string example: null - version: + username: type: string - example: WzUzMiwxXQ== - count_closed_cases: - type: integer - count_in_progress_cases: - type: integer - count_open_cases: - type: integer - page: - type: integer - per_page: - type: integer - total: - type: integer - examples: - findCaseResponse: - $ref: '#/components/examples/find_case_response' + example: elastic + error: + type: string + example: null + id: + type: string + example: 4a97a440-e1cd-11ec-be9b-9b1838238ee6 + mappings: + type: array + items: + type: object + properties: + action_type: + type: string + example: overwrite + source: + type: string + example: title + target: + type: string + example: summary + owner: + $ref: '#/components/schemas/owners' + updated_at: + type: string + format: date-time + nullable: true + example: '2022-06-01T19:58:48.169Z' + updated_by: + type: object + properties: + email: + type: string + example: null + full_name: + type: string + example: null + username: + type: string + example: elastic + nullable: true + version: + type: string + example: WzIwNzMsMV0= servers: - url: https://localhost:5601 servers: @@ -2468,7 +4176,7 @@ components: owner: in: query name: owner - description: >- + description: > A filter to limit the response to a specific set of applications. If this parameter is omitted, the response contains information about all the cases that the user has access to read. @@ -2490,6 +4198,22 @@ components: - high - low - medium + alert_id: + in: path + name: alertId + description: An identifier for the alert. + required: true + schema: + type: string + example: 09f0c261e39e36351d75995b78bb83673774d1bc2cca9df2d15f0e5c0a99a540 + configuration_id: + in: path + name: configurationId + description: An identifier for the configuration. + required: true + schema: + type: string + example: 3297a0f0-b5ec-11ec-b141-0fdb20a7f9a9 space_id: in: path name: spaceId @@ -2509,13 +4233,17 @@ components: - .servicenow - .servicenow-sir - .swimlane + example: .none owners: type: string - description: Owner apps + description: > + The application that owns the cases: Stack Management, Observability, or + Elastic Security. enum: - cases - observability - securitySolution + example: cases severity_property: type: string description: The severity of the case. @@ -2532,6 +4260,16 @@ components: - closed - in-progress - open + closure_types: + type: string + description: >- + Indicates whether a case is automatically closed when it is pushed to + external systems (`close-by-pushing`) or not automatically closed + (`close-by-user`). + enum: + - close-by-pushing + - close-by-user + example: close-by-user examples: create_case_request: summary: Create a security case that uses a Jira connector. diff --git a/x-pack/plugins/cases/docs/openapi/components/parameters/alert_id.yaml b/x-pack/plugins/cases/docs/openapi/components/parameters/alert_id.yaml new file mode 100644 index 00000000000000..8677b327b91be8 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/components/parameters/alert_id.yaml @@ -0,0 +1,7 @@ +in: path +name: alertId +description: An identifier for the alert. +required: true +schema: + type: string + example: 09f0c261e39e36351d75995b78bb83673774d1bc2cca9df2d15f0e5c0a99a540 \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/components/parameters/configuration_id.yaml b/x-pack/plugins/cases/docs/openapi/components/parameters/configuration_id.yaml new file mode 100644 index 00000000000000..65cce12afaa929 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/components/parameters/configuration_id.yaml @@ -0,0 +1,7 @@ +in: path +name: configurationId +description: An identifier for the configuration. +required: true +schema: + type: string + example: 3297a0f0-b5ec-11ec-b141-0fdb20a7f9a9 \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/components/parameters/owner.yaml b/x-pack/plugins/cases/docs/openapi/components/parameters/owner.yaml index 3f5e5ae73ad194..3c5e511742bf20 100644 --- a/x-pack/plugins/cases/docs/openapi/components/parameters/owner.yaml +++ b/x-pack/plugins/cases/docs/openapi/components/parameters/owner.yaml @@ -1,6 +1,9 @@ in: query name: owner -description: A filter to limit the response to a specific set of applications. If this parameter is omitted, the response contains information about all the cases that the user has access to read. +description: > + A filter to limit the response to a specific set of applications. If this + parameter is omitted, the response contains information about all the cases + that the user has access to read. schema: oneOf: - $ref: '../schemas/owners.yaml' diff --git a/x-pack/plugins/cases/docs/openapi/components/schemas/case_configure_response_properties.yaml b/x-pack/plugins/cases/docs/openapi/components/schemas/case_configure_response_properties.yaml new file mode 100644 index 00000000000000..8041c4e340125b --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/components/schemas/case_configure_response_properties.yaml @@ -0,0 +1,65 @@ +closure_type: + $ref: 'closure_types.yaml' +connector: + type: object + properties: + $ref: 'connector_properties.yaml' +created_at: + type: string + format: date-time + example: 2022-06-01T17:07:17.767Z +created_by: + type: object + properties: + email: + type: string + example: null + full_name: + type: string + example: null + username: + type: string + example: elastic +error: + type: string + example: null +id: + type: string + example: 4a97a440-e1cd-11ec-be9b-9b1838238ee6 +mappings: + type: array + items: + type: object + properties: + action_type: + type: string + example: overwrite + source: + type: string + example: title + target: + type: string + example: summary +owner: + $ref: 'owners.yaml' +updated_at: + type: string + format: date-time + nullable: true + example: 2022-06-01T19:58:48.169Z +updated_by: + type: object + properties: + email: + type: string + example: null + full_name: + type: string + example: null + username: + type: string + example: elastic + nullable: true +version: + type: string + example: WzIwNzMsMV0= \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/components/schemas/case_response_properties.yaml b/x-pack/plugins/cases/docs/openapi/components/schemas/case_response_properties.yaml index cb1df95b13d98b..f3d46c08a517ed 100644 --- a/x-pack/plugins/cases/docs/openapi/components/schemas/case_response_properties.yaml +++ b/x-pack/plugins/cases/docs/openapi/components/schemas/case_response_properties.yaml @@ -26,19 +26,19 @@ connector: created_at: type: string format: date-time - example: "2022-05-13T09:16:17.416Z" + example: 2022-05-13T09:16:17.416Z created_by: type: object properties: email: type: string - example: "ahunley@imf.usa.gov" + example: null full_name: type: string - example: "Alan Hunley" + example: null username: type: string - example: "ahunley" + example: elastic description: type: string example: "James Bond clicked on a highly suspicious email banner advertising cheap holidays for underpaid civil servants. Operation bubblegum is active. Repeat - operation bubblegum is now active" @@ -75,7 +75,7 @@ external_service: example: null id: type: string - example: "66b9aa00-94fa-11ea-9f74-e7e108796192" + example: 66b9aa00-94fa-11ea-9f74-e7e108796192 owner: $ref: 'owners.yaml' settings: @@ -95,7 +95,7 @@ tags: example: ["phishing","social engineering","bubblegum"] title: type: string - example: "This case will self-destruct in 5 seconds" + example: This case will self-destruct in 5 seconds totalAlerts: type: integer example: 0 @@ -120,4 +120,4 @@ updated_by: example: null version: type: string - example: "WzUzMiwxXQ==" + example: WzUzMiwxXQ== diff --git a/x-pack/plugins/cases/docs/openapi/components/schemas/closure_types.yaml b/x-pack/plugins/cases/docs/openapi/components/schemas/closure_types.yaml index f09063d0db18f7..6879f820d6f5cc 100644 --- a/x-pack/plugins/cases/docs/openapi/components/schemas/closure_types.yaml +++ b/x-pack/plugins/cases/docs/openapi/components/schemas/closure_types.yaml @@ -2,4 +2,5 @@ type: string description: Indicates whether a case is automatically closed when it is pushed to external systems (`close-by-pushing`) or not automatically closed (`close-by-user`). enum: - close-by-pushing - - close-by-user \ No newline at end of file + - close-by-user +example: close-by-user \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/components/schemas/connector_properties.yaml b/x-pack/plugins/cases/docs/openapi/components/schemas/connector_properties.yaml index c2bc2ab7c887ab..fbaa7ee66b568a 100644 --- a/x-pack/plugins/cases/docs/openapi/components/schemas/connector_properties.yaml +++ b/x-pack/plugins/cases/docs/openapi/components/schemas/connector_properties.yaml @@ -50,16 +50,14 @@ fields: urgency: description: The extent to which the incident resolution can be delayed for ServiceNow ITSM connectors. type: string - required: - - fields - - id - - name - - type + example: null id: description: The identifier for the connector. To create a case without a connector, use `none`. type: string + example: none name: description: The name of the connector. To create a case without a connector, use `none`. type: string + example: none type: $ref: 'connector_types.yaml' \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/components/schemas/connector_types.yaml b/x-pack/plugins/cases/docs/openapi/components/schemas/connector_types.yaml index 24c1ec58808289..2c31b93e2c2dbc 100644 --- a/x-pack/plugins/cases/docs/openapi/components/schemas/connector_types.yaml +++ b/x-pack/plugins/cases/docs/openapi/components/schemas/connector_types.yaml @@ -6,4 +6,5 @@ enum: - .resilient - .servicenow - .servicenow-sir - - .swimlane \ No newline at end of file + - .swimlane +example: .none \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/components/schemas/owners.yaml b/x-pack/plugins/cases/docs/openapi/components/schemas/owners.yaml index f39324a36e7028..9036fd5a3833ab 100644 --- a/x-pack/plugins/cases/docs/openapi/components/schemas/owners.yaml +++ b/x-pack/plugins/cases/docs/openapi/components/schemas/owners.yaml @@ -1,6 +1,9 @@ type: string -description: Owner apps +description: > + The application that owns the cases: Stack Management, Observability, or + Elastic Security. enum: - cases - observability - - securitySolution \ No newline at end of file + - securitySolution +example: cases \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/entrypoint.yaml b/x-pack/plugins/cases/docs/openapi/entrypoint.yaml index 6e1ef2bd1aa1ab..c43e207641d96e 100644 --- a/x-pack/plugins/cases/docs/openapi/entrypoint.yaml +++ b/x-pack/plugins/cases/docs/openapi/entrypoint.yaml @@ -21,12 +21,12 @@ paths: $ref: paths/api@cases.yaml /api/cases/_find: $ref: paths/api@cases@_find.yaml -# '/api/cases/alerts/{alertId}': -# $ref: 'paths/api@cases@alerts@{alertid}.yaml' -# '/api/cases/configure': -# $ref: paths/api@cases@configure.yaml -# '/api/cases/configure/{configurationId}': -# $ref: paths/api@cases@configure@{configurationid}.yaml + '/api/cases/alerts/{alertId}': + $ref: 'paths/api@cases@alerts@{alertid}.yaml' + '/api/cases/configure': + $ref: paths/api@cases@configure.yaml + '/api/cases/configure/{configurationId}': + $ref: paths/api@cases@configure@{configurationid}.yaml # '/api/cases/configure/connectors/_find': # $ref: paths/api@cases@configure@connectors@_find.yaml # '/api/cases/reporters': @@ -52,12 +52,12 @@ paths: $ref: 'paths/s@{spaceid}@api@cases.yaml' '/s/{spaceId}/api/cases/_find': $ref: 'paths/s@{spaceid}@api@cases@_find.yaml' - # '/s/{spaceId}/api/cases/alerts/{alertId}': - # $ref: 'paths/s@{spaceid}@api@cases@alerts@{alertid}.yaml' - # '/s/{spaceId}/api/cases/configure': - # $ref: paths/s@{spaceid}@api@cases@configure.yaml - # '/s/{spaceId}/api/cases/configure/{configurationId}': - # $ref: paths/s@{spaceid}@api@cases@configure@{configurationid}.yaml + '/s/{spaceId}/api/cases/alerts/{alertId}': + $ref: 'paths/s@{spaceid}@api@cases@alerts@{alertid}.yaml' + '/s/{spaceId}/api/cases/configure': + $ref: paths/s@{spaceid}@api@cases@configure.yaml + '/s/{spaceId}/api/cases/configure/{configurationId}': + $ref: paths/s@{spaceid}@api@cases@configure@{configurationid}.yaml # '/s/{spaceId}/api/cases/configure/connectors/_find': # $ref: paths/s@{spaceid}@api@cases@configure@connectors@_find.yaml # '/s/{spaceId}/api/cases/reporters': diff --git a/x-pack/plugins/cases/docs/openapi/paths/api@cases.yaml b/x-pack/plugins/cases/docs/openapi/paths/api@cases.yaml index 4956056e56b54d..6b8910c215ce2b 100644 --- a/x-pack/plugins/cases/docs/openapi/paths/api@cases.yaml +++ b/x-pack/plugins/cases/docs/openapi/paths/api@cases.yaml @@ -18,6 +18,11 @@ post: type: object properties: $ref: '../components/schemas/connector_properties.yaml' + required: + - fields + - id + - name + - type description: description: The description for the case. type: string @@ -112,6 +117,11 @@ patch: type: object properties: $ref: '../components/schemas/connector_properties.yaml' + required: + - fields + - id + - name + - type description: description: The description for the case. type: string diff --git a/x-pack/plugins/cases/docs/openapi/paths/api@cases@_find.yaml b/x-pack/plugins/cases/docs/openapi/paths/api@cases@_find.yaml index cc6363d7839002..3a20d0dcbdc49c 100644 --- a/x-pack/plugins/cases/docs/openapi/paths/api@cases@_find.yaml +++ b/x-pack/plugins/cases/docs/openapi/paths/api@cases@_find.yaml @@ -32,7 +32,7 @@ get: schema: type: string example: now-1d - x-preview: true + x-technical-preview: true - $ref: '../components/parameters/owner.yaml' - name: page in: query @@ -119,7 +119,7 @@ get: schema: type: string example: now%2B1d - x-preview: true + x-technical-preview: true responses: '200': description: Indicates a successful call. diff --git a/x-pack/plugins/cases/docs/openapi/paths/api@cases@alerts@{alertid}.yaml b/x-pack/plugins/cases/docs/openapi/paths/api@cases@alerts@{alertid}.yaml new file mode 100644 index 00000000000000..d79a3c7264b0ec --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/paths/api@cases@alerts@{alertid}.yaml @@ -0,0 +1,36 @@ +get: + description: > + Returns the cases associated with a specific alert. + You must have read privileges for the **Cases** feature in the **Management**, + **Observability**, or **Security** section of the Kibana feature privileges, + depending on the owner of the cases you're seeking. + x-technical-preview: true + tags: + - cases + - kibana + parameters: + - $ref: ../components/parameters/alert_id.yaml + - $ref: '../components/parameters/owner.yaml' + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: array + items: + type: object + properties: + id: + type: string + description: The case identifier. + title: + type: string + description: The case title. + example: + - id: 06116b80-e1c3-11ec-be9b-9b1838238ee6 + title: security_case + servers: + - url: https://localhost:5601 +servers: + - url: https://localhost:5601 \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/paths/api@cases@configure.yaml b/x-pack/plugins/cases/docs/openapi/paths/api@cases@configure.yaml new file mode 100644 index 00000000000000..6a685e903c89d9 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/paths/api@cases@configure.yaml @@ -0,0 +1,91 @@ +get: + description: > + Retrieves external connection details, such as the closure type and default + connector for cases. You must have read privileges for the **Cases** feature + in the **Management**, **Observability**, or **Security** section of the + Kibana feature privileges, depending on the owner of the case configuration. + tags: + - cases + - kibana + parameters: + - $ref: '../components/parameters/owner.yaml' + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: array + items: + type: object + properties: + $ref: '../components/schemas/case_configure_response_properties.yaml' + servers: + - url: https://localhost:5601 + +post: + description: > + Sets external connection details, such as the closure type and default + connector for cases. You must have all privileges for the **Cases** feature + in the **Management**, **Observability**, or **Security** section of the + Kibana feature privileges, depending on the owner of the case configuration. + Connectors are used to interface with external systems. You must create a + connector before you can use it in your cases. Refer to the add connectors + API. If you set a default connector, it is automatically selected when you + create cases in Kibana. If you use the create case API, however, you must + still specify all of the connector details. + tags: + - cases + - kibana + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + requestBody: + content: + application/json: + schema: + type: object + properties: + closure_type: + $ref: '../components/schemas/closure_types.yaml' + connector: + description: An object that contains the connector configuration. + type: object + properties: + $ref: '../components/schemas/connector_properties.yaml' + required: + - fields + - id + - name + - type + owner: + $ref: '../components/schemas/owners.yaml' + settings: + description: An object that contains the case settings. + type: object + properties: + syncAlerts: + description: Turns alert syncing on or off. + type: boolean + example: true + required: + - syncAlerts + required: + - closure_type + - connector + - owner + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: array + items: + type: object + properties: + $ref: '../components/schemas/case_configure_response_properties.yaml' + servers: + - url: https://localhost:5601 + +servers: + - url: https://localhost:5601 \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/paths/api@cases@configure@{configurationid}.yaml b/x-pack/plugins/cases/docs/openapi/paths/api@cases@configure@{configurationid}.yaml new file mode 100644 index 00000000000000..21d6d7f095d458 --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/paths/api@cases@configure@{configurationid}.yaml @@ -0,0 +1,55 @@ +patch: + description: > + Updates external connection details, such as the closure type and default + connector for cases. You must have all privileges for the **Cases** feature + in the **Management**, **Observability**, or **Security** section of the + Kibana feature privileges, depending on the owner of the case configuration. + Connectors are used to interface with external systems. You must create a + connector before you can it in your cases. Refer to the add connectors API. + tags: + - cases + - kibana + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + - $ref: ../components/parameters/configuration_id.yaml + requestBody: + content: + application/json: + schema: + type: object + properties: + closure_type: + $ref: '../components/schemas/closure_types.yaml' + connector: + description: An object that contains the connector configuration. + type: object + properties: + $ref: '../components/schemas/connector_properties.yaml' + required: + - fields + - id + - name + - type + version: + description: > + The version of the connector. To retrieve the version value, use + the get configuration API. + type: string + example: "WzIwMiwxXQ==" + required: + - version + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: array + items: + type: object + properties: + $ref: '../components/schemas/case_configure_response_properties.yaml' + servers: + - url: https://localhost:5601 +servers: + - url: https://localhost:5601 \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases.yaml b/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases.yaml index 368598ec5fbbfd..6b1ad03484ebf2 100644 --- a/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases.yaml +++ b/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases.yaml @@ -19,6 +19,11 @@ post: type: object properties: $ref: '../components/schemas/connector_properties.yaml' + required: + - fields + - id + - name + - type description: description: The description for the case. type: string @@ -115,6 +120,11 @@ patch: type: object properties: $ref: '../components/schemas/connector_properties.yaml' + required: + - fields + - id + - name + - type description: description: The description for the case. type: string diff --git a/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases@alerts@{alertid}.yaml b/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases@alerts@{alertid}.yaml new file mode 100644 index 00000000000000..e0d1bd3201ff9c --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases@alerts@{alertid}.yaml @@ -0,0 +1,37 @@ +get: + description: > + Returns the cases associated with a specific alert. You must have read + privileges for the **Cases** feature in the **Management**, + **Observability**, or **Security** section of the Kibana feature privileges, + depending on the owner of the cases you're seeking. + x-technical-preview: true + tags: + - cases + - kibana + parameters: + - $ref: ../components/parameters/alert_id.yaml + - $ref: '../components/parameters/space_id.yaml' + - $ref: '../components/parameters/owner.yaml' + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: array + items: + type: object + properties: + id: + type: string + description: The case identifier. + title: + type: string + description: The case title. + example: + - id: 06116b80-e1c3-11ec-be9b-9b1838238ee6 + title: security_case + servers: + - url: https://localhost:5601 +servers: + - url: https://localhost:5601 \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases@configure.yaml b/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases@configure.yaml new file mode 100644 index 00000000000000..886ed02d84b9cc --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases@configure.yaml @@ -0,0 +1,93 @@ +get: + description: > + Retrieves external connection details, such as the closure type and default + connector for cases. You must have read privileges for the **Cases** feature + in the **Management**, **Observability**, or **Security** section of the + Kibana feature privileges, depending on the owner of the case configuration. + tags: + - cases + - kibana + parameters: + - $ref: '../components/parameters/space_id.yaml' + - $ref: '../components/parameters/owner.yaml' + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: array + items: + type: object + properties: + $ref: '../components/schemas/case_configure_response_properties.yaml' + servers: + - url: https://localhost:5601 + +post: + description: > + Sets external connection details, such as the closure type and default + connector for cases. You must have all privileges for the **Cases** feature + in the **Management**, **Observability**, or **Security** section of the + Kibana feature privileges, depending on the owner of the case configuration. + Connectors are used to interface with external systems. You must create a + connector before you can use it in your cases. Refer to the add connectors + API. If you set a default connector, it is automatically selected when you + create cases in Kibana. If you use the create case API, however, you must + still specify all of the connector details. + tags: + - cases + - kibana + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + - $ref: '../components/parameters/space_id.yaml' + requestBody: + content: + application/json: + schema: + type: object + properties: + closure_type: + $ref: '../components/schemas/closure_types.yaml' + connector: + description: An object that contains the connector configuration. + type: object + properties: + $ref: '../components/schemas/connector_properties.yaml' + required: + - fields + - id + - name + - type + owner: + $ref: '../components/schemas/owners.yaml' + settings: + description: An object that contains the case settings. + type: object + properties: + syncAlerts: + description: Turns alert syncing on or off. + type: boolean + example: true + required: + - syncAlerts + required: + - closure_type + - connector + - owner + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: array + items: + type: object + properties: + $ref: '../components/schemas/case_configure_response_properties.yaml' + servers: + - url: https://localhost:5601 + +servers: + - url: https://localhost:5601 \ No newline at end of file diff --git a/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases@configure@{configurationid}.yaml b/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases@configure@{configurationid}.yaml new file mode 100644 index 00000000000000..2df211af8ea91e --- /dev/null +++ b/x-pack/plugins/cases/docs/openapi/paths/s@{spaceid}@api@cases@configure@{configurationid}.yaml @@ -0,0 +1,56 @@ +patch: + description: > + Updates external connection details, such as the closure type and default + connector for cases. You must have all privileges for the **Cases** feature + in the **Management**, **Observability**, or **Security** section of the + Kibana feature privileges, depending on the owner of the case configuration. + Connectors are used to interface with external systems. You must create a + connector before you can it in your cases. Refer to the add connectors API. + tags: + - cases + - kibana + parameters: + - $ref: ../components/headers/kbn_xsrf.yaml + - $ref: ../components/parameters/configuration_id.yaml + - $ref: '../components/parameters/space_id.yaml' + requestBody: + content: + application/json: + schema: + type: object + properties: + closure_type: + $ref: '../components/schemas/closure_types.yaml' + connector: + description: An object that contains the connector configuration. + type: object + properties: + $ref: '../components/schemas/connector_properties.yaml' + required: + - fields + - id + - name + - type + version: + description: > + The version of the connector. To retrieve the version value, use + the get configuration API. + type: string + example: "WzIwMiwxXQ==" + required: + - version + responses: + '200': + description: Indicates a successful call. + content: + application/json; charset=utf-8: + schema: + type: array + items: + type: object + properties: + $ref: '../components/schemas/case_configure_response_properties.yaml' + servers: + - url: https://localhost:5601 +servers: + - url: https://localhost:5601 \ No newline at end of file diff --git a/x-pack/plugins/cases/public/components/tag_list/index.tsx b/x-pack/plugins/cases/public/components/tag_list/index.tsx index 615005e5090bab..9c14c2b1aee530 100644 --- a/x-pack/plugins/cases/public/components/tag_list/index.tsx +++ b/x-pack/plugins/cases/public/components/tag_list/index.tsx @@ -70,10 +70,11 @@ export const TagList = React.memo( const { isValid, data: newData } = await submit(); if (isValid && newData.tags) { onSubmit(newData.tags); - setIsEditTags(false); form.reset({ defaultValue: newData }); + setIsEditTags(false); } - }, [form, onSubmit, submit]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [onSubmit, submit]); const { data: tagOptions = [] } = useGetTags(); const [options, setOptions] = useState( diff --git a/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/benchmarks.tsx b/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/benchmarks.tsx index d13caa5e915437..4177f6d15c50c9 100644 --- a/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/benchmarks.tsx +++ b/x-pack/plugins/cloud_security_posture/public/pages/benchmarks/benchmarks.tsx @@ -135,7 +135,7 @@ export const Benchmarks = () => { const [query, setQuery] = useState({ name: '', page: 1, - perPage: 5, + perPage: 10, sortField: 'package_policy.name', sortOrder: 'asc', }); diff --git a/x-pack/plugins/fleet/README.md b/x-pack/plugins/fleet/README.md index 0ec54b322baae1..20433cf72b5c19 100644 --- a/x-pack/plugins/fleet/README.md +++ b/x-pack/plugins/fleet/README.md @@ -109,9 +109,9 @@ Once the Fleet Server container is running, you should be able to treat it as if 2. Click "Add Agent" 3. Scroll down to the bottom of the flyout that opens to view the enrollment command, copy the contents of the `--enrollment-token` option 4. Run this docker command: - ``` - docker run -e FLEET_ENROLL=true -e FLEET_INSECURE=true -e FLEET_URL=https://192.168.65.2:8220 -e FLEET_ENROLLMENT_TOKEN= --rm docker.elastic.co/beats/elastic-agent:{VERSION} - ``` + ``` + docker run -e FLEET_ENROLL=true -e FLEET_INSECURE=true -e FLEET_URL=https://192.168.65.2:8220 -e FLEET_ENROLLMENT_TOKEN= --rm docker.elastic.co/beats/elastic-agent:{VERSION} + ``` ### Tests @@ -175,3 +175,14 @@ The set of bundled packages included with Kibana is dictated by a top-level `fle Until further automation is added, this `fleet_packages.json` file should be updated as part of the release process to ensure the latest compatible version of each bundled package is included with that Kibana version. **This must be done before the final BC for a release is built.** Tracking issues should be opened and tracked by the Fleet UI team. See https://github.com/elastic/kibana/issues/129309 as an example. + +As part of the bundled package update process, we'll likely also need to update the pinned Docker image that runs in Kibana's test environment. We configure this pinned registry image in + +- `x-pack/test/fleet_api_integration/config.ts` +- `x-pack/plugins/fleet/server/integration_tests/helpers/docker_registry_helper.ts` +- `x-pack/test/functional/config.base.js` +- `x-pack/test/functional_synthetics/config.js` + +To update this registry image, pull the digest SHA from the package storage Jenkins pipeline at https://beats-ci.elastic.co/blue/organizations/jenkins/Ingest-manager%2Fpackage-storage/activity and update the files above. The digest value should appear in the "publish Docker image" step as part of the `docker push` command in the logs. + +![image](https://user-images.githubusercontent.com/6766512/171409455-64f9ab1d-08fe-4872-9b74-58359ed938dd.png) diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index cb5d8f3bb009b6..9da1075e52dffe 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -397,6 +397,7 @@ export interface IntegrationCardItem { integration: string; id: string; categories: string[]; + fromIntegrations?: string; } export type PackagesGroupedByStatus = Record, PackageList>; diff --git a/x-pack/plugins/fleet/cypress/screens/integrations.ts b/x-pack/plugins/fleet/cypress/screens/integrations.ts index dddede9e77f8da..ed645d08d9b5ff 100644 --- a/x-pack/plugins/fleet/cypress/screens/integrations.ts +++ b/x-pack/plugins/fleet/cypress/screens/integrations.ts @@ -7,7 +7,7 @@ export const ADD_POLICY_BTN = 'addIntegrationPolicyButton'; export const CREATE_PACKAGE_POLICY_SAVE_BTN = 'createPackagePolicySaveButton'; -export const INTEGRATIONS_CARD = '.euiCard__titleAnchor'; +export const INTEGRATIONS_CARD = '.euiCard__titleButton'; export const INTEGRATION_NAME_LINK = 'integrationNameLink'; export const AGENT_POLICY_NAME_LINK = 'agentPolicyNameLink'; diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx index 4e1bad011c3dc1..686b0e8b1003ef 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/components/agent_upgrade_modal/index.tsx @@ -26,6 +26,7 @@ import type { EuiComboBoxOptionOption } from '@elastic/eui'; import semverCoerce from 'semver/functions/coerce'; import semverGt from 'semver/functions/gt'; +import semverValid from 'semver/functions/valid'; import { getMinVersion } from '../../../../../../../common/services/get_min_max_version'; import type { Agent } from '../../../../types'; @@ -199,6 +200,10 @@ export const AgentUpgradeAgentModal: React.FunctionComponent { + if (!semverValid(searchValue)) { + return; + } + const agentVersionNumber = semverCoerce(searchValue); if ( agentVersionNumber?.version && @@ -297,8 +302,12 @@ export const AgentUpgradeAgentModal: React.FunctionComponent>) => { + if (!selected.length) { + return; + } setSelectedVersion(selected); }} onCreateOption={onCreateOption} @@ -369,10 +378,14 @@ export const AgentUpgradeAgentModal: React.FunctionComponent>) => { + if (!selected.length) { + return; + } setSelectedMantainanceWindow(selected); }} /> diff --git a/x-pack/plugins/fleet/public/applications/integrations/hooks/index.ts b/x-pack/plugins/fleet/public/applications/integrations/hooks/index.ts index 9c907436af6e39..46ddeb809980f0 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/hooks/index.ts +++ b/x-pack/plugins/fleet/public/applications/integrations/hooks/index.ts @@ -11,3 +11,4 @@ export * from './use_links'; export * from './use_local_search'; export * from './use_package_install'; export * from './use_agent_policy_context'; +export * from './use_integrations_state'; diff --git a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_integrations_state.tsx b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_integrations_state.tsx new file mode 100644 index 00000000000000..e2b4a3ba1fa1b2 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_integrations_state.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { FunctionComponent } from 'react'; +import React, { createContext, useContext, useRef, useCallback } from 'react'; + +import type { IntegrationsAppBrowseRouteState } from '../../../types'; +import { useIntraAppState } from '../../../hooks'; + +interface IntegrationsStateContextValue { + getFromIntegrations(): string | undefined; +} + +const IntegrationsStateContext = createContext({ + getFromIntegrations: () => undefined, +}); + +export const IntegrationsStateContextProvider: FunctionComponent = ({ children }) => { + const maybeState = useIntraAppState(); + const fromIntegrationsRef = useRef(maybeState?.fromIntegrations); + + const getFromIntegrations = useCallback(() => { + return fromIntegrationsRef.current; + }, []); + return ( + + {children} + + ); +}; + +export const useIntegrationsStateContext = () => { + const ctx = useContext(IntegrationsStateContext); + if (!ctx) { + throw new Error( + 'useIntegrationsStateContext can only be used inside of IntegrationsStateContextProvider' + ); + } + return ctx; +}; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.test.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.test.tsx new file mode 100644 index 00000000000000..0b1dbd6daef87a --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { fireEvent, act } from '@testing-library/react'; + +import { createFleetTestRendererMock } from '../../../../../mock'; + +import { useStartServices } from '../../../hooks'; + +import type { PackageCardProps } from './package_card'; +import { PackageCard } from './package_card'; + +jest.mock('../../../hooks', () => { + return { + ...jest.requireActual('../../../hooks'), + useStartServices: jest.fn().mockReturnValue({ + application: { + navigateToApp: jest.fn(), + navigateToUrl: jest.fn(), + }, + }), + }; +}); + +function renderPackageCard(props: PackageCardProps) { + const renderer = createFleetTestRendererMock(); + + const utils = renderer.render(); + + return { utils }; +} + +describe('package card', () => { + let mockNavigateToApp: jest.Mock; + let mockNavigateToUrl: jest.Mock; + + beforeEach(() => { + mockNavigateToApp = useStartServices().application.navigateToApp as jest.Mock; + mockNavigateToUrl = useStartServices().application.navigateToUrl as jest.Mock; + }); + + it('should navigate with state when integrations card', async () => { + const { utils } = renderPackageCard({ + id: 'card-1', + url: '/app/integrations/detail/apache-1.0/overview', + fromIntegrations: 'installed', + title: 'System', + description: 'System', + } as PackageCardProps); + + await act(async () => { + const el = utils.getByRole('button'); + fireEvent.click(el!, {}); + }); + expect(mockNavigateToApp).toHaveBeenCalledWith('integrations', { + path: '/detail/apache-1.0/overview', + state: { fromIntegrations: 'installed' }, + }); + }); + + it('should navigate with url when enterprise search card', async () => { + const { utils } = renderPackageCard({ + id: 'card-1', + url: '/app/enterprise_search/workplace_search/setup_guide', + fromIntegrations: 'installed', + title: 'System', + description: 'System', + } as PackageCardProps); + + await act(async () => { + const el = utils.getByRole('button'); + fireEvent.click(el!, {}); + }); + expect(mockNavigateToUrl).toHaveBeenCalledWith( + '/app/enterprise_search/workplace_search/setup_guide' + ); + }); + + it('should navigate with window open when external url', async () => { + window.open = jest.fn(); + + const { utils } = renderPackageCard({ + id: 'card-1', + url: 'https://google.com', + fromIntegrations: 'installed', + title: 'System', + description: 'System', + } as PackageCardProps); + + await act(async () => { + const el = utils.getByRole('button'); + fireEvent.click(el!, {}); + }); + expect(window.open).toHaveBeenCalledWith('https://google.com', '_blank'); + }); +}); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx index a97a9ec8c1c248..53d6312a207736 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx @@ -14,6 +14,9 @@ import { TrackApplicationView } from '@kbn/usage-collection-plugin/public'; import { CardIcon } from '../../../../../components/package_icon'; import type { IntegrationCardItem } from '../../../../../../common/types/models/epm'; +import { useStartServices } from '../../../hooks'; +import { INTEGRATIONS_BASE_PATH, INTEGRATIONS_PLUGIN_ID } from '../../../constants'; + import { RELEASE_BADGE_DESCRIPTION, RELEASE_BADGE_LABEL } from './release_badge'; export type PackageCardProps = IntegrationCardItem; @@ -34,6 +37,7 @@ export function PackageCard({ url, release, id, + fromIntegrations, }: PackageCardProps) { let releaseBadge: React.ReactNode | null = null; @@ -50,6 +54,21 @@ export function PackageCard({ ); } + const { application } = useStartServices(); + + const onCardClick = () => { + if (url.startsWith(INTEGRATIONS_BASE_PATH)) { + application.navigateToApp(INTEGRATIONS_PLUGIN_ID, { + path: url.slice(INTEGRATIONS_BASE_PATH.length), + state: { fromIntegrations }, + }); + } else if (url.startsWith('http') || url.startsWith('https')) { + window.open(url, '_blank'); + } else { + application.navigateToUrl(url); + } + }; + const testid = `integration-card:${id}`; return ( @@ -69,8 +88,7 @@ export function PackageCard({ size="xl" /> } - href={url} - target={url.startsWith('http') || url.startsWith('https') ? '_blank' : undefined} + onClick={onCardClick} > {releaseBadge} diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/index.tsx index 07fd657a01708f..b21c790edd0f38 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/index.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { Switch, Route } from 'react-router-dom'; import { INTEGRATIONS_ROUTING_PATHS } from '../../constants'; -import { useBreadcrumbs } from '../../hooks'; +import { IntegrationsStateContextProvider, useBreadcrumbs } from '../../hooks'; import { EPMHomePage } from './screens/home'; import { Detail } from './screens/detail'; @@ -24,7 +24,9 @@ export const EPMApp: React.FunctionComponent = () => { - + + + diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx index 9cb1599a3a8c64..45d11730adba9f 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx @@ -34,6 +34,7 @@ import { useStartServices, useAuthz, usePermissionCheck, + useIntegrationsStateContext, } from '../../../../hooks'; import { INTEGRATIONS_ROUTING_PATHS } from '../../../../constants'; import { ExperimentalFeaturesService } from '../../../../services'; @@ -94,6 +95,7 @@ function Breadcrumbs({ packageTitle }: { packageTitle: string }) { export function Detail() { const { getId: getAgentPolicyId } = useAgentPolicyContext(); + const { getFromIntegrations } = useIntegrationsStateContext(); const { pkgkey, panel } = useParams(); const { getHref } = useLink(); const canInstallPackages = useAuthz().integrations.installPackages; @@ -195,21 +197,25 @@ export function Detail() { [integration, packageInfo] ); + const fromIntegrations = getFromIntegrations(); + + const href = + fromIntegrations === 'updates_available' + ? getHref('integrations_installed_updates_available') + : fromIntegrations === 'installed' + ? getHref('integrations_installed') + : getHref('integrations_all'); + const headerLeftContent = useMemo( () => ( {/* Allows button to break out of full width */}
- +
@@ -261,7 +267,7 @@ export function Detail() {
), - [getHref, integrationInfo, isLoading, packageInfo] + [integrationInfo, isLoading, packageInfo, href] ); const handleAddIntegrationPolicyClick = useCallback( diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/update_button.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/update_button.tsx index 798c5ce43e50b1..cc11dd6819695c 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/update_button.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/update_button.tsx @@ -144,6 +144,16 @@ export const UpdateButton: React.FunctionComponent = ({ }, []); const navigateToNewSettingsPage = useCallback(() => { + // only navigate if still on old settings page (user has not navigated away) + if ( + !history.location.pathname.match( + getPath('integration_details_settings', { + pkgkey: `${name}-.*`, + }) + ) + ) { + return; + } const settingsPath = getPath('integration_details_settings', { pkgkey: `${name}-${version}`, }); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx index 0898f099e3e8c8..2ddc78218466ab 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx @@ -46,7 +46,8 @@ export const categoryExists = (category: string, categories: CategoryFacet[]) => export const mapToCard = ( getAbsolutePath: (p: string) => string, getHref: (page: StaticPage | DynamicPage, values?: DynamicPagePathValues) => string, - item: CustomIntegration | PackageListItem + item: CustomIntegration | PackageListItem, + selectedCategory?: string ): IntegrationCardItem => { let uiInternalPathUrl; @@ -80,6 +81,7 @@ export const mapToCard = ( icons: !item.icons || !item.icons.length ? [] : item.icons, title: item.title, url: uiInternalPathUrl, + fromIntegrations: selectedCategory, integration: 'integration' in item ? item.integration || '' : '', name: 'name' in item ? item.name || '' : '', version: 'version' in item ? item.version || '' : '', diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/installed_packages.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/installed_packages.tsx index 964994c250aa0b..19de166cebc169 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/installed_packages.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/installed_packages.tsx @@ -143,7 +143,7 @@ export const InstalledPackages: React.FC = memo(() => { const cards = ( selectedCategory === 'updates_available' ? updatablePackages : allInstalledPackages - ).map((item) => mapToCard(getAbsolutePath, getHref, item)); + ).map((item) => mapToCard(getAbsolutePath, getHref, item, selectedCategory || 'installed')); const callout = selectedCategory === 'updates_available' ? null : ; diff --git a/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx b/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx index ca7293a8c99c96..74385a7eff0c8e 100644 --- a/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx +++ b/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx @@ -27,7 +27,7 @@ cd elastic-agent-${kibanaVersion}-darwin-x86_64 sudo ./elastic-agent install ${enrollArgs}`; const windowsCommand = `$ProgressPreference = 'SilentlyContinue' -wget https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent-${kibanaVersion}-windows-x86_64.zip -OutFile elastic-agent-${kibanaVersion}-windows-x86_64.zip +Invoke-WebRequest -Uri https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent-${kibanaVersion}-windows-x86_64.zip -OutFile elastic-agent-${kibanaVersion}-windows-x86_64.zip Expand-Archive .\\elastic-agent-${kibanaVersion}-windows-x86_64.zip -DestinationPath . cd elastic-agent-${kibanaVersion}-windows-x86_64 .\\elastic-agent.exe install ${enrollArgs}`; diff --git a/x-pack/plugins/fleet/public/components/enrollment_instructions/standalone/index.tsx b/x-pack/plugins/fleet/public/components/enrollment_instructions/standalone/index.tsx index 2d9326cf6cbb1c..e16c0e5dec867a 100644 --- a/x-pack/plugins/fleet/public/components/enrollment_instructions/standalone/index.tsx +++ b/x-pack/plugins/fleet/public/components/enrollment_instructions/standalone/index.tsx @@ -24,7 +24,7 @@ cd elastic-agent-${kibanaVersion}-darwin-x86_64 sudo ./elastic-agent install`; const STANDALONE_RUN_INSTRUCTIONS_WINDOWS = `$ProgressPreference = 'SilentlyContinue' -wget https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent-${kibanaVersion}-windows-x86_64.zip -OutFile elastic-agent-${kibanaVersion}-windows-x86_64.zip +Invoke-WebRequest -Uri https://artifacts.elastic.co/downloads/beats/elastic-agent/elastic-agent-${kibanaVersion}-windows-x86_64.zip -OutFile elastic-agent-${kibanaVersion}-windows-x86_64.zip Expand-Archive .\elastic-agent-${kibanaVersion}-windows-x86_64.zip -DestinationPath . cd elastic-agent-${kibanaVersion}-windows-x86_64 .\\elastic-agent.exe install`; diff --git a/x-pack/plugins/fleet/public/constants/page_paths.ts b/x-pack/plugins/fleet/public/constants/page_paths.ts index a87c9fe9e0869a..26db09d68f41d5 100644 --- a/x-pack/plugins/fleet/public/constants/page_paths.ts +++ b/x-pack/plugins/fleet/public/constants/page_paths.ts @@ -22,6 +22,7 @@ export type StaticPage = export type DynamicPage = | 'integrations_all' | 'integrations_installed' + | 'integrations_installed_updates_available' | 'integration_details_overview' | 'integration_details_policies' | 'integration_details_assets' @@ -76,6 +77,7 @@ export const INTEGRATIONS_ROUTING_PATHS = { integrations: '/:tabId', integrations_all: '/browse/:category?', integrations_installed: '/installed/:category?', + integrations_installed_updates_available: '/installed/updates_available/:category?', integration_details: '/detail/:pkgkey/:panel?', integration_details_overview: '/detail/:pkgkey/overview', integration_details_policies: '/detail/:pkgkey/policies', @@ -104,6 +106,17 @@ export const pagePathGetters: { const queryParams = query ? `?${INTEGRATIONS_SEARCH_QUERYPARAM}=${query}` : ``; return [INTEGRATIONS_BASE_PATH, `/installed${categoryPath}${queryParams}`]; }, + integrations_installed_updates_available: ({ + query, + category, + }: { + query?: string; + category?: string; + }) => { + const categoryPath = category ? `/${category}` : ``; + const queryParams = query ? `?${INTEGRATIONS_SEARCH_QUERYPARAM}=${query}` : ``; + return [INTEGRATIONS_BASE_PATH, `/installed/updates_available${categoryPath}${queryParams}`]; + }, integration_details_overview: ({ pkgkey, integration }) => [ INTEGRATIONS_BASE_PATH, `/detail/${pkgkey}/overview${integration ? `?integration=${integration}` : ''}`, diff --git a/x-pack/plugins/fleet/public/types/intra_app_route_state.ts b/x-pack/plugins/fleet/public/types/intra_app_route_state.ts index a36a72ee3516c0..e3ffef62ac4ec4 100644 --- a/x-pack/plugins/fleet/public/types/intra_app_route_state.ts +++ b/x-pack/plugins/fleet/public/types/intra_app_route_state.ts @@ -56,6 +56,8 @@ export interface AgentDetailsReassignPolicyAction { export interface IntegrationsAppBrowseRouteState { /** The agent policy that we are browsing integrations for */ forAgentPolicyId: string; + /** The integration tab the user navigated to details from */ + fromIntegrations: 'installed' | 'updates_available' | undefined; } /** diff --git a/x-pack/plugins/fleet/server/integration_tests/__snapshots__/cloud_preconfiguration.test.ts.snap b/x-pack/plugins/fleet/server/integration_tests/__snapshots__/cloud_preconfiguration.test.ts.snap index 4a93afd73b130d..e36302a955fcff 100644 --- a/x-pack/plugins/fleet/server/integration_tests/__snapshots__/cloud_preconfiguration.test.ts.snap +++ b/x-pack/plugins/fleet/server/integration_tests/__snapshots__/cloud_preconfiguration.test.ts.snap @@ -68,6 +68,7 @@ Object { "max_connections": 0, "max_event_size": 307200, "max_header_size": 1048576, + "pprof.enabled": false, "read_timeout": "3600s", "response_headers": null, "rum": Object { diff --git a/x-pack/plugins/fleet/server/integration_tests/cloud_preconfiguration.test.ts b/x-pack/plugins/fleet/server/integration_tests/cloud_preconfiguration.test.ts index 0442a48e22deff..04bc8d37485ad8 100644 --- a/x-pack/plugins/fleet/server/integration_tests/cloud_preconfiguration.test.ts +++ b/x-pack/plugins/fleet/server/integration_tests/cloud_preconfiguration.test.ts @@ -176,46 +176,49 @@ describe('Fleet preconfiguration reset', () => { ); expect(fleetServerPackagePolicy?.attributes.vars).toMatchInlineSnapshot(`undefined`); expect(fleetServerPackagePolicy?.attributes.inputs).toMatchInlineSnapshot(` - Array [ - Object { - "compiled_input": Object { - "server": Object { - "host": "0.0.0.0", - "port": 8220, - }, - "server.runtime": Object { - "gc_percent": 20, - }, - }, - "enabled": true, - "keep_enabled": true, - "policy_template": "fleet_server", - "streams": Array [], - "type": "fleet-server", - "vars": Object { - "custom": Object { - "type": "yaml", - "value": "server.runtime: - gc_percent: 20 # Force the GC to execute more frequently: see https://golang.org/pkg/runtime/debug/#SetGCPercent - ", - }, - "host": Object { - "frozen": true, - "type": "text", - "value": "0.0.0.0", - }, - "max_connections": Object { - "type": "integer", + Array [ + Object { + "compiled_input": Object { + "server": Object { + "host": "0.0.0.0", + "port": 8220, + }, + "server.runtime": Object { + "gc_percent": 20, + }, }, - "port": Object { - "frozen": true, - "type": "integer", - "value": 8220, + "enabled": true, + "keep_enabled": true, + "policy_template": "fleet_server", + "streams": Array [], + "type": "fleet-server", + "vars": Object { + "custom": Object { + "type": "yaml", + "value": "server.runtime: + gc_percent: 20 # Force the GC to execute more frequently: see https://golang.org/pkg/runtime/debug/#SetGCPercent + ", + }, + "host": Object { + "frozen": true, + "type": "text", + "value": "0.0.0.0", + }, + "max_agents": Object { + "type": "integer", + }, + "max_connections": Object { + "type": "integer", + }, + "port": Object { + "frozen": true, + "type": "integer", + "value": 8220, + }, }, }, - }, - ] - `); + ] + `); }); }); describe('Adding APM to a preconfigured agent policy after first setup', () => { diff --git a/x-pack/plugins/fleet/server/integration_tests/helpers/docker_registry_helper.ts b/x-pack/plugins/fleet/server/integration_tests/helpers/docker_registry_helper.ts index 622cc8d1472732..f400becfa0085c 100644 --- a/x-pack/plugins/fleet/server/integration_tests/helpers/docker_registry_helper.ts +++ b/x-pack/plugins/fleet/server/integration_tests/helpers/docker_registry_helper.ts @@ -24,7 +24,7 @@ export function useDockerRegistry() { let dockerProcess: ChildProcess | undefined; async function startDockerRegistryServer() { - const dockerImage = `docker.elastic.co/package-registry/distribution:e1a3906e0c9944ecade05308022ba35eb0ebd00a`; + const dockerImage = `docker.elastic.co/package-registry/distribution:93ffe45d8c4ae11365bc70b1038643121049b9fe`; const args = ['run', '--rm', '-p', `${packageRegistryPort}:8080`, dockerImage]; diff --git a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts index ab563255ea1647..93c67a11e2d0e2 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/agent.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/agent.ts @@ -7,6 +7,7 @@ import { schema } from '@kbn/config-schema'; import moment from 'moment'; +import semverIsValid from 'semver/functions/valid'; import { NewAgentActionSchema } from '../models'; @@ -61,13 +62,21 @@ export const PostBulkAgentUnenrollRequestSchema = { }), }; +function validateVersion(s: string) { + if (!semverIsValid(s)) { + return 'not a valid semver'; + } +} + export const PostAgentUpgradeRequestSchema = { params: schema.object({ agentId: schema.string(), }), body: schema.object({ source_uri: schema.maybe(schema.string()), - version: schema.string(), + version: schema.string({ + validate: validateVersion, + }), force: schema.maybe(schema.boolean()), }), }; @@ -76,7 +85,7 @@ export const PostBulkAgentUpgradeRequestSchema = { body: schema.object({ agents: schema.oneOf([schema.arrayOf(schema.string()), schema.string()]), source_uri: schema.maybe(schema.string()), - version: schema.string(), + version: schema.string({ validate: validateVersion }), force: schema.maybe(schema.boolean()), rollout_duration_seconds: schema.maybe(schema.number({ min: 600 })), start_time: schema.maybe( diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/create_condition_script.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/create_condition_script.ts index 782c65d8a0beb7..82c4afb49ed3eb 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/create_condition_script.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/lib/create_condition_script.ts @@ -15,10 +15,27 @@ export const createConditionScript = ( ) => { const threshold = conditionThresholds.map((n) => convertMetricValue(metric, n)); if (comparator === Comparator.BETWEEN && threshold.length === 2) { - return `params.value > ${threshold[0]} && params.value < ${threshold[1]} ? 1 : 0`; + return { + source: `params.value > params.threshold0 && params.value < params.threshold1 ? 1 : 0`, + params: { + threshold0: threshold[0], + threshold1: threshold[1], + }, + }; } if (comparator === Comparator.OUTSIDE_RANGE && threshold.length === 2) { - return `params.value < ${threshold[0]} && params.value > ${threshold[1]} ? 1 : 0`; + return { + source: `params.value < params.threshold0 && params.value > params.threshold1 ? 1 : 0`, + params: { + threshold0: threshold[0], + threshold1: threshold[1], + }, + }; } - return `params.value ${comparator} ${threshold[0]} ? 1 : 0`; + return { + source: `params.value ${comparator} params.threshold ? 1 : 0`, + params: { + threshold: threshold[0], + }, + }; }; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/create_condition_script.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/create_condition_script.ts index 843a1a79eaf62b..b4285863dbccbd 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/create_condition_script.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/create_condition_script.ts @@ -8,10 +8,27 @@ import { Comparator } from '../../../../../common/alerting/metrics'; export const createConditionScript = (threshold: number[], comparator: Comparator) => { if (comparator === Comparator.BETWEEN && threshold.length === 2) { - return `params.value > ${threshold[0]} && params.value < ${threshold[1]} ? 1 : 0`; + return { + source: `params.value > params.threshold0 && params.value < params.threshold1 ? 1 : 0`, + params: { + threshold0: threshold[0], + threshold1: threshold[1], + }, + }; } if (comparator === Comparator.OUTSIDE_RANGE && threshold.length === 2) { - return `params.value < ${threshold[0]} && params.value > ${threshold[1]} ? 1 : 0`; + return { + source: `params.value < params.threshold0 && params.value > params.threshold1 ? 1 : 0`, + params: { + threshold0: threshold[0], + threshold1: threshold[1], + }, + }; } - return `params.value ${comparator} ${threshold[0]} ? 1 : 0`; + return { + source: `params.value ${comparator} params.threshold ? 1 : 0`, + params: { + threshold: threshold[0], + }, + }; }; diff --git a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx index 4ae1b8860c8782..c17417e5106a11 100644 --- a/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx +++ b/x-pack/plugins/lens/public/app_plugin/lens_top_nav.tsx @@ -367,14 +367,16 @@ export const LensTopNavMenu = ({ datasourceMap[activeDatasourceId], datasourceStates[activeDatasourceId].state, activeData, + data.query.timefilter.timefilter.getTime(), application.capabilities ); }, [ - activeData, activeDatasourceId, + discover, datasourceMap, datasourceStates, - discover, + activeData, + data.query.timefilter.timefilter, application.capabilities, ]); diff --git a/x-pack/plugins/lens/public/app_plugin/show_underlying_data.test.ts b/x-pack/plugins/lens/public/app_plugin/show_underlying_data.test.ts index 367349f17a5b2c..4ed822e7dc2f67 100644 --- a/x-pack/plugins/lens/public/app_plugin/show_underlying_data.test.ts +++ b/x-pack/plugins/lens/public/app_plugin/show_underlying_data.test.ts @@ -17,7 +17,13 @@ describe('getLayerMetaInfo', () => { }; it('should return error in case of no data', () => { expect( - getLayerMetaInfo(createMockDatasource('testDatasource'), {}, undefined, capabilities).error + getLayerMetaInfo( + createMockDatasource('testDatasource'), + {}, + undefined, + undefined, + capabilities + ).error ).toBe('Visualization has no data available to show'); }); @@ -30,20 +36,27 @@ describe('getLayerMetaInfo', () => { datatable1: { type: 'datatable', columns: [], rows: [] }, datatable2: { type: 'datatable', columns: [], rows: [] }, }, + undefined, capabilities ).error ).toBe('Cannot show underlying data for visualizations with multiple layers'); }); it('should return error in case of missing activeDatasource', () => { - expect(getLayerMetaInfo(undefined, {}, undefined, capabilities).error).toBe( + expect(getLayerMetaInfo(undefined, {}, undefined, undefined, capabilities).error).toBe( 'Visualization has no data available to show' ); }); it('should return error in case of missing configuration/state', () => { expect( - getLayerMetaInfo(createMockDatasource('testDatasource'), undefined, {}, capabilities).error + getLayerMetaInfo( + createMockDatasource('testDatasource'), + undefined, + {}, + undefined, + capabilities + ).error ).toBe('Visualization has no data available to show'); }); @@ -67,10 +80,35 @@ describe('getLayerMetaInfo', () => { }; mockDatasource.getPublicAPI.mockReturnValue(updatedPublicAPI); expect( - getLayerMetaInfo(createMockDatasource('testDatasource'), {}, {}, capabilities).error + getLayerMetaInfo(createMockDatasource('testDatasource'), {}, {}, undefined, capabilities) + .error ).toBe('Visualization has no data available to show'); }); + it('should return error in case of getFilters returning errors', () => { + const mockDatasource = createMockDatasource('testDatasource'); + const updatedPublicAPI: DatasourcePublicAPI = { + datasourceId: 'indexpattern', + getOperationForColumnId: jest.fn(), + getTableSpec: jest.fn(() => [{ columnId: 'col1', fields: ['bytes'] }]), + getVisualDefaults: jest.fn(), + getSourceId: jest.fn(), + getFilters: jest.fn(() => ({ error: 'filters error' })), + }; + mockDatasource.getPublicAPI.mockReturnValue(updatedPublicAPI); + expect( + getLayerMetaInfo( + mockDatasource, + {}, // the publicAPI has been mocked, so no need for a state here + { + datatable1: { type: 'datatable', columns: [], rows: [] }, + }, + undefined, + capabilities + ).error + ).toBe('filters error'); + }); + it('should not be visible if discover is not available', () => { // both capabilities should be enabled to enable discover expect( @@ -80,6 +118,7 @@ describe('getLayerMetaInfo', () => { { datatable1: { type: 'datatable', columns: [], rows: [] }, }, + undefined, { navLinks: { discover: false }, discover: { show: true }, @@ -93,6 +132,7 @@ describe('getLayerMetaInfo', () => { { datatable1: { type: 'datatable', columns: [], rows: [] }, }, + undefined, { navLinks: { discover: true }, discover: { show: false }, @@ -124,6 +164,7 @@ describe('getLayerMetaInfo', () => { { datatable1: { type: 'datatable', columns: [], rows: [] }, }, + undefined, capabilities ); expect(error).toBeUndefined(); diff --git a/x-pack/plugins/lens/public/app_plugin/show_underlying_data.ts b/x-pack/plugins/lens/public/app_plugin/show_underlying_data.ts index e673108585524e..a3900d229363f9 100644 --- a/x-pack/plugins/lens/public/app_plugin/show_underlying_data.ts +++ b/x-pack/plugins/lens/public/app_plugin/show_underlying_data.ts @@ -12,6 +12,7 @@ import { buildCustomFilter, buildEsQuery, FilterStateStore, + TimeRange, } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import { RecursiveReadonly } from '@kbn/utility-types'; @@ -59,6 +60,7 @@ export function getLayerMetaInfo( currentDatasource: Datasource | undefined, datasourceState: unknown, activeData: TableInspectorAdapter | undefined, + timeRange: TimeRange | undefined, capabilities: RecursiveReadonly<{ navLinks: Capabilities['navLinks']; discover?: Capabilities['discover']; @@ -116,12 +118,22 @@ export function getLayerMetaInfo( }; } + const filtersOrError = datasourceAPI.getFilters(activeData, timeRange); + + if ('error' in filtersOrError) { + return { + meta: undefined, + error: filtersOrError.error, + isVisible, + }; + } + const uniqueFields = [...new Set(columnsWithNoTimeShifts.map(({ fields }) => fields).flat())]; return { meta: { id: datasourceAPI.getSourceId()!, columns: uniqueFields, - filters: datasourceAPI.getFilters(activeData), + filters: filtersOrError, }, error: undefined, isVisible, diff --git a/x-pack/plugins/lens/public/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/embeddable/embeddable.tsx index bc7770e815ba6c..fff323ae4293b7 100644 --- a/x-pack/plugins/lens/public/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/embeddable/embeddable.tsx @@ -178,6 +178,7 @@ function getViewUnderlyingDataArgs({ activeDatasource, activeDatasourceState, activeData, + timeRange, capabilities ); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 8ed569ddfd3283..6ff17eadd63886 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -10,6 +10,7 @@ import { render } from 'react-dom'; import { I18nProvider } from '@kbn/i18n-react'; import type { CoreStart, SavedObjectReference } from '@kbn/core/public'; import { i18n } from '@kbn/i18n'; +import { TimeRange } from '@kbn/es-query'; import type { IStorageWrapper } from '@kbn/kibana-utils-plugin/public'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import { isEqual } from 'lodash'; @@ -532,8 +533,14 @@ export function getIndexPatternDatasource({ return null; }, getSourceId: () => layer.indexPatternId, - getFilters: (activeData: FramePublicAPI['activeData']) => - getFiltersInLayer(layer, visibleColumnIds, activeData?.[layerId]), + getFilters: (activeData: FramePublicAPI['activeData'], timeRange?: TimeRange) => + getFiltersInLayer( + layer, + visibleColumnIds, + activeData?.[layerId], + state.indexPatterns[layer.indexPatternId], + timeRange + ), getVisualDefaults: () => getVisualDefaultsForLayer(layer), }; }, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/utils.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/utils.tsx index b22369dfb2dd29..768783d5ce38c5 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/utils.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/utils.tsx @@ -9,6 +9,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import type { DocLinksStart } from '@kbn/core/public'; +import { TimeRange } from '@kbn/es-query'; import { EuiLink, EuiTextColor, EuiButton, EuiSpacer } from '@elastic/eui'; import { DatatableColumn } from '@kbn/expressions-plugin'; @@ -27,6 +28,7 @@ import { updateDefaultLabels, RangeIndexPatternColumn, FormulaIndexPatternColumn, + DateHistogramIndexPatternColumn, } from './operations'; import { getInvalidFieldMessage, isColumnOfType } from './operations/definitions/helpers'; @@ -341,6 +343,21 @@ function extractQueriesFromRanges(column: RangeIndexPatternColumn) { .filter(({ query }) => query?.trim()); } +/** + * If the data view doesn't have a default time field, Discover can't use the global time range - construct an equivalent filter instead + */ +function extractTimeRangeFromDateHistogram( + column: DateHistogramIndexPatternColumn, + timeRange: TimeRange +) { + return [ + { + language: 'kuery', + query: `${column.sourceField} >= "${timeRange.from}" AND ${column.sourceField} <= "${timeRange.to}"`, + }, + ]; +} + /** * Given an Terms/Top values column transform each entry into a "field: term" KQL query * This works also for multi-terms variant @@ -442,14 +459,16 @@ function collectOnlyValidQueries( export function getFiltersInLayer( layer: IndexPatternLayer, columnIds: string[], - layerData: NonNullable[string] | undefined + layerData: NonNullable[string] | undefined, + indexPattern: IndexPattern, + timeRange: TimeRange | undefined ) { const filtersGroupedByState = collectFiltersFromMetrics(layer, columnIds); const [enabledFiltersFromMetricsByLanguage, disabledFitleredFromMetricsByLanguage] = ( ['enabled', 'disabled'] as const ).map((state) => groupBy(filtersGroupedByState[state], 'language') as unknown as GroupedQueries); - const filterOperation = columnIds + const filterOperationsOrErrors = columnIds .map((colId) => { const column = layer.columns[colId]; @@ -471,6 +490,28 @@ export function getFiltersInLayer( }; } + if ( + isColumnOfType('date_histogram', column) && + timeRange && + column.sourceField && + !column.params.ignoreTimeRange && + indexPattern.timeFieldName !== column.sourceField + ) { + if (indexPattern.timeFieldName) { + // non-default time field is not supported in Discover if data view has a time field + return { + error: i18n.translate('xpack.lens.indexPattern.nonDefaultTimeFieldError', { + defaultMessage: + 'Underlying data does not support date histograms on non-default time fields if time field is set on the data view', + }), + }; + } + // if the data view has no default time field but the date histograms' time field is bound to the time range, create a KQL query for the time range + return { + kuery: extractTimeRangeFromDateHistogram(column, timeRange), + }; + } + if ( isColumnOfType('terms', column) && !(column.params.otherBucket || column.params.missingBucket) @@ -490,13 +531,30 @@ export function getFiltersInLayer( }; } }) - .filter(Boolean) as GroupedQueries[]; + .filter(Boolean); + + const errors = filterOperationsOrErrors.filter((filter) => filter && 'error' in filter) as Array<{ + error: string; + }>; + + if (errors.length) { + return { + error: errors.map(({ error }) => error).join(', '), + }; + } + + const filterOperations = filterOperationsOrErrors as GroupedQueries[]; + return { enabled: { - kuery: collectOnlyValidQueries(enabledFiltersFromMetricsByLanguage, filterOperation, 'kuery'), + kuery: collectOnlyValidQueries( + enabledFiltersFromMetricsByLanguage, + filterOperations, + 'kuery' + ), lucene: collectOnlyValidQueries( enabledFiltersFromMetricsByLanguage, - filterOperation, + filterOperations, 'lucene' ), }, diff --git a/x-pack/plugins/lens/public/types.ts b/x-pack/plugins/lens/public/types.ts index 1ffc300542b09b..4c2f0785e7a3e4 100644 --- a/x-pack/plugins/lens/public/types.ts +++ b/x-pack/plugins/lens/public/types.ts @@ -14,7 +14,7 @@ import type { import type { PaletteOutput } from '@kbn/coloring'; import type { TopNavMenuData } from '@kbn/navigation-plugin/public'; import type { MutableRefObject } from 'react'; -import { Filter } from '@kbn/es-query'; +import { Filter, TimeRange } from '@kbn/es-query'; import type { ExpressionAstExpression, ExpressionRendererEvent, @@ -369,15 +369,20 @@ export interface DatasourcePublicAPI { */ getSourceId: () => string | undefined; /** - * Collect all defined filters from all the operations in the layer + * Collect all defined filters from all the operations in the layer. If it returns undefined, this means that filters can't be constructed for the current layer */ - getFilters: (activeData?: FramePublicAPI['activeData']) => Record< - 'enabled' | 'disabled', - { - kuery: Query[][]; - lucene: Query[][]; - } - >; + getFilters: ( + activeData?: FramePublicAPI['activeData'], + timeRange?: TimeRange + ) => + | { error: string } + | Record< + 'enabled' | 'disabled', + { + kuery: Query[][]; + lucene: Query[][]; + } + >; } export interface DatasourceDataPanelProps { diff --git a/x-pack/plugins/observability/public/pages/rule_details/components/item_value_rule_summary.tsx b/x-pack/plugins/observability/public/pages/rule_details/components/item_value_rule_summary.tsx index 6e178250c53ffc..1a05e991b08fff 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/components/item_value_rule_summary.tsx +++ b/x-pack/plugins/observability/public/pages/rule_details/components/item_value_rule_summary.tsx @@ -8,9 +8,13 @@ import React from 'react'; import { EuiFlexItem, EuiText } from '@elastic/eui'; import { ItemValueRuleSummaryProps } from '../types'; -export function ItemValueRuleSummary({ itemValue, extraSpace = true }: ItemValueRuleSummaryProps) { +export function ItemValueRuleSummary({ + itemValue, + extraSpace = true, + ...otherProps +}: ItemValueRuleSummaryProps) { return ( - + {itemValue} ); diff --git a/x-pack/plugins/observability/public/pages/rule_details/components/page_title.tsx b/x-pack/plugins/observability/public/pages/rule_details/components/page_title.tsx index d75be330df548e..8318e4b7c8e60f 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/components/page_title.tsx +++ b/x-pack/plugins/observability/public/pages/rule_details/components/page_title.tsx @@ -23,7 +23,12 @@ export function PageTitle({ rule }: PageHeaderProps) { const closeTagsPopover = () => setIsTagsPopoverOpen(false); return ( <> - {rule.name} + + + {rule.name} + + + diff --git a/x-pack/plugins/observability/public/pages/rule_details/index.tsx b/x-pack/plugins/observability/public/pages/rule_details/index.tsx index 745ab2ca044fff..e88467b225e9ec 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/index.tsx +++ b/x-pack/plugins/observability/public/pages/rule_details/index.tsx @@ -266,6 +266,7 @@ export function RuleDetailsPage() { rule.notifyWhen; return ( , bottomBorder: false, @@ -284,11 +285,17 @@ export function RuleDetailsPage() { iconType="boxesHorizontal" aria-label="More" onClick={handleOpenPopover} + data-test-subj="moreButton" /> } > - + {i18n.translate('xpack.observability.ruleDetails.editRule', { @@ -302,6 +309,7 @@ export function RuleDetailsPage() { iconType="trash" color="danger" onClick={handleRemoveRule} + data-test-subj="deleteRuleButton" > {i18n.translate('xpack.observability.ruleDetails.deleteRule', { @@ -332,7 +340,7 @@ export function RuleDetailsPage() { > {/* Left side of Rule Summary */} - + @@ -411,7 +419,7 @@ export function RuleDetailsPage() { {/* Right side of Rule Summary */} - + @@ -439,6 +447,7 @@ export function RuleDetailsPage() { })} diff --git a/x-pack/plugins/reporting/public/lib/__snapshots__/stream_handler.test.ts.snap b/x-pack/plugins/reporting/public/lib/__snapshots__/stream_handler.test.ts.snap index 935f3e297b2cb9..ab6a5109a10664 100644 --- a/x-pack/plugins/reporting/public/lib/__snapshots__/stream_handler.test.ts.snap +++ b/x-pack/plugins/reporting/public/lib/__snapshots__/stream_handler.test.ts.snap @@ -84,6 +84,9 @@ Array [ />, }, }, + Object { + "toastLifeTimeMs": 86400000, + }, ] `; @@ -184,44 +187,52 @@ Array [ />, }, }, + Object { + "toastLifeTimeMs": 86400000, + }, ] `; exports[`stream handler showNotifications show success 1`] = ` -Object { - "color": "success", - "data-test-subj": "completeReportSuccess", - "text": MountPoint { - "reactNode": -

- +

+ +

+ -

- , + }, + "title": MountPoint { + "reactNode": -
, + />, + }, }, - "title": MountPoint { - "reactNode": , + Object { + "toastLifeTimeMs": 86400000, }, -} +] `; diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.test.ts b/x-pack/plugins/reporting/public/lib/stream_handler.test.ts index d3075d4e5a9069..6f575652450c1a 100644 --- a/x-pack/plugins/reporting/public/lib/stream_handler.test.ts +++ b/x-pack/plugins/reporting/public/lib/stream_handler.test.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { omit } from 'lodash'; import sinon, { stub } from 'sinon'; import { NotificationsStart } from '@kbn/core/public'; import { coreMock, themeServiceMock, docLinksServiceMock } from '@kbn/core/public/mocks'; @@ -124,7 +123,7 @@ describe('stream handler', () => { expect(mockShowDanger.callCount).toBe(0); expect(mockShowSuccess.callCount).toBe(1); expect(mockShowWarning.callCount).toBe(0); - expect(omit(mockShowSuccess.args[0][0], 'toastLifeTimeMs')).toMatchSnapshot(); + expect(mockShowSuccess.args[0]).toMatchSnapshot(); done(); }); }); diff --git a/x-pack/plugins/reporting/public/lib/stream_handler.ts b/x-pack/plugins/reporting/public/lib/stream_handler.ts index ba2c32de49f642..ef27989a6d4202 100644 --- a/x-pack/plugins/reporting/public/lib/stream_handler.ts +++ b/x-pack/plugins/reporting/public/lib/stream_handler.ts @@ -22,6 +22,12 @@ import { import { Job } from './job'; import { ReportingAPIClient } from './reporting_api_client'; +/** + * @todo Replace with `Infinity` once elastic/eui#5945 is resolved. + * @see https://github.com/elastic/eui/issues/5945 + */ +const COMPLETED_JOB_TOAST_TIMEOUT = 24 * 60 * 60 * 1000; // 24 hours + function updateStored(jobIds: JobId[]): void { sessionStorage.setItem(JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, JSON.stringify(jobIds)); } @@ -54,6 +60,8 @@ export class ReportingNotifierStreamHandler { failed: failedJobs, }: JobSummarySet): Rx.Observable { const showNotificationsAsync = async () => { + const completedOptions = { toastLifeTimeMs: COMPLETED_JOB_TOAST_TIMEOUT }; + // notifications with download link for (const job of completedJobs) { if (job.csvContainsFormulas) { @@ -63,7 +71,8 @@ export class ReportingNotifierStreamHandler { this.apiClient.getManagementLink, this.apiClient.getDownloadLink, this.theme - ) + ), + completedOptions ); } else if (job.maxSizeReached) { this.notifications.toasts.addWarning( @@ -72,7 +81,8 @@ export class ReportingNotifierStreamHandler { this.apiClient.getManagementLink, this.apiClient.getDownloadLink, this.theme - ) + ), + completedOptions ); } else if (job.status === JOB_STATUSES.WARNINGS) { this.notifications.toasts.addWarning( @@ -81,7 +91,8 @@ export class ReportingNotifierStreamHandler { this.apiClient.getManagementLink, this.apiClient.getDownloadLink, this.theme - ) + ), + completedOptions ); } else { this.notifications.toasts.addSuccess( @@ -90,7 +101,8 @@ export class ReportingNotifierStreamHandler { this.apiClient.getManagementLink, this.apiClient.getDownloadLink, this.theme - ) + ), + completedOptions ); } } diff --git a/x-pack/plugins/reporting/public/notifier/job_success.tsx b/x-pack/plugins/reporting/public/notifier/job_success.tsx index f7b71d78de8bd2..44389e164472ac 100644 --- a/x-pack/plugins/reporting/public/notifier/job_success.tsx +++ b/x-pack/plugins/reporting/public/notifier/job_success.tsx @@ -37,12 +37,5 @@ export const getSuccessToast = ( , { theme$: theme.theme$ } ), - /** - * If timeout is an Infinity value, a Not-a-Number (NaN) value, or negative, then timeout will be zero. - * And we cannot use `Number.MAX_SAFE_INTEGER` because EUI's Timer implementation - * subtracts it from the current time to evaluate the remainder. - * @see https://www.w3.org/TR/2011/WD-html5-20110525/timers.html - */ - toastLifeTimeMs: Number.MAX_SAFE_INTEGER - Date.now(), 'data-test-subj': 'completeReportSuccess', }); diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_console/endpoint_response_actions_console_commands.ts b/x-pack/plugins/security_solution/public/management/components/endpoint_console/endpoint_response_actions_console_commands.ts index 161b2aaff4d3e9..7f1eeeabfd69a8 100644 --- a/x-pack/plugins/security_solution/public/management/components/endpoint_console/endpoint_response_actions_console_commands.ts +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_console/endpoint_response_actions_console_commands.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { CommandDefinition } from '../console'; import { IsolateActionResult } from './isolate_action'; +import { ReleaseActionResult } from './release_action'; import { EndpointStatusActionResult } from './status_action'; export const getEndpointResponseActionsConsoleCommands = ( @@ -28,7 +29,27 @@ export const getEndpointResponseActionsConsoleCommands = ( required: false, allowMultiples: false, about: i18n.translate( - 'xpack.securitySolution.endpointConsoleCommands.isolate.arg.command', + 'xpack.securitySolution.endpointConsoleCommands.isolate.arg.comment', + { defaultMessage: 'A comment to go along with the action' } + ), + }, + }, + }, + { + name: 'release', + about: i18n.translate('xpack.securitySolution.endpointConsoleCommands.release.about', { + defaultMessage: 'Release the host', + }), + RenderComponent: ReleaseActionResult, + meta: { + endpointId: endpointAgentId, + }, + args: { + comment: { + required: false, + allowMultiples: false, + about: i18n.translate( + 'xpack.securitySolution.endpointConsoleCommands.release.arg.comment', { defaultMessage: 'A comment to go along with the action' } ), }, diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_console/release_action.test.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_console/release_action.test.tsx new file mode 100644 index 00000000000000..746f2ec5d72a42 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_console/release_action.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AppContextTestRender, createAppRootMockRenderer } from '../../../common/mock/endpoint'; +import { + ConsoleManagerTestComponent, + getConsoleManagerMockRenderResultQueriesAndActions, +} from '../console/components/console_manager/mocks'; +import React from 'react'; +import { getEndpointResponseActionsConsoleCommands } from './endpoint_response_actions_console_commands'; +import { enterConsoleCommand } from '../console/mocks'; +import { waitFor } from '@testing-library/react'; +import { responseActionsHttpMocks } from '../../mocks/response_actions_http_mocks'; + +describe('When using the release action from response actions console', () => { + let render: () => Promise>; + let renderResult: ReturnType; + let apiMocks: ReturnType; + let consoleManagerMockAccess: ReturnType< + typeof getConsoleManagerMockRenderResultQueriesAndActions + >; + + beforeEach(() => { + const mockedContext = createAppRootMockRenderer(); + + apiMocks = responseActionsHttpMocks(mockedContext.coreStart.http); + + render = async () => { + renderResult = mockedContext.render( + { + return { + consoleProps: { + 'data-test-subj': 'test', + commands: getEndpointResponseActionsConsoleCommands('a.b.c'), + }, + }; + }} + /> + ); + + consoleManagerMockAccess = getConsoleManagerMockRenderResultQueriesAndActions(renderResult); + + await consoleManagerMockAccess.clickOnRegisterNewConsole(); + await consoleManagerMockAccess.openRunningConsole(); + + return renderResult; + }; + }); + + it('should call `release` api when command is entered', async () => { + await render(); + enterConsoleCommand(renderResult, 'release'); + + await waitFor(() => { + expect(apiMocks.responseProvider.releaseHost).toHaveBeenCalledTimes(1); + expect(apiMocks.responseProvider.actionDetails).toHaveBeenCalled(); + }); + }); + + it('should accept an optional `--comment`', async () => { + await render(); + enterConsoleCommand(renderResult, 'release --comment "This is a comment"'); + + await waitFor(() => { + expect(apiMocks.responseProvider.releaseHost).toHaveBeenCalledWith( + expect.objectContaining({ + body: expect.stringContaining('This is a comment'), + }) + ); + }); + }); + + it('should only accept one `--comment`', async () => { + await render(); + enterConsoleCommand(renderResult, 'release --comment "one" --comment "two"'); + + expect(renderResult.getByTestId('test-badArgument').textContent).toMatch( + /argument can only be used once: --comment/ + ); + }); + + it('should call the action status api after creating the `release` request', async () => { + await render(); + enterConsoleCommand(renderResult, 'release'); + + await waitFor(() => { + expect(apiMocks.responseProvider.actionDetails).toHaveBeenCalled(); + }); + }); + + it('should show success when `release` action completes with no errors', async () => { + await render(); + enterConsoleCommand(renderResult, 'release'); + + await waitFor(() => { + expect(renderResult.getByTestId('releaseSuccessCallout')).toBeTruthy(); + }); + }); + + it('should show error if release failed to complete successfully', async () => { + const pendingDetailResponse = apiMocks.responseProvider.actionDetails({ + path: '/api/endpoint/action/1.2.3', + }); + pendingDetailResponse.data.wasSuccessful = false; + pendingDetailResponse.data.errors = ['error one', 'error two']; + apiMocks.responseProvider.actionDetails.mockReturnValue(pendingDetailResponse); + await render(); + enterConsoleCommand(renderResult, 'release'); + + await waitFor(() => { + expect(renderResult.getByTestId('releaseErrorCallout').textContent).toMatch( + /error one \| error two/ + ); + }); + }); + + describe('and when console is closed (not terminated) and then reopened', () => { + beforeEach(() => { + const _render = render; + + render = async () => { + const response = await _render(); + enterConsoleCommand(response, 'release'); + + await waitFor(() => { + expect(apiMocks.responseProvider.releaseHost).toHaveBeenCalledTimes(1); + }); + + // Hide the console + await consoleManagerMockAccess.hideOpenedConsole(); + + return response; + }; + }); + + it('should NOT send the `release` request again', async () => { + await render(); + await consoleManagerMockAccess.openRunningConsole(); + + expect(apiMocks.responseProvider.releaseHost).toHaveBeenCalledTimes(1); + }); + + it('should continue to check action status when still pending', async () => { + const pendingDetailResponse = apiMocks.responseProvider.actionDetails({ + path: '/api/endpoint/action/1.2.3', + }); + pendingDetailResponse.data.isCompleted = false; + apiMocks.responseProvider.actionDetails.mockReturnValue(pendingDetailResponse); + await render(); + + expect(apiMocks.responseProvider.actionDetails).toHaveBeenCalledTimes(2); + + await consoleManagerMockAccess.hideOpenedConsole(); + await consoleManagerMockAccess.openRunningConsole(); + + expect(apiMocks.responseProvider.actionDetails).toHaveBeenCalledTimes(3); + }); + + it('should display completion output if done (no additional API calls)', async () => { + await render(); + + expect(apiMocks.responseProvider.actionDetails).toHaveBeenCalledTimes(1); + + await consoleManagerMockAccess.hideOpenedConsole(); + await consoleManagerMockAccess.openRunningConsole(); + + expect(apiMocks.responseProvider.actionDetails).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/components/endpoint_console/release_action.tsx b/x-pack/plugins/security_solution/public/management/components/endpoint_console/release_action.tsx new file mode 100644 index 00000000000000..3e2ae27ffbf099 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/endpoint_console/release_action.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiCallOut } from '@elastic/eui'; +import { ActionDetails } from '../../../../common/endpoint/types'; +import { useGetActionDetails } from '../../hooks/endpoint/use_get_action_details'; +import { EndpointCommandDefinitionMeta } from './types'; +import { useSendReleaseEndpointRequest } from '../../hooks/endpoint/use_send_release_endpoint_request'; +import { CommandExecutionComponentProps } from '../console/types'; + +export const ReleaseActionResult = memo< + CommandExecutionComponentProps< + { + actionId?: string; + actionRequestSent?: boolean; + completedActionDetails?: ActionDetails; + }, + EndpointCommandDefinitionMeta + > +>(({ command, setStore, store, status, setStatus }) => { + const endpointId = command.commandDefinition?.meta?.endpointId; + const { actionId, completedActionDetails } = store; + const isPending = status === 'pending'; + const actionRequestSent = Boolean(store.actionRequestSent); + + const releaseHostApi = useSendReleaseEndpointRequest(); + + const { data: actionDetails } = useGetActionDetails(actionId ?? '-', { + enabled: Boolean(actionId) && isPending, + refetchInterval: isPending ? 3000 : false, + }); + + // Send Release request if not yet done + useEffect(() => { + if (!actionRequestSent && endpointId) { + releaseHostApi.mutate({ + endpoint_ids: [endpointId], + comment: command.args.args?.comment?.value, + }); + + setStore((prevState) => { + return { ...prevState, actionRequestSent: true }; + }); + } + }, [actionRequestSent, command.args.args?.comment?.value, endpointId, releaseHostApi, setStore]); + + // If release request was created, store the action id if necessary + useEffect(() => { + if (releaseHostApi.isSuccess && actionId !== releaseHostApi.data.action) { + setStore((prevState) => { + return { ...prevState, actionId: releaseHostApi.data.action }; + }); + } + }, [actionId, releaseHostApi?.data?.action, releaseHostApi.isSuccess, setStore]); + + useEffect(() => { + if (actionDetails?.data.isCompleted) { + setStatus('success'); + setStore((prevState) => { + return { + ...prevState, + completedActionDetails: actionDetails.data, + }; + }); + } + }, [actionDetails?.data, setStatus, setStore]); + + // Show nothing if still pending + if (isPending) { + return null; + } + + // Show errors + if (completedActionDetails?.errors) { + return ( + + + + ); + } + + // Show Success + return ( + + + + ); +}); +ReleaseActionResult.displayName = 'ReleaseActionResult'; diff --git a/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx b/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx index 80dd9e9563bb56..d1833c2df0ba2b 100644 --- a/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx +++ b/x-pack/plugins/security_solution/public/management/components/management_empty_state.tsx @@ -62,7 +62,7 @@ const PolicyEmptyState = React.memo<{

@@ -78,12 +78,12 @@ const PolicyEmptyState = React.memo<{ {policyEntryPoint ? ( ) : ( )}
@@ -91,7 +91,7 @@ const PolicyEmptyState = React.memo<{
@@ -181,7 +181,8 @@ const EndpointsEmptyState = React.memo<{ }, { title: i18n.translate('xpack.securitySolution.endpoint.list.stepTwoTitle', { - defaultMessage: 'Enroll your agents enabled with Endpoint Security through Fleet', + defaultMessage: + 'Enroll your agents enabled with Endpoint and Cloud Security through Fleet', }), status: actionDisabled ? 'disabled' : '', children: ( @@ -222,13 +223,13 @@ const EndpointsEmptyState = React.memo<{ headerComponent={ } bodyComponent={ } /> diff --git a/x-pack/plugins/security_solution/public/management/hooks/endpoint/use_send_release_endpoint_request.ts b/x-pack/plugins/security_solution/public/management/hooks/endpoint/use_send_release_endpoint_request.ts new file mode 100644 index 00000000000000..297265953bfed8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/hooks/endpoint/use_send_release_endpoint_request.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useMutation, UseMutationOptions, UseMutationResult } from 'react-query'; +import { HttpFetchError } from '@kbn/core/public'; +import { HostIsolationRequestBody, HostIsolationResponse } from '../../../../common/endpoint/types'; +import { unIsolateHost } from '../../../common/lib/endpoint_isolation'; + +/** + * Create host release requests + * @param customOptions + */ +export const useSendReleaseEndpointRequest = ( + customOptions?: UseMutationOptions< + HostIsolationResponse, + HttpFetchError, + HostIsolationRequestBody + > +): UseMutationResult => { + return useMutation( + (releaseData: HostIsolationRequestBody) => { + return unIsolateHost(releaseData); + }, + customOptions + ); +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx index 3ea50af79a9c3c..6d9ade15971ebc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_list.test.tsx @@ -62,7 +62,7 @@ describe('When on the policy list page', () => { it('should show instruction text and a button to add the Endpoint Security integration', () => { expect( renderResult.findByText( - 'From this page, you’ll be able to view and manage the Endpoint Security Integration policies in your environment running Endpoint Security.' + 'From this page, you’ll be able to view and manage the Endpoint and Cloud Security Integration policies in your environment running Endpoint and Cloud Security.' ) ).toBeTruthy(); expect(renderResult.getByTestId('onboardingStartButton')).toBeTruthy(); diff --git a/x-pack/plugins/synthetics/common/constants/ui.ts b/x-pack/plugins/synthetics/common/constants/ui.ts index 994cc205367233..226eda19868864 100644 --- a/x-pack/plugins/synthetics/common/constants/ui.ts +++ b/x-pack/plugins/synthetics/common/constants/ui.ts @@ -14,7 +14,10 @@ export const MONITOR_EDIT_ROUTE = '/edit-monitor/:monitorId'; export const MONITOR_MANAGEMENT_ROUTE = '/manage-monitors'; export const OVERVIEW_ROUTE = '/'; -export const GETTING_STARTED_ROUTE = '/manage-monitors/getting-started'; + +export const MONITORS_ROUTE = '/monitors'; + +export const GETTING_STARTED_ROUTE = '/monitors/getting-started'; export const SETTINGS_ROUTE = '/settings'; diff --git a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts index 2b343cfa688831..3351d3da6140c3 100644 --- a/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts +++ b/x-pack/plugins/synthetics/common/runtime_types/monitor_management/monitor_types.ts @@ -315,12 +315,20 @@ export const EncryptedSyntheticsMonitorWithIdCodec = t.intersection([ t.interface({ id: t.string }), ]); +// TODO: Remove EncryptedSyntheticsMonitorWithIdCodec (as well as SyntheticsMonitorWithIdCodec if possible) along with respective TypeScript types in favor of EncryptedSyntheticsSavedMonitorCodec +export const EncryptedSyntheticsSavedMonitorCodec = t.intersection([ + EncryptedSyntheticsMonitorCodec, + t.interface({ id: t.string, updated_at: t.string }), +]); + export type SyntheticsMonitorWithId = t.TypeOf; export type EncryptedSyntheticsMonitorWithId = t.TypeOf< typeof EncryptedSyntheticsMonitorWithIdCodec >; +export type EncryptedSyntheticsSavedMonitor = t.TypeOf; + export const MonitorDefaultsCodec = t.interface({ [DataStream.HTTP]: HTTPFieldsCodec, [DataStream.TCP]: TCPFieldsCodec, diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/synthetics_page_template.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/synthetics_page_template.tsx index 50497c4c9214cd..95ec1c5a62975b 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/synthetics_page_template.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/common/pages/synthetics_page_template.tsx @@ -11,9 +11,9 @@ import { EuiPageHeaderProps, EuiPageTemplateProps } from '@elastic/eui'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { useInspectorContext } from '@kbn/observability-plugin/public'; import { ClientPluginsStart } from '../../../../../plugin'; -import { EmptyStateLoading } from '../../overview/empty_state/empty_state_loading'; -import { EmptyStateError } from '../../overview/empty_state/empty_state_error'; -import { useHasData } from '../../overview/empty_state/use_has_data'; +import { EmptyStateLoading } from '../../monitors_page/overview/empty_state/empty_state_loading'; +import { EmptyStateError } from '../../monitors_page/overview/empty_state/empty_state_error'; +import { useHasData } from '../../monitors_page/overview/empty_state/use_has_data'; import { useBreakpoints } from '../../../hooks'; interface Props { diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/form_fields/service_locations.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/form_fields/service_locations.tsx index 252b650cc7058a..f345b79ae5488e 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/form_fields/service_locations.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/form_fields/service_locations.tsx @@ -5,12 +5,14 @@ * 2.0. */ -import { EuiComboBox, EuiFormRow } from '@elastic/eui'; -import { Controller, FieldErrors, Control } from 'react-hook-form'; import React from 'react'; -import { i18n } from '@kbn/i18n'; +import { Controller, FieldErrors, Control } from 'react-hook-form'; import { useSelector } from 'react-redux'; -import { serviceLocationsSelector } from '../../../state/monitor_management/selectors'; + +import { EuiComboBox, EuiFormRow } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { selectServiceLocationsState } from '../../../state'; + import { SimpleFormData } from '../simple_monitor_form'; import { ConfigKey } from '../../../../../../common/constants/monitor_management'; @@ -21,7 +23,7 @@ export const ServiceLocationsField = ({ errors: FieldErrors; control: Control; }) => { - const locations = useSelector(serviceLocationsSelector); + const { locations } = useSelector(selectServiceLocationsState); return ( { const dispatch = useDispatch(); useEffect(() => { - dispatch(fetchServiceLocationsAction.get()); + dispatch(getServiceLocations()); }, [dispatch]); useBreadcrumbs([{ text: MONITORING_OVERVIEW_LABEL }]); // No extra breadcrumbs on overview diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/use_simple_monitor.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/use_simple_monitor.ts index 81585a9f26a993..d8d645451c18a9 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/use_simple_monitor.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/getting_started/use_simple_monitor.ts @@ -9,18 +9,22 @@ import { useFetcher } from '@kbn/observability-plugin/public'; import { useEffect } from 'react'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { useSelector } from 'react-redux'; -import { serviceLocationsSelector } from '../../state/monitor_management/selectors'; -import { showSyncErrors } from '../monitor_management/show_sync_errors'; -import { createMonitorAPI } from '../../state/monitor_management/api'; +import { selectServiceLocationsState } from '../../state'; +import { showSyncErrors } from '../monitors_page/management/show_sync_errors'; +import { fetchCreateMonitor } from '../../state'; import { DEFAULT_FIELDS } from '../../../../../common/constants/monitor_defaults'; import { ConfigKey } from '../../../../../common/constants/monitor_management'; -import { DataStream, SyntheticsMonitorWithId } from '../../../../../common/runtime_types'; +import { + DataStream, + ServiceLocationErrors, + SyntheticsMonitorWithId, +} from '../../../../../common/runtime_types'; import { MONITOR_SUCCESS_LABEL, MY_FIRST_MONITOR, SimpleFormData } from './simple_monitor_form'; import { kibanaService } from '../../../../utils/kibana_service'; export const useSimpleMonitor = ({ monitorData }: { monitorData?: SimpleFormData }) => { const { application } = useKibana().services; - const locationsList = useSelector(serviceLocationsSelector); + const { locations: serviceLocations } = useSelector(selectServiceLocationsState); const { data, loading } = useFetcher(() => { if (!monitorData) { @@ -28,7 +32,7 @@ export const useSimpleMonitor = ({ monitorData }: { monitorData?: SimpleFormData } const { urls, locations } = monitorData; - return createMonitorAPI({ + return fetchCreateMonitor({ monitor: { ...DEFAULT_FIELDS.browser, 'source.inline.script': `step('Go to ${urls}', async () => { @@ -46,7 +50,11 @@ export const useSimpleMonitor = ({ monitorData }: { monitorData?: SimpleFormData const newMonitor = data as SyntheticsMonitorWithId; const hasErrors = data && 'attributes' in data && data.attributes.errors?.length > 0; if (hasErrors && !loading) { - showSyncErrors(data.attributes.errors, locationsList, kibanaService.toasts); + showSyncErrors( + (data as { attributes: { errors: ServiceLocationErrors } })?.attributes.errors ?? [], + serviceLocations, + kibanaService.toasts + ); } if (!loading && newMonitor?.id) { @@ -56,7 +64,7 @@ export const useSimpleMonitor = ({ monitorData }: { monitorData?: SimpleFormData }); application?.navigateToApp('uptime', { path: `/monitor/${btoa(newMonitor.id)}` }); } - }, [application, data, loading, locationsList]); + }, [application, data, loading, serviceLocations]); return { data, loading }; }; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_management/monitor_management_page.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_management/monitor_management_page.tsx deleted file mode 100644 index 7c20fcfe1c143c..00000000000000 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_management/monitor_management_page.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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useEffect } from 'react'; -import { useTrackPageview } from '@kbn/observability-plugin/public'; -import { useDispatch, useSelector } from 'react-redux'; -import { Redirect } from 'react-router-dom'; -import { monitorListSelector } from '../../state/monitor_management/selectors'; -import { fetchMonitorListAction } from '../../state/monitor_management/monitor_list'; -import { GETTING_STARTED_ROUTE } from '../../../../../common/constants'; -import { useMonitorManagementBreadcrumbs } from './use_breadcrumbs'; - -export const MonitorManagementPage: React.FC = () => { - useTrackPageview({ app: 'synthetics', path: 'manage-monitors' }); - useTrackPageview({ app: 'synthetics', path: 'manage-monitors', delay: 15000 }); - useMonitorManagementBreadcrumbs(); - - const dispatch = useDispatch(); - - const { total } = useSelector(monitorListSelector); - - useEffect(() => { - dispatch(fetchMonitorListAction.get()); - }, [dispatch]); - - if (total === 0) { - return ; - } - - return ( - <> -

This page is under construction and will be updated in a future release

- - ); -}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_management/use_breadcrumbs.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/hooks/use_breadcrumbs.ts similarity index 65% rename from x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_management/use_breadcrumbs.ts rename to x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/hooks/use_breadcrumbs.ts index 30d23128d1e82a..e13e982203e1af 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_management/use_breadcrumbs.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/hooks/use_breadcrumbs.ts @@ -6,22 +6,22 @@ */ import { i18n } from '@kbn/i18n'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { useBreadcrumbs } from '../../hooks/use_breadcrumbs'; -import { MONITOR_MANAGEMENT_ROUTE } from '../../../../../common/constants'; -import { PLUGIN } from '../../../../../common/constants/plugin'; +import { useBreadcrumbs } from '../../../hooks/use_breadcrumbs'; +import { MONITORS_ROUTE } from '../../../../../../common/constants'; +import { PLUGIN } from '../../../../../../common/constants/plugin'; -export const useMonitorManagementBreadcrumbs = () => { +export const useMonitorListBreadcrumbs = () => { const kibana = useKibana(); const appPath = kibana.services.application?.getUrlForApp(PLUGIN.SYNTHETICS_PLUGIN_ID) ?? ''; useBreadcrumbs([ { text: MONITOR_MANAGEMENT_CRUMB, - href: `${appPath}/${MONITOR_MANAGEMENT_ROUTE}`, + href: `${appPath}/${MONITORS_ROUTE}`, }, ]); }; -const MONITOR_MANAGEMENT_CRUMB = i18n.translate('xpack.synthetics.monitorsPage.monitorCrumb', { +const MONITOR_MANAGEMENT_CRUMB = i18n.translate('xpack.synthetics.monitorsPage.monitorsMCrumb', { defaultMessage: 'Monitors', }); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/hooks/use_inline_errors.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/hooks/use_inline_errors.ts new file mode 100644 index 00000000000000..aaf94f46e283a2 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/hooks/use_inline_errors.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useSelector } from 'react-redux'; +import moment from 'moment'; +import { useMemo } from 'react'; +import { useEsSearch } from '@kbn/observability-plugin/public'; +import { selectEncryptedSyntheticsSavedMonitors } from '../../../state'; +import { Ping } from '../../../../../../common/runtime_types'; +import { EXCLUDE_RUN_ONCE_FILTER } from '../../../../../../common/constants/client_defaults'; +import { useSyntheticsRefreshContext } from '../../../contexts/synthetics_refresh_context'; +import { useInlineErrorsCount } from './use_inline_errors_count'; +import { SYNTHETICS_INDEX_PATTERN } from '../../../../../../common/constants'; + +const sortFieldMap: Record = { + ['name.keyword']: 'monitor.name', + ['urls.keyword']: 'url.full', + ['type.keyword']: 'monitor.type', + '@timestamp': '@timestamp', +}; + +export const getInlineErrorFilters = () => [ + { + exists: { + field: 'summary', + }, + }, + { + exists: { + field: 'error', + }, + }, + { + bool: { + minimum_should_match: 1, + should: [ + { + match_phrase: { + 'error.message': 'journey did not finish executing', + }, + }, + { + match_phrase: { + 'error.message': 'ReferenceError:', + }, + }, + ], + }, + }, + { + range: { + 'monitor.timespan': { + lte: moment().toISOString(), + gte: moment().subtract(5, 'minutes').toISOString(), + }, + }, + }, + EXCLUDE_RUN_ONCE_FILTER, +]; + +export function useInlineErrors({ + onlyInvalidMonitors, + sortField = '@timestamp', + sortOrder = 'desc', +}: { + onlyInvalidMonitors?: boolean; + sortField?: string; + sortOrder?: 'asc' | 'desc'; +}) { + const syntheticsMonitors = useSelector(selectEncryptedSyntheticsSavedMonitors); + + const { lastRefresh } = useSyntheticsRefreshContext(); + + const configIds = syntheticsMonitors.map((monitor) => monitor.id); + + const doFetch = configIds.length > 0 || onlyInvalidMonitors; + + const { data } = useEsSearch( + { + index: doFetch ? SYNTHETICS_INDEX_PATTERN : '', + body: { + size: 1000, + query: { + bool: { + filter: getInlineErrorFilters(), + }, + }, + collapse: { field: 'config_id' }, + sort: [{ [sortFieldMap[sortField]]: sortOrder }], + }, + }, + [syntheticsMonitors, lastRefresh, doFetch, sortField, sortOrder], + { name: 'getInvalidMonitors' } + ); + + const { count, loading: countLoading } = useInlineErrorsCount(); + + return useMemo(() => { + const errorSummaries = data?.hits.hits.map(({ _source: source }) => ({ + ...(source as Ping), + timestamp: (source as any)['@timestamp'], + })); + + return { loading: countLoading, errorSummaries, count }; + }, [count, countLoading, data]); +} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/hooks/use_inline_errors_count.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/hooks/use_inline_errors_count.ts new file mode 100644 index 00000000000000..be6e80e3f84695 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/hooks/use_inline_errors_count.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useSelector } from 'react-redux'; +import { useMemo } from 'react'; +import { useEsSearch } from '@kbn/observability-plugin/public'; +import { selectEncryptedSyntheticsSavedMonitors } from '../../../state'; +import { useSyntheticsRefreshContext } from '../../../contexts/synthetics_refresh_context'; +import { getInlineErrorFilters } from './use_inline_errors'; +import { SYNTHETICS_INDEX_PATTERN } from '../../../../../../common/constants'; + +export function useInlineErrorsCount() { + const syntheticsMonitors = useSelector(selectEncryptedSyntheticsSavedMonitors); + + const { lastRefresh } = useSyntheticsRefreshContext(); + + const { data, loading } = useEsSearch( + { + index: SYNTHETICS_INDEX_PATTERN, + body: { + size: 0, + query: { + bool: { + filter: getInlineErrorFilters(), + }, + }, + aggs: { + total: { + cardinality: { field: 'config_id' }, + }, + }, + }, + }, + [syntheticsMonitors, lastRefresh], + { name: 'getInvalidMonitorsCount' } + ); + + return useMemo(() => { + const errorSummariesCount = data?.aggregations?.total.value; + + return { loading: loading ?? false, count: errorSummariesCount }; + }, [data, loading]); +} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/hooks/use_monitor_list.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/hooks/use_monitor_list.ts new file mode 100644 index 00000000000000..a0e024ef748f89 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/hooks/use_monitor_list.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useCallback, useState } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { + fetchMonitorListAction, + MonitorListPageState, + selectEncryptedSyntheticsSavedMonitors, + selectMonitorListState, +} from '../../../state'; + +export function useMonitorList() { + const dispatch = useDispatch(); + const [isDataQueried, setIsDataQueried] = useState(false); + + const { pageState, loading, error } = useSelector(selectMonitorListState); + const syntheticsMonitors = useSelector(selectEncryptedSyntheticsSavedMonitors); + + const loadPage = useCallback( + (state: MonitorListPageState) => dispatch(fetchMonitorListAction.get(state)), + [dispatch] + ); + + const reloadPage = useCallback(() => loadPage(pageState), [pageState, loadPage]); + + // Initial loading + useEffect(() => { + if (!loading && !isDataQueried) { + reloadPage(); + } + + if (loading) { + setIsDataQueried(true); + } + }, [reloadPage, isDataQueried, syntheticsMonitors, loading]); + + return { + loading, + error, + pageState, + syntheticsMonitors, + loadPage, + reloadPage, + isDataQueried, + }; +} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/labels.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/labels.ts new file mode 100644 index 00000000000000..dbece4ae959834 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/labels.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const LOADING_LABEL = i18n.translate( + 'xpack.synthetics.monitorManagement.manageMonitorLoadingLabel', + { + defaultMessage: 'Loading Monitor Management', + } +); + +export const LEARN_MORE_LABEL = i18n.translate( + 'xpack.synthetics.monitorManagement.manageMonitorLoadingLabel.callout.learnMore', + { + defaultMessage: 'Learn more.', + } +); + +export const CALLOUT_MANAGEMENT_DISABLED = i18n.translate( + 'xpack.synthetics.monitorManagement.callout.disabled', + { + defaultMessage: 'Monitor Management is disabled', + } +); + +export const CALLOUT_MANAGEMENT_CONTACT_ADMIN = i18n.translate( + 'xpack.synthetics.monitorManagement.callout.disabled.adminContact', + { + defaultMessage: 'Please contact your administrator to enable Monitor Management.', + } +); + +export const CALLOUT_MANAGEMENT_DESCRIPTION = i18n.translate( + 'xpack.synthetics.monitorManagement.callout.description.disabled', + { + defaultMessage: + 'Monitor Management is currently disabled. To run your monitors on Elastic managed Synthetics service, enable Monitor Management. Your existing monitors are paused.', + } +); + +export const ERROR_HEADING_BODY = i18n.translate( + 'xpack.synthetics.monitorManagement.editMonitorError.description', + { + defaultMessage: 'Monitor Management settings could not be loaded. Please contact Support.', + } +); + +export const SYNTHETICS_ENABLE_LABEL = i18n.translate( + 'xpack.synthetics.monitorManagement.syntheticsEnableLabel.management', + { + defaultMessage: 'Enable Monitor Management', + } +); + +export const ERROR_HEADING_LABEL = i18n.translate( + 'xpack.synthetics.monitorManagement.editMonitorError', + { + defaultMessage: 'Error loading Monitor Management', + } +); + +export const BETA_TOOLTIP_MESSAGE = i18n.translate( + 'xpack.synthetics.monitors.management.betaLabel', + { + defaultMessage: + 'This functionality is in beta and is subject to change. The design and code is less mature than official generally available features and is being provided as-is with no warranties. Beta features are not subject to the support service level agreement of official generally available features.', + } +); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/loader/loader.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/loader/loader.test.tsx new file mode 100644 index 00000000000000..61e32d41af6dfc --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/loader/loader.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen } from '@testing-library/react'; +import { render } from '../../../../utils/testing/rtl_helpers'; +import { Loader } from './loader'; + +describe('', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('shows children when loading and error are both false', () => { + render( + + {'children'} + + ); + + expect(screen.getByText('children')).toBeInTheDocument(); + }); + + it('shows loading when loading is true', () => { + render( + + {'children'} + + ); + + expect(screen.getByText('loading')).toBeInTheDocument(); + }); + + it('shows error content when error is true ', () => { + render( + + {'children'} + + ); + + expect(screen.getByText('A problem occurred')).toBeInTheDocument(); + expect(screen.getByText('Please try again')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/loader/loader.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/loader/loader.tsx new file mode 100644 index 00000000000000..fbaf5c1d536cfa --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/loader/loader.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiEmptyPrompt, EuiLoadingLogo, EuiSpacer } from '@elastic/eui'; + +interface Props { + loading: boolean; + loadingTitle: React.ReactNode; + error: boolean; + errorTitle?: React.ReactNode; + errorBody?: React.ReactNode; + children: React.ReactNode; +} + +export const Loader = ({ + loading, + loadingTitle, + error, + errorTitle, + errorBody, + children, +}: Props) => { + return ( + <> + {!loading && !error ? children : null} + {error && !loading ? ( + <> + + {errorTitle}} + body={

{errorBody}

} + /> + + ) : null} + {loading ? ( + } + title={

{loadingTitle}

} + data-test-subj="uptimeLoader" + /> + ) : null} + + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_errors/monitor_async_error.test.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_errors/monitor_async_error.test.tsx new file mode 100644 index 00000000000000..7bf951b671c952 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_errors/monitor_async_error.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SyntheticsAppState } from '../../../../state/root_reducer'; +import { screen } from '@testing-library/react'; +import React from 'react'; +import { ConfigKey, DEFAULT_THROTTLING } from '../../../../../../../common/runtime_types'; +import { render } from '../../../../utils/testing/rtl_helpers'; +import { MonitorListState, ServiceLocationsState } from '../../../../state'; +import { MonitorAsyncError } from './monitor_async_error'; + +describe('', () => { + const location1 = 'US Central'; + const location2 = 'US North'; + const reason1 = 'Unauthorized'; + const reason2 = 'Forbidden'; + const status1 = 401; + const status2 = 403; + const state: Partial = { + serviceLocations: { + locations: [ + { + id: 'us_central', + label: location1, + geo: { + lat: 0, + lon: 0, + }, + url: '', + isServiceManaged: true, + }, + { + id: 'us_north', + label: location2, + geo: { + lat: 0, + lon: 0, + }, + url: '', + isServiceManaged: true, + }, + ], + throttling: DEFAULT_THROTTLING, + loading: false, + error: null, + } as ServiceLocationsState, + monitorList: { + error: null, + loading: true, + data: { + perPage: 5, + page: 1, + total: 6, + monitors: [], + syncErrors: [ + { + locationId: 'us_central', + error: { + reason: reason1, + status: status1, + }, + }, + { + locationId: 'us_north', + error: { + reason: reason2, + status: status2, + }, + }, + ], + }, + pageState: { + pageIndex: 1, + pageSize: 10, + sortOrder: 'asc', + sortField: `${ConfigKey.NAME}.keyword`, + }, + } as MonitorListState, + }; + + it('renders when errors are defined', () => { + render(, { state }); + + expect(screen.getByText(new RegExp(reason1))).toBeInTheDocument(); + expect(screen.getByText(new RegExp(`${status1}`))).toBeInTheDocument(); + expect(screen.getByText(new RegExp(reason2))).toBeInTheDocument(); + expect(screen.getByText(new RegExp(`${status2}`))).toBeInTheDocument(); + expect(screen.getByText(new RegExp(location1))).toBeInTheDocument(); + expect(screen.getByText(new RegExp(location2))).toBeInTheDocument(); + }); + + it('renders null when errors are empty', () => { + render(, { + state: { + ...state, + monitorList: { + ...state.monitorList, + data: { + ...(state.monitorList?.data ?? {}), + syncErrors: [], + }, + }, + } as SyntheticsAppState, + }); + + expect(screen.queryByText(new RegExp(reason1))).not.toBeInTheDocument(); + expect(screen.queryByText(new RegExp(`${status1}`))).not.toBeInTheDocument(); + expect(screen.queryByText(new RegExp(reason2))).not.toBeInTheDocument(); + expect(screen.queryByText(new RegExp(`${status2}`))).not.toBeInTheDocument(); + expect(screen.queryByText(new RegExp(location1))).not.toBeInTheDocument(); + expect(screen.queryByText(new RegExp(location2))).not.toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_errors/monitor_async_error.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_errors/monitor_async_error.tsx new file mode 100644 index 00000000000000..4f285dcb911d18 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_errors/monitor_async_error.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useState } from 'react'; +import { useSelector } from 'react-redux'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiButton, EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { selectMonitorListState, selectServiceLocationsState } from '../../../../state'; + +export const MonitorAsyncError = () => { + const [isDismissed, setIsDismissed] = useState(false); + const { + data: { syncErrors }, + } = useSelector(selectMonitorListState); + const { locations } = useSelector(selectServiceLocationsState); + + return syncErrors && syncErrors.length > 0 && !isDismissed ? ( + <> + + } + color="warning" + iconType="alert" + > +

+ +

+
    + {Object.values(syncErrors ?? {}).map((e) => { + return ( +
  • + {`${ + locations.find((location) => location.id === e.locationId)?.label + } - ${STATUS_LABEL}: ${e.error?.status ?? NOT_AVAILABLE_LABEL}; ${REASON_LABEL}: ${ + e.error?.reason ?? NOT_AVAILABLE_LABEL + }`} +
  • + ); + })} +
+ setIsDismissed(true)} color="warning"> + {DISMISS_LABEL} + +
+ + + ) : null; +}; + +const REASON_LABEL = i18n.translate( + 'xpack.synthetics.monitorManagement.monitorSync.failure.reasonLabel', + { + defaultMessage: 'Reason', + } +); + +const STATUS_LABEL = i18n.translate( + 'xpack.synthetics.monitorManagement.monitorSync.failure.statusLabel', + { + defaultMessage: 'Status', + } +); + +const NOT_AVAILABLE_LABEL = i18n.translate( + 'xpack.synthetics.monitorManagement.monitorSync.failure.notAvailable', + { + defaultMessage: 'Not available', + } +); + +const DISMISS_LABEL = i18n.translate( + 'xpack.synthetics.monitorManagement.monitorSync.failure.dismissLabel', + { + defaultMessage: 'Dismiss', + } +); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_container.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_container.tsx new file mode 100644 index 00000000000000..0a9d253d287fcd --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_container.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useMonitorList } from '../hooks/use_monitor_list'; +import { MonitorList } from './monitor_list_table/monitor_list'; +import { MonitorAsyncError } from './monitor_errors/monitor_async_error'; +import { useInlineErrors } from '../hooks/use_inline_errors'; + +export const MonitorListContainer = ({ isEnabled }: { isEnabled?: boolean }) => { + const { + pageState, + error, + loading: monitorsLoading, + syntheticsMonitors, + loadPage, + reloadPage, + } = useMonitorList(); + + const { errorSummaries, loading: errorsLoading } = useInlineErrors({ + onlyInvalidMonitors: false, + sortField: pageState.sortField, + sortOrder: pageState.sortOrder, + }); + + if (!isEnabled && syntheticsMonitors.length === 0) { + return null; + } + + return ( + <> + + + + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/actions.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/actions.tsx new file mode 100644 index 00000000000000..9d92c584592d33 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/actions.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useContext, useEffect, useState } from 'react'; +import { EuiThemeComputed } from '@elastic/eui/src/services/theme/types'; +import { toMountPoint } from '@kbn/kibana-react-plugin/public'; +import { FETCH_STATUS, useFetcher } from '@kbn/observability-plugin/public'; +import { + EuiContextMenuPanel, + EuiContextMenuItem, + EuiPopover, + EuiButtonEmpty, + EuiConfirmModal, +} from '@elastic/eui'; +import { kibanaService } from '../../../../../../utils/kibana_service'; +import { fetchDeleteMonitor } from '../../../../state'; +import { SyntheticsSettingsContext } from '../../../../contexts/synthetics_settings_context'; + +import * as labels from './labels'; + +interface Props { + euiTheme: EuiThemeComputed; + id: string; + name: string; + canEditSynthetics: boolean; + reloadPage: () => void; +} + +export const Actions = ({ euiTheme, id, name, reloadPage, canEditSynthetics }: Props) => { + const { basePath } = useContext(SyntheticsSettingsContext); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const [isDeleting, setIsDeleting] = useState(false); + const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); + + const { status: monitorDeleteStatus } = useFetcher(() => { + if (isDeleting) { + return fetchDeleteMonitor({ id }); + } + }, [id, isDeleting]); + + // TODO: Move deletion logic to redux state + useEffect(() => { + if ( + monitorDeleteStatus === FETCH_STATUS.SUCCESS || + monitorDeleteStatus === FETCH_STATUS.FAILURE + ) { + setIsDeleting(false); + setIsDeleteModalVisible(false); + } + if (monitorDeleteStatus === FETCH_STATUS.FAILURE) { + kibanaService.toasts.addDanger( + { + title: toMountPoint( +

{labels.MONITOR_DELETE_FAILURE_LABEL}

+ ), + }, + { toastLifeTimeMs: 3000 } + ); + } else if (monitorDeleteStatus === FETCH_STATUS.SUCCESS) { + reloadPage(); + kibanaService.toasts.addSuccess( + { + title: toMountPoint( +

{labels.MONITOR_DELETE_SUCCESS_LABEL}

+ ), + }, + { toastLifeTimeMs: 3000 } + ); + } + }, [setIsDeleting, reloadPage, monitorDeleteStatus]); + + const openPopover = () => { + setIsPopoverOpen(true); + }; + + const closePopover = () => { + setIsPopoverOpen(false); + }; + + const handleDeleteMonitor = () => { + setIsDeleteModalVisible(true); + closePopover(); + }; + + const handleConfirmDelete = () => { + setIsDeleting(true); + }; + + const menuButton = ( + + ); + + /* + TODO: Implement duplication functionality + const duplicateMenuItem = ( + + {labels.DUPLICATE_LABEL} + + ); + */ + + /* + TODO: See if disable enabled is needed as an action menu item + const disableEnableMenuItem = ( + isDisabled ? ( + + {labels.ENABLE_LABEL} + + ) : ( + + {labels.DISABLE_LABEL} + + ) + ); + */ + + const menuItems = [ + + {labels.EDIT_LABEL} + , + + {labels.DELETE_LABEL} + , + ]; + + return ( + <> + + + + + {isDeleteModalVisible ? ( + setIsDeleteModalVisible(false)} + onConfirm={handleConfirmDelete} + cancelButtonText={labels.NO_LABEL} + confirmButtonText={labels.YES_LABEL} + buttonColor="danger" + defaultFocusedButton="confirm" + isLoading={isDeleting} + > +

{labels.DELETE_DESCRIPTION_LABEL}

+
+ ) : null} + + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/columns.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/columns.tsx new file mode 100644 index 00000000000000..ece118812c0fb6 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/columns.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiBadge, EuiBasicTableColumn, EuiLink, EuiIcon, EuiThemeComputed } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; +import moment from 'moment'; +import React from 'react'; + +import { + ConfigKey, + DataStream, + EncryptedSyntheticsSavedMonitor, + Ping, + ServiceLocations, + SyntheticsMonitorSchedule, +} from '../../../../../../../common/runtime_types'; + +import { getFrequencyLabel } from './labels'; +import { Actions } from './actions'; +import { MonitorEnabled } from './monitor_enabled'; +import { MonitorLocations } from './monitor_locations'; + +export function getMonitorListColumns({ + basePath, + euiTheme, + errorSummaries, + errorSummariesById, + canEditSynthetics, + reloadPage, + syntheticsMonitors, +}: { + basePath: string; + euiTheme: EuiThemeComputed; + errorSummaries?: Ping[]; + errorSummariesById: Map; + canEditSynthetics: boolean; + syntheticsMonitors: EncryptedSyntheticsSavedMonitor[]; + reloadPage: () => void; +}) { + const getIsMonitorUnHealthy = (monitor: EncryptedSyntheticsSavedMonitor) => { + const errorSummary = errorSummariesById.get(monitor.id); + + if (errorSummary) { + return moment(monitor.updated_at).isBefore(moment(errorSummary.timestamp)); + } + + return false; + }; + + return [ + { + align: 'left' as const, + field: ConfigKey.NAME as string, + name: i18n.translate('xpack.synthetics.management.monitorList.monitorName', { + defaultMessage: 'Monitor name', + }), + sortable: true, + render: (name: string, { id }: EncryptedSyntheticsSavedMonitor) => ( + {name} + ), + }, + { + align: 'left' as const, + field: 'id', + name: i18n.translate('xpack.synthetics.management.monitorList.monitorStatus', { + defaultMessage: 'Status', + }), + sortable: false, + render: (_: string, monitor: EncryptedSyntheticsSavedMonitor) => { + const isMonitorHealthy = !getIsMonitorUnHealthy(monitor); + + return ( + <> + + {isMonitorHealthy ? ( + + ) : ( + + )} + + ); + }, + }, + { + align: 'left' as const, + field: ConfigKey.MONITOR_TYPE, + name: i18n.translate('xpack.synthetics.management.monitorList.monitorType', { + defaultMessage: 'Type', + }), + sortable: true, + render: (monitorType: DataStream) => ( + {monitorType === DataStream.BROWSER ? 'Browser' : 'Ping'} + ), + }, + { + align: 'left' as const, + field: ConfigKey.LOCATIONS, + name: i18n.translate('xpack.synthetics.management.monitorList.locations', { + defaultMessage: 'Locations', + }), + render: (locations: ServiceLocations) => + locations ? : null, + }, + { + align: 'left' as const, + field: ConfigKey.SCHEDULE, + name: i18n.translate('xpack.synthetics.management.monitorList.frequency', { + defaultMessage: 'Frequency', + }), + render: (schedule: SyntheticsMonitorSchedule) => getFrequencyLabel(schedule), + }, + { + align: 'left' as const, + field: ConfigKey.ENABLED as string, + name: i18n.translate('xpack.synthetics.management.monitorList.enabled', { + defaultMessage: 'Enabled', + }), + render: (_enabled: boolean, monitor: EncryptedSyntheticsSavedMonitor) => ( + + ), + }, + { + align: 'right' as const, + name: i18n.translate('xpack.synthetics.management.monitorList.actions', { + defaultMessage: 'Actions', + }), + render: (fields: EncryptedSyntheticsSavedMonitor) => ( + + ), + }, + ] as Array>; +} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/labels.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/labels.tsx new file mode 100644 index 00000000000000..fd910f512caa48 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/labels.tsx @@ -0,0 +1,209 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiI18nNumber, EuiText } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import { ScheduleUnit, SyntheticsMonitorSchedule } from '../../../../../../../common/runtime_types'; + +export const NO_MONITOR_ITEM_SELECTED = i18n.translate( + 'xpack.synthetics.management.monitorList.noItemForSelectedFiltersMessage', + { + defaultMessage: 'No monitors found for selected filter criteria', + description: + 'This message is shown if there are no monitors in the table and some filter or search criteria exists', + } +); + +export const LOADING = i18n.translate('xpack.synthetics.management.monitorList.loading', { + defaultMessage: 'Loading...', + description: 'Shown when the monitor list is waiting for a server response', +}); + +export const NO_DATA_MESSAGE = i18n.translate( + 'xpack.synthetics.management.monitorList.noItemMessage', + { + defaultMessage: 'No monitors found', + description: 'This message is shown if the monitors table is rendered but has no items.', + } +); + +export const EXPAND_LOCATIONS_LABEL = i18n.translate( + 'xpack.synthetics.management.monitorList.locations.expand', + { + defaultMessage: 'Click to view remaining locations', + } +); + +export const EXPAND_TAGS_LABEL = i18n.translate( + 'xpack.synthetics.management.monitorList.tags.expand', + { + defaultMessage: 'Click to view remaining tags', + } +); + +export const EDIT_LABEL = i18n.translate('xpack.synthetics.management.editLabel', { + defaultMessage: 'Edit', +}); + +export const DUPLICATE_LABEL = i18n.translate('xpack.synthetics.management.duplicateLabel', { + defaultMessage: 'Duplicate', +}); + +export const DISABLE_LABEL = i18n.translate('xpack.synthetics.management.disableLabel', { + defaultMessage: 'Disable', +}); + +export const ENABLE_LABEL = i18n.translate('xpack.synthetics.management.enableLabel', { + defaultMessage: 'Enable', +}); + +export const DELETE_LABEL = i18n.translate('xpack.synthetics.management.deleteLabel', { + defaultMessage: 'Delete', +}); + +export const DELETE_DESCRIPTION_LABEL = i18n.translate( + 'xpack.synthetics.management.confirmDescriptionLabel', + { + defaultMessage: + 'This action will delete the monitor but keep any data collected. This action cannot be undone.', + } +); + +export const YES_LABEL = i18n.translate('xpack.synthetics.management.yesLabel', { + defaultMessage: 'Delete', +}); + +export const NO_LABEL = i18n.translate('xpack.synthetics.management.noLabel', { + defaultMessage: 'Cancel', +}); + +export const DELETE_MONITOR_LABEL = i18n.translate( + 'xpack.synthetics.management.deleteMonitorLabel', + { + defaultMessage: 'Delete monitor', + } +); + +export const MONITOR_DELETE_SUCCESS_LABEL = i18n.translate( + 'xpack.synthetics.management.monitorDeleteSuccessMessage', + { + defaultMessage: 'Monitor deleted successfully.', + } +); + +export const MONITOR_DELETE_FAILURE_LABEL = i18n.translate( + 'xpack.synthetics.management.monitorDeleteFailureMessage', + { + defaultMessage: 'Monitor was unable to be deleted. Please try again later.', + } +); + +export const MONITOR_DELETE_LOADING_LABEL = i18n.translate( + 'xpack.synthetics.management.monitorDeleteLoadingMessage', + { + defaultMessage: 'Deleting monitor...', + } +); + +export const getRecordRangeLabel = ({ + rangeStart, + rangeEnd, + total, +}: { + rangeStart: number; + rangeEnd: number; + total: number; +}) => { + // If total is less than the end range, use total as end range. + const availableEndRange = Math.min(rangeEnd, total); + + return ( + + - + + ), + total: , + monitorsLabel: ( + + {i18n.translate('xpack.synthetics.management.monitorList.recordRangeLabel', { + defaultMessage: '{monitorCount, plural, one {Monitor} other {Monitors}}', + values: { + monitorCount: total, + }, + })} + + ), + }} + /> + ); +}; + +export const getFrequencyLabel = (schedule: SyntheticsMonitorSchedule) => { + return schedule.unit === ScheduleUnit.SECONDS ? ( + + {i18n.translate('xpack.synthetics.management.monitorList.frequencyInSeconds', { + description: 'Monitor frequency in seconds', + defaultMessage: + '{countSeconds, number} {countSeconds, plural, one {second} other {seconds}}', + values: { + countSeconds: Number(schedule.number), + }, + })} + + ) : ( + + {i18n.translate('xpack.synthetics.management.monitorList.frequencyInMinutes', { + description: 'Monitor frequency in minutes', + defaultMessage: + '{countMinutes, number} {countMinutes, plural, one {minute} other {minutes}}', + values: { + countMinutes: Number(schedule.number), + }, + })} + + ); +}; + +export const ENABLE_MONITOR_LABEL = i18n.translate( + 'xpack.synthetics.management.enableMonitorLabel', + { + defaultMessage: 'Enable monitor', + } +); + +export const DISABLE_MONITOR_LABEL = i18n.translate( + 'xpack.synthetics.management.disableMonitorLabel', + { + defaultMessage: 'Disable monitor', + } +); + +export const getMonitorEnabledSuccessLabel = (name: string) => + i18n.translate('xpack.synthetics.management.monitorEnabledSuccessMessage', { + defaultMessage: 'Monitor {name} enabled successfully.', + values: { name }, + }); + +export const getMonitorDisabledSuccessLabel = (name: string) => + i18n.translate('xpack.synthetics.management.monitorDisabledSuccessMessage', { + defaultMessage: 'Monitor {name} disabled successfully.', + values: { name }, + }); + +export const getMonitorEnabledUpdateFailureMessage = (name: string) => + i18n.translate('xpack.synthetics.management.monitorEnabledUpdateFailureMessage', { + defaultMessage: 'Unable to update monitor {name}.', + values: { name }, + }); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_enabled.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_enabled.tsx new file mode 100644 index 00000000000000..e98ca2a466f0d7 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_enabled.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiSwitch, EuiSwitchEvent, EuiLoadingSpinner } from '@elastic/eui'; +import React, { useEffect, useState } from 'react'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { FETCH_STATUS, useFetcher } from '@kbn/observability-plugin/public'; + +import { ConfigKey, EncryptedSyntheticsMonitor } from '../../../../../../../common/runtime_types'; +import { fetchUpsertMonitor } from '../../../../state'; + +import * as labels from './labels'; + +interface Props { + id: string; + monitor: EncryptedSyntheticsMonitor; + reloadPage: () => void; + isDisabled?: boolean; +} + +export const MonitorEnabled = ({ id, monitor, reloadPage, isDisabled }: Props) => { + const [isEnabled, setIsEnabled] = useState(null); + + const { notifications } = useKibana(); + + const { status } = useFetcher(() => { + if (isEnabled !== null) { + return fetchUpsertMonitor({ id, monitor: { ...monitor, [ConfigKey.ENABLED]: isEnabled } }); + } + }, [isEnabled]); + + useEffect(() => { + if (status === FETCH_STATUS.FAILURE) { + notifications.toasts.danger({ + title: ( +

+ {labels.getMonitorEnabledUpdateFailureMessage(monitor[ConfigKey.NAME])} +

+ ), + toastLifeTimeMs: 3000, + }); + setIsEnabled(null); + } else if (status === FETCH_STATUS.SUCCESS) { + notifications.toasts.success({ + title: ( +

+ {isEnabled + ? labels.getMonitorEnabledSuccessLabel(monitor[ConfigKey.NAME]) + : labels.getMonitorDisabledSuccessLabel(monitor[ConfigKey.NAME])} +

+ ), + toastLifeTimeMs: 3000, + }); + reloadPage(); + } + }, [status]); // eslint-disable-line react-hooks/exhaustive-deps + + const enabled = isEnabled ?? monitor[ConfigKey.ENABLED]; + const isLoading = status === FETCH_STATUS.LOADING; + + const handleEnabledChange = (event: EuiSwitchEvent) => { + const checked = event.target.checked; + setIsEnabled(checked); + }; + + return ( + <> + {isLoading ? ( + + ) : ( + + )} + + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_list.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_list.tsx new file mode 100644 index 00000000000000..f692e843546660 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_list.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useCallback, useContext, useMemo } from 'react'; +import { + Criteria, + EuiBasicTable, + EuiTableSortingType, + EuiPanel, + EuiSpacer, + useEuiTheme, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { IHttpSerializedFetchError } from '../../../../state/utils/http_error'; +import { MonitorListPageState } from '../../../../state'; +import { useCanEditSynthetics } from '../../../../../../hooks/use_capabilities'; +import { + ConfigKey, + Ping, + EncryptedSyntheticsSavedMonitor, +} from '../../../../../../../common/runtime_types'; +import { SyntheticsSettingsContext } from '../../../../contexts/synthetics_settings_context'; +import { useBreakpoints } from '../../../../hooks'; +import { getMonitorListColumns } from './columns'; +import * as labels from './labels'; + +interface Props { + pageState: MonitorListPageState; + syntheticsMonitors: EncryptedSyntheticsSavedMonitor[]; + error: IHttpSerializedFetchError | null; + loading: boolean; + loadPage: (state: MonitorListPageState) => void; + reloadPage: () => void; + errorSummaries?: Ping[]; +} + +export const MonitorList = ({ + pageState: { pageIndex, pageSize, sortField, sortOrder }, + syntheticsMonitors, + error, + loading, + loadPage, + reloadPage, + errorSummaries, +}: Props) => { + const { basePath } = useContext(SyntheticsSettingsContext); + const isXl = useBreakpoints().up('xl'); + const canEditSynthetics = useCanEditSynthetics(); + const { euiTheme } = useEuiTheme(); + + const errorSummariesById = useMemo( + () => + (errorSummaries ?? []).reduce((acc, cur) => { + if (cur.config_id) { + acc.set(cur.config_id, cur); + } + return acc; + }, new Map()), + [errorSummaries] + ); + + const handleOnChange = useCallback( + ({ + page = { index: 0, size: 10 }, + sort = { field: ConfigKey.NAME, direction: 'asc' }, + }: Criteria) => { + const { index, size } = page; + const { field, direction } = sort; + + loadPage({ + pageIndex: index, + pageSize: size, + sortField: `${field}.keyword`, + sortOrder: direction, + }); + }, + [loadPage] + ); + + const pagination = { + pageIndex: pageIndex - 1, // page index for EuiBasicTable is base 0 + pageSize, + totalItemCount: syntheticsMonitors.length || 0, + pageSizeOptions: [5, 10, 25, 50, 100], + }; + + const sorting: EuiTableSortingType = { + sort: { + field: sortField.replace('.keyword', '') as keyof EncryptedSyntheticsSavedMonitor, + direction: sortOrder, + }, + }; + + const recordRangeLabel = labels.getRecordRangeLabel({ + rangeStart: pageSize * pageIndex + 1, + rangeEnd: pageSize * pageIndex + pageSize, + total: syntheticsMonitors.length, + }); + + const columns = getMonitorListColumns({ + basePath, + euiTheme, + errorSummaries, + errorSummariesById, + canEditSynthetics, + syntheticsMonitors, + reloadPage, + }); + + return ( + + + {recordRangeLabel} + +
+ +
+ ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_locations.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_locations.tsx new file mode 100644 index 00000000000000..fc9b014b184812 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/monitor_locations.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { EuiBadge, EuiBadgeGroup } from '@elastic/eui'; +import { ServiceLocations, ServiceLocation } from '../../../../../../../common/runtime_types'; +import { useLocations } from '../../../../hooks/use_locations'; +import { EXPAND_LOCATIONS_LABEL } from './labels'; + +interface Props { + locations: ServiceLocations; +} + +const INITIAL_LIMIT = 3; + +export const MonitorLocations = ({ locations }: Props) => { + const { locations: allLocations } = useLocations(); + const [toDisplay, setToDisplay] = useState(INITIAL_LIMIT); + + const locationsToDisplay = locations.slice(0, toDisplay); + + return ( + + {locationsToDisplay.map((location: ServiceLocation) => ( + + {`${allLocations.find((loc) => loc.id === location.id)?.label}`} + + ))} + {locations.length > toDisplay && ( + { + setToDisplay(locations.length); + }} + onClickAriaLabel={EXPAND_LOCATIONS_LABEL} + > + +{locations.length - INITIAL_LIMIT} + + )} + + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/tags.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/tags.tsx new file mode 100644 index 00000000000000..b50d97fcecefa1 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/monitor_list_table/tags.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState } from 'react'; +import { EuiBadge, EuiBadgeGroup } from '@elastic/eui'; +import { EXPAND_TAGS_LABEL } from './labels'; + +interface Props { + tags: string[]; +} + +export const MonitorTags = ({ tags }: Props) => { + const [toDisplay, setToDisplay] = useState(5); + + const tagsToDisplay = tags.slice(0, toDisplay); + + return ( + + {tagsToDisplay.map((tag) => ( + // filtering only makes sense in monitor list, where we have summary + + {tag} + + ))} + {tags.length > toDisplay && ( + { + setToDisplay(tags.length); + }} + onClickAriaLabel={EXPAND_TAGS_LABEL} + > + +{tags.length - 5} + + )} + + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/page_header/monitors_page_header.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/page_header/monitors_page_header.tsx new file mode 100644 index 00000000000000..8dbeaa74d618d9 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/page_header/monitors_page_header.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useContext } from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiBetaBadge, EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; + +import { MONITOR_ADD_ROUTE } from '../../../../../../../common/constants'; + +import { SyntheticsSettingsContext } from '../../../../contexts/synthetics_settings_context'; + +import { BETA_TOOLTIP_MESSAGE } from '../labels'; + +export const MonitorsPageHeader = () => { + const { basePath } = useContext(SyntheticsSettingsContext); + + return ( + + + + + +
+ +
+
+ + + + + +
+ ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_management/show_sync_errors.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/show_sync_errors.tsx similarity index 98% rename from x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_management/show_sync_errors.tsx rename to x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/show_sync_errors.tsx index 2d4412b71f230e..ee048593e5de1d 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitor_management/show_sync_errors.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/show_sync_errors.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { toMountPoint } from '@kbn/kibana-react-plugin/public'; import { IToasts } from '@kbn/core/public'; -import { ServiceLocationErrors, ServiceLocations } from '../../../../../common/runtime_types'; +import { ServiceLocationErrors, ServiceLocations } from '../../../../../../common/runtime_types'; export const showSyncErrors = ( errors: ServiceLocationErrors, diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/synthetics_enablement/labels.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/synthetics_enablement/labels.ts new file mode 100644 index 00000000000000..ad7220e328f3bc --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/synthetics_enablement/labels.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const SYNTHETICS_ENABLE_FAILURE = i18n.translate( + 'xpack.synthetics.monitorManagement.syntheticsEnabledFailure', + { + defaultMessage: 'Monitor Management was not able to be enabled. Please contact support.', + } +); + +export const SYNTHETICS_DISABLE_FAILURE = i18n.translate( + 'xpack.synthetics.monitorManagement.syntheticsDisabledFailure', + { + defaultMessage: 'Monitor Management was not able to be disabled. Please contact support.', + } +); + +export const SYNTHETICS_ENABLE_SUCCESS = i18n.translate( + 'xpack.synthetics.monitorManagement.syntheticsEnableSuccess', + { + defaultMessage: 'Monitor Management enabled successfully.', + } +); + +export const SYNTHETICS_DISABLE_SUCCESS = i18n.translate( + 'xpack.synthetics.monitorManagement.syntheticsDisabledSuccess', + { + defaultMessage: 'Monitor Management disabled successfully.', + } +); + +export const MONITOR_MANAGEMENT_ENABLEMENT_LABEL = i18n.translate( + 'xpack.synthetics.monitorManagement.emptyState.enablement.enabled.title', + { + defaultMessage: 'Enable Monitor Management', + } +); + +export const MONITOR_MANAGEMENT_DISABLED_LABEL = i18n.translate( + 'xpack.synthetics.monitorManagement.emptyState.enablement.disabled.title', + { + defaultMessage: 'Monitor Management is disabled', + } +); + +export const MONITOR_MANAGEMENT_ENABLEMENT_MESSAGE = i18n.translate( + 'xpack.synthetics.monitorManagement.emptyState.enablement', + { + defaultMessage: + 'Enable Monitor Management to run lightweight and real-browser monitors from hosted testing locations around the world. Enabling Monitor Management will generate an API key to allow the Synthetics Service to write back to your Elasticsearch cluster.', + } +); + +export const MONITOR_MANAGEMENT_DISABLED_MESSAGE = i18n.translate( + 'xpack.synthetics.monitorManagement.emptyState.enablement.disabledDescription', + { + defaultMessage: + 'Monitor Management is currently disabled. Monitor Management allows you to run lightweight and real-browser monitors from hosted testing locations around the world. To enable Monitor Management, please contact an administrator.', + } +); + +export const MONITOR_MANAGEMENT_ENABLEMENT_BTN_LABEL = i18n.translate( + 'xpack.synthetics.monitorManagement.emptyState.enablement.title', + { + defaultMessage: 'Enable', + } +); + +export const DOCS_LABEL = i18n.translate( + 'xpack.synthetics.monitorManagement.emptyState.enablement.doc', + { + defaultMessage: 'Read the docs', + } +); + +export const LEARN_MORE_LABEL = i18n.translate( + 'xpack.synthetics.monitorManagement.emptyState.enablement.learnMore', + { + defaultMessage: 'Want to learn more?', + } +); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/synthetics_enablement/synthetics_enablement.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/synthetics_enablement/synthetics_enablement.tsx new file mode 100644 index 00000000000000..643764f0d3f97b --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/synthetics_enablement/synthetics_enablement.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useState, useEffect, useRef } from 'react'; +import { EuiEmptyPrompt, EuiButton, EuiTitle, EuiLink } from '@elastic/eui'; +import { useEnablement } from '../../../../hooks/use_enablement'; +import { kibanaService } from '../../../../../../utils/kibana_service'; +import * as labels from './labels'; + +export const EnablementEmptyState = () => { + const { error, enablement, enableSynthetics, loading } = useEnablement(); + const [shouldFocusEnablementButton, setShouldFocusEnablementButton] = useState(false); + const [isEnabling, setIsEnabling] = useState(false); + const { isEnabled, canEnable } = enablement; + const isEnabledRef = useRef(isEnabled); + const buttonRef = useRef(null); + + useEffect(() => { + if (!isEnabled && isEnabledRef.current === true) { + /* shift focus to enable button when enable toggle disappears. Prevent + * focus loss on the page */ + setShouldFocusEnablementButton(true); + } + isEnabledRef.current = Boolean(isEnabled); + }, [isEnabled]); + + useEffect(() => { + if (isEnabling && isEnabled) { + setIsEnabling(false); + kibanaService.toasts.addSuccess({ + title: labels.SYNTHETICS_ENABLE_SUCCESS, + toastLifeTimeMs: 3000, + }); + } else if (isEnabling && error) { + setIsEnabling(false); + kibanaService.toasts.addSuccess({ + title: labels.SYNTHETICS_DISABLE_SUCCESS, + toastLifeTimeMs: 3000, + }); + } + }, [isEnabled, isEnabling, error]); + + const handleEnableSynthetics = () => { + enableSynthetics(); + setIsEnabling(true); + }; + + useEffect(() => { + if (shouldFocusEnablementButton) { + buttonRef.current?.focus(); + } + }, [shouldFocusEnablementButton]); + + return !isEnabled && !loading ? ( + + {canEnable + ? labels.MONITOR_MANAGEMENT_ENABLEMENT_LABEL + : labels.MONITOR_MANAGEMENT_DISABLED_LABEL} + + } + body={ +

+ {canEnable + ? labels.MONITOR_MANAGEMENT_ENABLEMENT_MESSAGE + : labels.MONITOR_MANAGEMENT_DISABLED_MESSAGE} +

+ } + actions={ + canEnable ? ( + + {labels.MONITOR_MANAGEMENT_ENABLEMENT_BTN_LABEL} + + ) : null + } + footer={ + <> + +

{labels.LEARN_MORE_LABEL}

+
+ + {labels.DOCS_LABEL} + + + } + /> + ) : null; +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/monitor_page.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/monitor_page.tsx new file mode 100644 index 00000000000000..41d5c611e97e47 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/monitor_page.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { Redirect } from 'react-router-dom'; +import { EuiButton, EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui'; +import { useTrackPageview } from '@kbn/observability-plugin/public'; + +import { GETTING_STARTED_ROUTE } from '../../../../../common/constants'; + +import { useLocations } from '../../hooks/use_locations'; + +import { Loader } from './management/loader/loader'; +import { useEnablement } from '../../hooks/use_enablement'; + +import { EnablementEmptyState } from './management/synthetics_enablement/synthetics_enablement'; +import { MonitorListContainer } from './management/monitor_list_container'; +import { useMonitorListBreadcrumbs } from './hooks/use_breadcrumbs'; +import { useMonitorList } from './hooks/use_monitor_list'; +import * as labels from './management/labels'; + +export const MonitorPage: React.FC = () => { + useTrackPageview({ app: 'synthetics', path: 'monitors' }); + useTrackPageview({ app: 'synthetics', path: 'monitors', delay: 15000 }); + + useMonitorListBreadcrumbs(); + + const { syntheticsMonitors, loading: monitorsLoading, isDataQueried } = useMonitorList(); + + const { + error: enablementError, + enablement: { isEnabled, canEnable }, + loading: enablementLoading, + enableSynthetics, + } = useEnablement(); + + const { loading: locationsLoading } = useLocations(); + const showEmptyState = isEnabled !== undefined && syntheticsMonitors.length === 0; + + if (isEnabled && !monitorsLoading && syntheticsMonitors.length === 0 && isDataQueried) { + return ; + } + + return ( + <> + + {!isEnabled && syntheticsMonitors.length > 0 ? ( + <> + +

{labels.CALLOUT_MANAGEMENT_DESCRIPTION}

+ {canEnable ? ( + { + enableSynthetics(); + }} + > + {labels.SYNTHETICS_ENABLE_LABEL} + + ) : ( +

+ {labels.CALLOUT_MANAGEMENT_CONTACT_ADMIN}{' '} + + {labels.LEARN_MORE_LABEL} + +

+ )} +
+ + + ) : null} + +
+ {showEmptyState && } + + ); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/empty_state/empty_state_error.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/empty_state/empty_state_error.tsx similarity index 100% rename from x-pack/plugins/synthetics/public/apps/synthetics/components/overview/empty_state/empty_state_error.tsx rename to x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/empty_state/empty_state_error.tsx index 3f2150169e2dfd..f842518af6ec94 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/empty_state/empty_state_error.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/empty_state/empty_state_error.tsx @@ -5,9 +5,9 @@ * 2.0. */ +import React, { Fragment } from 'react'; import { EuiEmptyPrompt, EuiPanel, EuiTitle, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { Fragment } from 'react'; import { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public'; interface EmptyStateErrorProps { diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/empty_state/empty_state_loading.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/empty_state/empty_state_loading.tsx similarity index 100% rename from x-pack/plugins/synthetics/public/apps/synthetics/components/overview/empty_state/empty_state_loading.tsx rename to x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/empty_state/empty_state_loading.tsx index 0f71c9bafa962e..ca14e3751c9494 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/empty_state/empty_state_loading.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/empty_state/empty_state_loading.tsx @@ -5,9 +5,9 @@ * 2.0. */ +import React, { Fragment } from 'react'; import { EuiEmptyPrompt, EuiLoadingSpinner, EuiSpacer, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { Fragment } from 'react'; export const EmptyStateLoading = () => ( { diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview_page.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview_page.tsx new file mode 100644 index 00000000000000..134d8c024555e0 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/overview_page.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useTrackPageview } from '@kbn/observability-plugin/public'; +import { Redirect } from 'react-router-dom'; +import { useEnablement } from '../../../hooks'; + +import { MONITORS_ROUTE, GETTING_STARTED_ROUTE } from '../../../../../../common/constants'; + +import { useMonitorList } from '../hooks/use_monitor_list'; +import { useOverviewBreadcrumbs } from './use_breadcrumbs'; + +export const OverviewPage: React.FC = () => { + useTrackPageview({ app: 'synthetics', path: 'overview' }); + useTrackPageview({ app: 'synthetics', path: 'overview', delay: 15000 }); + useOverviewBreadcrumbs(); + + const { + enablement: { isEnabled }, + loading: enablementLoading, + } = useEnablement(); + + const { syntheticsMonitors, loading: monitorsLoading } = useMonitorList(); + + if (!enablementLoading && isEnabled && !monitorsLoading && syntheticsMonitors.length === 0) { + return ; + } else { + return ; + } +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/use_breadcrumbs.ts b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/use_breadcrumbs.ts similarity index 85% rename from x-pack/plugins/synthetics/public/apps/synthetics/components/overview/use_breadcrumbs.ts rename to x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/use_breadcrumbs.ts index d33a0fd3c20cc6..9f0ab7d740ec55 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/use_breadcrumbs.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/overview/use_breadcrumbs.ts @@ -6,8 +6,8 @@ */ import { i18n } from '@kbn/i18n'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { useBreadcrumbs } from '../../hooks/use_breadcrumbs'; -import { PLUGIN } from '../../../../../common/constants/plugin'; +import { useBreadcrumbs } from '../../../hooks/use_breadcrumbs'; +import { PLUGIN } from '../../../../../../common/constants/plugin'; export const useOverviewBreadcrumbs = () => { const kibana = useKibana(); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/overview_page.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/overview_page.tsx deleted file mode 100644 index 9e229308a402e3..00000000000000 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/overview/overview_page.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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; -import React, { useEffect } from 'react'; -import { useTrackPageview } from '@kbn/observability-plugin/public'; -import { useDispatch, useSelector } from 'react-redux'; -import { Redirect } from 'react-router-dom'; -import { monitorListSelector } from '../../state/monitor_management/selectors'; -import { GETTING_STARTED_ROUTE } from '../../../../../common/constants'; -import { fetchMonitorListAction } from '../../state/monitor_management/monitor_list'; -import { useSyntheticsSettingsContext } from '../../contexts'; -import { useOverviewBreadcrumbs } from './use_breadcrumbs'; - -export const OverviewPage: React.FC = () => { - useTrackPageview({ app: 'synthetics', path: 'overview' }); - useTrackPageview({ app: 'synthetics', path: 'overview', delay: 15000 }); - useOverviewBreadcrumbs(); - const { basePath } = useSyntheticsSettingsContext(); - - const dispatch = useDispatch(); - - const { total } = useSelector(monitorListSelector); - - useEffect(() => { - dispatch(fetchMonitorListAction.get()); - }, [dispatch]); - - if (total === 0) { - return ; - } - - return ( - - -

This page should show empty state or overview

-
- - Monitor Management - - - Add Monitor - -
- ); -}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/contexts/synthetics_data_view_context.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/contexts/synthetics_data_view_context.tsx index af657abd775408..4ddd22a23cbdb9 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/contexts/synthetics_data_view_context.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/contexts/synthetics_data_view_context.tsx @@ -8,7 +8,7 @@ import React, { createContext, useContext } from 'react'; import { useFetcher } from '@kbn/observability-plugin/public'; import { DataViewsPublicPluginStart, DataView } from '@kbn/data-views-plugin/public'; -import { useHasData } from '../components/overview/empty_state/use_has_data'; +import { useHasData } from '../components/monitors_page/overview/empty_state/use_has_data'; export const SyntheticsDataViewContext = createContext({} as DataView); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/index.ts index c3cde2eaffec5f..320c41b7ee1f88 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/index.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/index.ts @@ -9,3 +9,4 @@ export * from './use_url_params'; export * from './use_breadcrumbs'; export * from '../../../hooks/use_breakpoints'; export * from './use_service_allowed'; +export * from './use_enablement'; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_enablement.ts b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_enablement.ts new file mode 100644 index 00000000000000..74a430240b6160 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_enablement.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useCallback } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { + getSyntheticsEnablement, + enableSynthetics, + disableSynthetics, + selectSyntheticsEnablement, +} from '../state'; + +export function useEnablement() { + const dispatch = useDispatch(); + + const { loading, error, enablement } = useSelector(selectSyntheticsEnablement); + + useEffect(() => { + if (!enablement) { + dispatch(getSyntheticsEnablement()); + } + }, [dispatch, enablement]); + + return { + enablement: { + areApiKeysEnabled: enablement?.areApiKeysEnabled, + canEnable: enablement?.canEnable, + isEnabled: enablement?.isEnabled, + }, + error, + loading, + enableSynthetics: useCallback(() => dispatch(enableSynthetics()), [dispatch]), + disableSynthetics: useCallback(() => dispatch(disableSynthetics()), [dispatch]), + }; +} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_locations.ts b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_locations.ts new file mode 100644 index 00000000000000..67b2b9fc1b76bc --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/hooks/use_locations.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect } from 'react'; +import { useDispatch, useSelector } from 'react-redux'; +import { getServiceLocations, selectServiceLocationsState } from '../state'; + +export function useLocations() { + const dispatch = useDispatch(); + const { error, loading, locations, throttling } = useSelector(selectServiceLocationsState); + + useEffect(() => { + if (!locations.length) { + dispatch(getServiceLocations()); + } + }, [dispatch, locations]); + + return { + error, + loading, + locations, + throttling, + }; +} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx index 27f2599fbc102d..75f0604e0c2461 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/routes.tsx @@ -5,8 +5,10 @@ * 2.0. */ +import { EuiThemeComputed } from '@elastic/eui/src/services/theme/types'; import React, { FC, useEffect } from 'react'; -import { EuiPageTemplateProps, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { tint } from 'polished'; +import { EuiPageTemplateProps, EuiFlexGroup, EuiFlexItem, useEuiTheme } from '@elastic/eui'; import { Route, Switch } from 'react-router-dom'; import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; @@ -14,17 +16,18 @@ import { APP_WRAPPER_CLASS } from '@kbn/core/public'; import { useInspectorContext } from '@kbn/observability-plugin/public'; import { GettingStartedPage } from './components/getting_started/getting_started_page'; import { MonitorAddEditPage } from './components/monitor_add_edit/monitor_add_edit_page'; -import { OverviewPage } from './components/overview/overview_page'; +import { MonitorsPageHeader } from './components/monitors_page/management/page_header/monitors_page_header'; +import { OverviewPage } from './components/monitors_page/overview/overview_page'; import { SyntheticsPageTemplateComponent } from './components/common/pages/synthetics_page_template'; import { NotFoundPage } from './components/common/pages/not_found'; import { ServiceAllowedWrapper } from './components/common/wrappers/service_allowed_wrapper'; import { - GETTING_STARTED_ROUTE, MONITOR_ADD_ROUTE, - MONITOR_MANAGEMENT_ROUTE, + MONITORS_ROUTE, OVERVIEW_ROUTE, + GETTING_STARTED_ROUTE, } from '../../../common/constants'; -import { MonitorManagementPage } from './components/monitor_management/monitor_management_page'; +import { MonitorPage } from './components/monitors_page/monitor_page'; import { apiService } from '../../utils/api_service'; type RouteProps = { @@ -43,7 +46,14 @@ const baseTitle = i18n.translate('xpack.synthetics.routes.baseTitle', { defaultMessage: 'Synthetics - Kibana', }); -const getRoutes = (): RouteProps[] => { +export const MONITOR_MANAGEMENT_LABEL = i18n.translate( + 'xpack.synthetics.monitorManagement.heading', + { + defaultMessage: 'Monitor Management', + } +); + +const getRoutes = (euiTheme: EuiThemeComputed): RouteProps[] => { return [ { title: i18n.translate('xpack.synthetics.gettingStartedRoute.title', { @@ -88,26 +98,37 @@ const getRoutes = (): RouteProps[] => { defaultMessage: 'Monitor Management | {baseTitle}', values: { baseTitle }, }), - path: MONITOR_MANAGEMENT_ROUTE, + path: MONITORS_ROUTE, component: () => ( - - - + <> + + + + ), dataTestSubj: 'syntheticsMonitorManagementPage', + paddingSize: 'none', + pageBodyProps: { + style: { backgroundColor: tint(0.5, euiTheme.colors.body) }, + }, + pageContentProps: { + paddingSize: 'l', + style: { backgroundColor: euiTheme.colors.ghost }, + }, pageHeader: { - pageTitle: ( - - + paddingSize: 'l', + style: { margin: 0 }, + pageTitle: , + tabs: [ + { + label: ( - - - ), - rightSideItems: [ - /* */ + ), + isSelected: true, + }, ], }, }, @@ -145,8 +166,9 @@ const RouteInit: React.FC> = ({ path, title } }; export const PageRouter: FC = () => { - const routes = getRoutes(); const { addInspectorRequest } = useInspectorContext(); + const { euiTheme } = useEuiTheme(); + const routes = getRoutes(euiTheme); apiService.addInspectorRequest = addInspectorRequest; @@ -160,7 +182,7 @@ export const PageRouter: FC = () => { dataTestSubj, pageHeader, ...pageTemplateProps - }) => ( + }: RouteProps) => (
diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/index.ts index 5bc9517d6aa118..af377f27387a3a 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/index.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/index.ts @@ -8,6 +8,10 @@ export { store, storage } from './store'; export type { SyntheticsAppState as AppState } from './root_reducer'; +export type { IHttpSerializedFetchError } from './utils/http_error'; export * from './ui'; export * from './index_status'; +export * from './synthetics_enablement'; +export * from './service_locations'; +export * from './monitor_list'; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/actions.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/actions.ts new file mode 100644 index 00000000000000..b9969cca2afc62 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/actions.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { MonitorManagementListResult } from '../../../../../common/runtime_types'; +import { createAsyncAction } from '../utils/actions'; + +import { MonitorListPageState } from './models'; + +export const fetchMonitorListAction = createAsyncAction< + MonitorListPageState, + MonitorManagementListResult +>('fetchMonitorListAction'); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/api.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/api.ts similarity index 56% rename from x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/api.ts rename to x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/api.ts index 777e72069f6f2b..b500a3d8d86881 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/api.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/api.ts @@ -5,53 +5,66 @@ * 2.0. */ -import { ServiceLocationsState } from './service_locations'; -import { apiService } from '../../../../utils/api_service'; +import { API_URLS } from '../../../../../common/constants'; import { EncryptedSyntheticsMonitor, FetchMonitorManagementListQueryArgs, MonitorManagementListResult, MonitorManagementListResultCodec, ServiceLocationErrors, - ServiceLocationsApiResponseCodec, SyntheticsMonitor, - SyntheticsMonitorWithId, } from '../../../../../common/runtime_types'; -import { API_URLS } from '../../../../../common/constants'; - -export const createMonitorAPI = async ({ - monitor, -}: { - monitor: SyntheticsMonitor | EncryptedSyntheticsMonitor; -}): Promise<{ attributes: { errors: ServiceLocationErrors } } | SyntheticsMonitor> => { - return await apiService.post(API_URLS.SYNTHETICS_MONITORS, monitor); -}; +import { apiService } from '../../../../utils/api_service'; -export const updateMonitorAPI = async ({ - monitor, - id, -}: { - monitor: SyntheticsMonitor | EncryptedSyntheticsMonitor; - id: string; -}): Promise<{ attributes: { errors: ServiceLocationErrors } } | SyntheticsMonitorWithId> => { - return await apiService.put(`${API_URLS.SYNTHETICS_MONITORS}/${id}`, monitor); -}; +import { MonitorListPageState } from './models'; -export const fetchServiceLocations = async (): Promise => { - const { throttling, locations } = await apiService.get( - API_URLS.SERVICE_LOCATIONS, - undefined, - ServiceLocationsApiResponseCodec - ); - return { throttling, locations }; -}; +function toMonitorManagementListQueryArgs( + pageState: MonitorListPageState +): FetchMonitorManagementListQueryArgs { + return { + perPage: pageState.pageSize, + page: pageState.pageIndex + 1, + sortOrder: pageState.sortOrder, + sortField: pageState.sortField, + search: '', + searchFields: [], + }; +} export const fetchMonitorManagementList = async ( - params: FetchMonitorManagementListQueryArgs + pageState: MonitorListPageState ): Promise => { + const params = toMonitorManagementListQueryArgs(pageState); + return await apiService.get( API_URLS.SYNTHETICS_MONITORS, params, MonitorManagementListResultCodec ); }; + +export const fetchDeleteMonitor = async ({ id }: { id: string }): Promise => { + return await apiService.delete(`${API_URLS.SYNTHETICS_MONITORS}/${id}`); +}; + +export const fetchUpsertMonitor = async ({ + monitor, + id, +}: { + monitor: SyntheticsMonitor | EncryptedSyntheticsMonitor; + id?: string; +}): Promise<{ attributes: { errors: ServiceLocationErrors } } | SyntheticsMonitor> => { + if (id) { + return await apiService.put(`${API_URLS.SYNTHETICS_MONITORS}/${id}`, monitor); + } else { + return await apiService.post(API_URLS.SYNTHETICS_MONITORS, monitor); + } +}; + +export const fetchCreateMonitor = async ({ + monitor, +}: { + monitor: SyntheticsMonitor | EncryptedSyntheticsMonitor; +}): Promise<{ attributes: { errors: ServiceLocationErrors } } | SyntheticsMonitor> => { + return await apiService.post(API_URLS.SYNTHETICS_MONITORS, monitor); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/effects.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/effects.ts similarity index 53% rename from x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/effects.ts rename to x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/effects.ts index 924fb8baf1da00..e155250eec19b8 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/effects.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/effects.ts @@ -6,25 +6,13 @@ */ import { takeLeading } from 'redux-saga/effects'; -import { fetchMonitorListAction } from './monitor_list'; -import { fetchMonitorManagementList, fetchServiceLocations } from './api'; import { fetchEffectFactory } from '../utils/fetch_effect'; -import { fetchServiceLocationsAction } from './service_locations'; - -export function* fetchServiceLocationsEffect() { - yield takeLeading( - String(fetchServiceLocationsAction.get), - fetchEffectFactory( - fetchServiceLocations, - fetchServiceLocationsAction.success, - fetchServiceLocationsAction.fail - ) - ); -} +import { fetchMonitorListAction } from './actions'; +import { fetchMonitorManagementList } from './api'; export function* fetchMonitorListEffect() { yield takeLeading( - String(fetchMonitorListAction.get), + fetchMonitorListAction.get, fetchEffectFactory( fetchMonitorManagementList, fetchMonitorListAction.success, diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/index.ts new file mode 100644 index 00000000000000..fbe152f290aa7b --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/index.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createReducer } from '@reduxjs/toolkit'; + +import { ConfigKey, MonitorManagementListResult } from '../../../../../common/runtime_types'; + +import { IHttpSerializedFetchError, serializeHttpFetchError } from '../utils/http_error'; + +import { MonitorListPageState } from './models'; +import { fetchMonitorListAction } from './actions'; + +export interface MonitorListState { + data: MonitorManagementListResult; + pageState: MonitorListPageState; + loading: boolean; + error: IHttpSerializedFetchError | null; +} + +const initialState: MonitorListState = { + data: { page: 1, perPage: 10, total: null, monitors: [], syncErrors: [] }, + pageState: { + pageIndex: 0, + pageSize: 10, + sortOrder: 'asc', + sortField: `${ConfigKey.NAME}.keyword`, + }, + loading: false, + error: null, +}; + +export const monitorListReducer = createReducer(initialState, (builder) => { + builder + .addCase(fetchMonitorListAction.get, (state, action) => { + state.pageState = action.payload; + state.loading = true; + }) + .addCase(fetchMonitorListAction.success, (state, action) => { + state.loading = false; + state.data = action.payload; + }) + .addCase(fetchMonitorListAction.fail, (state, action) => { + state.loading = false; + state.error = serializeHttpFetchError(action.payload); + }); +}); + +export * from './models'; +export * from './actions'; +export * from './effects'; +export * from './selectors'; +export { fetchDeleteMonitor, fetchUpsertMonitor, fetchCreateMonitor } from './api'; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/models.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/models.ts new file mode 100644 index 00000000000000..bfc4272b04a674 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/models.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EncryptedSyntheticsSavedMonitor, + FetchMonitorManagementListQueryArgs, +} from '../../../../../common/runtime_types'; + +export type MonitorListSortField = `${keyof EncryptedSyntheticsSavedMonitor}.keyword`; + +export interface MonitorListPageState { + pageIndex: number; + pageSize: number; + sortField: MonitorListSortField; + sortOrder: NonNullable; +} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/selectors.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/selectors.ts new file mode 100644 index 00000000000000..6d92e75977cf65 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_list/selectors.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createSelector } from 'reselect'; + +import { EncryptedSyntheticsSavedMonitor } from '../../../../../common/runtime_types'; +import { SyntheticsAppState } from '../root_reducer'; + +export const selectMonitorListState = (state: SyntheticsAppState) => state.monitorList; +export const selectEncryptedSyntheticsSavedMonitors = createSelector( + selectMonitorListState, + (state) => + state.data.monitors.map((monitor) => ({ + ...monitor.attributes, + id: monitor.id, + updated_at: monitor.updated_at, + })) as EncryptedSyntheticsSavedMonitor[] +); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/monitor_list.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/monitor_list.ts deleted file mode 100644 index 2493f9eb173d8a..00000000000000 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/monitor_list.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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { createReducer } from '@reduxjs/toolkit'; -import { IHttpFetchError } from '@kbn/core/public'; -import { createAsyncAction, Nullable } from '../utils/actions'; -import { MonitorManagementListResult } from '../../../../../common/runtime_types'; - -export const fetchMonitorListAction = createAsyncAction( - 'fetchMonitorListAction' -); - -export const monitorListReducer = createReducer( - { - data: {} as MonitorManagementListResult, - loading: false, - error: null as Nullable, - }, - (builder) => { - builder - .addCase(fetchMonitorListAction.get, (state, action) => { - state.loading = true; - }) - .addCase(fetchMonitorListAction.success, (state, action) => { - state.loading = false; - state.data = action.payload; - }) - .addCase(fetchMonitorListAction.fail, (state, action) => { - state.loading = false; - state.error = action.payload; - }); - } -); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/service_locations.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/service_locations.ts deleted file mode 100644 index 572d00ce3892fa..00000000000000 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/service_locations.ts +++ /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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { createReducer, PayloadAction } from '@reduxjs/toolkit'; -import { IHttpFetchError } from '@kbn/core/public'; -import { createAsyncAction, Nullable } from '../utils/actions'; -import { ServiceLocations, ThrottlingOptions } from '../../../../../common/runtime_types'; - -export const fetchServiceLocationsAction = createAsyncAction( - 'fetchServiceLocationsAction' -); - -export interface ServiceLocationsState { - throttling: ThrottlingOptions | undefined; - locations: ServiceLocations; -} - -export const serviceLocationReducer = createReducer( - { - locations: [] as ServiceLocations, - loading: false, - error: null as Nullable, - }, - (builder) => { - builder - .addCase(fetchServiceLocationsAction.get, (state, action) => { - state.loading = true; - }) - .addCase( - fetchServiceLocationsAction.success, - (state, action: PayloadAction) => { - state.loading = false; - state.locations = action.payload.locations; - } - ) - .addCase(fetchServiceLocationsAction.fail, (state, action) => { - state.loading = false; - state.error = action.payload; - }); - } -); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/root_effect.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/root_effect.ts index 9a66b4e6b9e742..78d26f231fca1b 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/root_effect.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/root_effect.ts @@ -6,12 +6,15 @@ */ import { all, fork } from 'redux-saga/effects'; -import { fetchMonitorListEffect, fetchServiceLocationsEffect } from './monitor_management/effects'; import { fetchIndexStatusEffect } from './index_status'; +import { fetchSyntheticsEnablementEffect } from './synthetics_enablement'; +import { fetchMonitorListEffect } from './monitor_list'; +import { fetchServiceLocationsEffect } from './service_locations'; export const rootEffect = function* root(): Generator { yield all([ fork(fetchIndexStatusEffect), + fork(fetchSyntheticsEnablementEffect), fork(fetchServiceLocationsEffect), fork(fetchMonitorListEffect), ]); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/root_reducer.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/root_reducer.ts index 1c8ed190fd80eb..e358b185fbeb3a 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/root_reducer.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/root_reducer.ts @@ -7,16 +7,18 @@ import { combineReducers } from '@reduxjs/toolkit'; -import { monitorListReducer } from './monitor_management/monitor_list'; -import { serviceLocationReducer } from './monitor_management/service_locations'; import { uiReducer } from './ui'; import { indexStatusReducer } from './index_status'; +import { syntheticsEnablementReducer } from './synthetics_enablement'; +import { monitorListReducer } from './monitor_list'; +import { serviceLocationsReducer } from './service_locations'; export const rootReducer = combineReducers({ ui: uiReducer, indexStatus: indexStatusReducer, - serviceLocations: serviceLocationReducer, + syntheticsEnablement: syntheticsEnablementReducer, monitorList: monitorListReducer, + serviceLocations: serviceLocationsReducer, }); export type SyntheticsAppState = ReturnType; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/service_locations/actions.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/service_locations/actions.ts new file mode 100644 index 00000000000000..794e16d0292c5b --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/service_locations/actions.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createAction } from '@reduxjs/toolkit'; +import { ServiceLocations, ThrottlingOptions } from '../../../../../common/runtime_types'; + +export const getServiceLocations = createAction('[SERVICE LOCATIONS] GET'); +export const getServiceLocationsSuccess = createAction<{ + throttling: ThrottlingOptions | undefined; + locations: ServiceLocations; +}>('[SERVICE LOCATIONS] GET SUCCESS'); +export const getServiceLocationsFailure = createAction('[SERVICE LOCATIONS] GET FAILURE'); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/service_locations/api.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/service_locations/api.ts new file mode 100644 index 00000000000000..3435c06f6cf8e6 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/service_locations/api.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { API_URLS } from '../../../../../common/constants'; +import { + ServiceLocations, + ServiceLocationsApiResponseCodec, + ThrottlingOptions, +} from '../../../../../common/runtime_types'; +import { apiService } from '../../../../utils/api_service'; + +export const fetchServiceLocations = async (): Promise<{ + throttling: ThrottlingOptions | undefined; + locations: ServiceLocations; +}> => { + const { throttling, locations } = await apiService.get( + API_URLS.SERVICE_LOCATIONS, + undefined, + ServiceLocationsApiResponseCodec + ); + return { throttling, locations }; +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/service_locations/effects.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/service_locations/effects.ts new file mode 100644 index 00000000000000..e72f173af6c860 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/service_locations/effects.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { takeLeading } from 'redux-saga/effects'; +import { + getServiceLocations, + getServiceLocationsFailure, + getServiceLocationsSuccess, +} from './actions'; +import { fetchServiceLocations } from './api'; +import { fetchEffectFactory } from '../utils/fetch_effect'; + +export function* fetchServiceLocationsEffect() { + yield takeLeading( + getServiceLocations, + fetchEffectFactory( + fetchServiceLocations, + getServiceLocationsSuccess, + getServiceLocationsFailure + ) + ); +} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/service_locations/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/service_locations/index.ts new file mode 100644 index 00000000000000..98676baf421a91 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/service_locations/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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createReducer } from '@reduxjs/toolkit'; +import { + DEFAULT_THROTTLING, + ServiceLocations, + ThrottlingOptions, +} from '../../../../../common/runtime_types'; + +import { + getServiceLocations, + getServiceLocationsSuccess, + getServiceLocationsFailure, +} from './actions'; + +export interface ServiceLocationsState { + locations: ServiceLocations; + throttling: ThrottlingOptions | null; + loading: boolean; + error: Error | null; +} + +const initialState: ServiceLocationsState = { + locations: [], + throttling: DEFAULT_THROTTLING, + loading: false, + error: null, +}; + +export const serviceLocationsReducer = createReducer(initialState, (builder) => { + builder + .addCase(getServiceLocations, (state) => { + state.loading = true; + }) + .addCase(getServiceLocationsSuccess, (state, action) => { + state.loading = false; + state.error = null; + state.locations = action.payload.locations; + state.throttling = action.payload.throttling || DEFAULT_THROTTLING; + }) + .addCase(getServiceLocationsFailure, (state, action) => { + state.loading = false; + state.error = action.payload; + }); +}); + +export * from './actions'; +export * from './effects'; +export * from './selectors'; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/service_locations/selectors.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/service_locations/selectors.ts new file mode 100644 index 00000000000000..3ced345c6259e1 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/service_locations/selectors.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createSelector } from 'reselect'; +import type { SyntheticsAppState } from '../root_reducer'; + +const getState = (appState: SyntheticsAppState) => appState.serviceLocations; +export const selectServiceLocationsState = createSelector(getState, (state) => state); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/actions.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/actions.ts new file mode 100644 index 00000000000000..c38fadc0952a65 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/actions.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createAction } from '@reduxjs/toolkit'; +import { MonitorManagementEnablementResult } from '../../../../../common/runtime_types'; + +export const getSyntheticsEnablement = createAction('[SYNTHETICS_ENABLEMENT] GET'); +export const getSyntheticsEnablementSuccess = createAction( + '[SYNTHETICS_ENABLEMENT] GET SUCCESS' +); +export const getSyntheticsEnablementFailure = createAction( + '[SYNTHETICS_ENABLEMENT] GET FAILURE' +); + +export const disableSynthetics = createAction('[SYNTHETICS_ENABLEMENT] DISABLE'); +export const disableSyntheticsSuccess = createAction<{}>('[SYNTHETICS_ENABLEMENT] DISABLE SUCCESS'); +export const disableSyntheticsFailure = createAction( + '[SYNTHETICS_ENABLEMENT] DISABLE FAILURE' +); + +export const enableSynthetics = createAction('[SYNTHETICS_ENABLEMENT] ENABLE'); +export const enableSyntheticsSuccess = createAction<{}>('[SYNTHETICS_ENABLEMENT] ENABLE SUCCESS'); +export const enableSyntheticsFailure = createAction( + '[SYNTHETICS_ENABLEMENT] ENABLE FAILURE' +); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/api.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/api.ts new file mode 100644 index 00000000000000..4593f241b41f5b --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/api.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { API_URLS } from '../../../../../common/constants'; +import { + MonitorManagementEnablementResult, + MonitorManagementEnablementResultCodec, +} from '../../../../../common/runtime_types'; +import { apiService } from '../../../../utils/api_service'; + +export const fetchGetSyntheticsEnablement = + async (): Promise => { + return await apiService.get( + API_URLS.SYNTHETICS_ENABLEMENT, + undefined, + MonitorManagementEnablementResultCodec + ); + }; + +export const fetchDisableSynthetics = async (): Promise<{}> => { + return await apiService.delete(API_URLS.SYNTHETICS_ENABLEMENT); +}; + +export const fetchEnableSynthetics = async (): Promise<{}> => { + return await apiService.post(API_URLS.SYNTHETICS_ENABLEMENT); +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/effects.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/effects.ts new file mode 100644 index 00000000000000..d3134c60f8fd37 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/effects.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { takeLatest, takeLeading } from 'redux-saga/effects'; +import { + getSyntheticsEnablement, + getSyntheticsEnablementSuccess, + getSyntheticsEnablementFailure, + disableSynthetics, + disableSyntheticsSuccess, + disableSyntheticsFailure, + enableSynthetics, + enableSyntheticsSuccess, + enableSyntheticsFailure, +} from './actions'; +import { fetchGetSyntheticsEnablement, fetchDisableSynthetics, fetchEnableSynthetics } from './api'; +import { fetchEffectFactory } from '../utils/fetch_effect'; + +export function* fetchSyntheticsEnablementEffect() { + yield takeLeading( + getSyntheticsEnablement, + fetchEffectFactory( + fetchGetSyntheticsEnablement, + getSyntheticsEnablementSuccess, + getSyntheticsEnablementFailure + ) + ); + yield takeLatest( + disableSynthetics, + fetchEffectFactory(fetchDisableSynthetics, disableSyntheticsSuccess, disableSyntheticsFailure) + ); + yield takeLatest( + enableSynthetics, + fetchEffectFactory(fetchEnableSynthetics, enableSyntheticsSuccess, enableSyntheticsFailure) + ); +} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/index.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/index.ts new file mode 100644 index 00000000000000..62ed85ad17e86f --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/index.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createReducer } from '@reduxjs/toolkit'; +import { + getSyntheticsEnablement, + getSyntheticsEnablementSuccess, + disableSynthetics, + disableSyntheticsSuccess, + disableSyntheticsFailure, + enableSynthetics, + enableSyntheticsSuccess, + enableSyntheticsFailure, + getSyntheticsEnablementFailure, +} from './actions'; +import { MonitorManagementEnablementResult } from '../../../../../common/runtime_types'; + +export interface SyntheticsEnablementState { + loading: boolean; + error: Error | null; + enablement: MonitorManagementEnablementResult | null; +} + +export const initialState: SyntheticsEnablementState = { + loading: false, + error: null, + enablement: null, +}; + +export const syntheticsEnablementReducer = createReducer(initialState, (builder) => { + builder + .addCase(getSyntheticsEnablement, (state) => { + state.loading = true; + }) + .addCase(getSyntheticsEnablementSuccess, (state, action) => { + state.loading = false; + state.error = null; + state.enablement = action.payload; + }) + .addCase(getSyntheticsEnablementFailure, (state, action) => { + state.loading = false; + state.error = action.payload; + }) + + .addCase(disableSynthetics, (state) => { + state.loading = true; + }) + .addCase(disableSyntheticsSuccess, (state, action) => { + state.loading = false; + state.error = null; + state.enablement = { + canEnable: state.enablement?.canEnable ?? false, + areApiKeysEnabled: state.enablement?.areApiKeysEnabled ?? false, + canManageApiKeys: state.enablement?.canManageApiKeys ?? false, + isEnabled: false, + }; + }) + .addCase(disableSyntheticsFailure, (state, action) => { + state.loading = false; + state.error = action.payload; + }) + + .addCase(enableSynthetics, (state) => { + state.loading = true; + }) + .addCase(enableSyntheticsSuccess, (state, action) => { + state.loading = false; + state.error = null; + state.enablement = { + canEnable: state.enablement?.canEnable ?? false, + areApiKeysEnabled: state.enablement?.areApiKeysEnabled ?? false, + canManageApiKeys: state.enablement?.canManageApiKeys ?? false, + isEnabled: true, + }; + }) + .addCase(enableSyntheticsFailure, (state, action) => { + state.loading = false; + state.error = action.payload; + }); +}); + +export * from './actions'; +export * from './effects'; +export * from './selectors'; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/selectors.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/selectors.ts new file mode 100644 index 00000000000000..fd69d44871637e --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/synthetics_enablement/selectors.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createSelector } from 'reselect'; +import type { SyntheticsAppState } from '../root_reducer'; + +const getState = (appState: SyntheticsAppState) => appState.syntheticsEnablement; +export const selectSyntheticsEnablement = createSelector(getState, (state) => state); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/utils/http_error.ts b/x-pack/plugins/synthetics/public/apps/synthetics/state/utils/http_error.ts new file mode 100644 index 00000000000000..b34402556ed911 --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/state/utils/http_error.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IHttpFetchError } from '@kbn/core/public'; + +export interface IHttpSerializedFetchError { + name: string; + body: { + error?: string; + message?: string; + statusCode?: number; + }; + requestUrl: string; +} + +export const serializeHttpFetchError = (error: IHttpFetchError): IHttpSerializedFetchError => { + const body = error.body as { error: string; message: string; statusCode: number }; + return { + name: error.name, + body: { + error: body!.error, + message: body!.message, + statusCode: body!.statusCode, + }, + requestUrl: error.request.url, + }; +}; diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/syncthetics_store.mock.ts b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/syncthetics_store.mock.ts index a045c7a7f7bede..72421609019641 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/syncthetics_store.mock.ts +++ b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/__mocks__/syncthetics_store.mock.ts @@ -6,7 +6,11 @@ */ import { SyntheticsAppState } from '../../../state/root_reducer'; -import { LocationStatus } from '../../../../../../common/runtime_types'; +import { + ConfigKey, + DEFAULT_THROTTLING, + LocationStatus, +} from '../../../../../../common/runtime_types'; /** * NOTE: This variable name MUST start with 'mock*' in order for @@ -27,6 +31,7 @@ export const mockState: SyntheticsAppState = { loading: false, }, serviceLocations: { + throttling: DEFAULT_THROTTLING, locations: [ { id: 'us_central', @@ -55,6 +60,12 @@ export const mockState: SyntheticsAppState = { error: null, }, monitorList: { + pageState: { + pageIndex: 0, + pageSize: 10, + sortOrder: 'asc', + sortField: `${ConfigKey.NAME}.keyword`, + }, data: { total: 0, monitors: [], @@ -65,6 +76,5 @@ export const mockState: SyntheticsAppState = { error: null, loading: false, }, + syntheticsEnablement: { loading: false, error: null, enablement: null }, }; - -// TODO: Complete mock state diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/spy_use_fetcher.ts b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/spy_use_fetcher.ts new file mode 100644 index 00000000000000..47d52a73e6850d --- /dev/null +++ b/x-pack/plugins/synthetics/public/apps/synthetics/utils/testing/spy_use_fetcher.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as observabilityPublic from '@kbn/observability-plugin/public'; + +jest.mock('@kbn/observability-plugin/public', () => { + const originalModule = jest.requireActual('@kbn/observability-plugin/public'); + + return { + ...originalModule, + useFetcher: jest.fn().mockReturnValue({ + data: null, + status: 'success', + }), + useTrackPageview: jest.fn(), + }; +}); + +export function spyOnUseFetcher( + payload: unknown, + status = observabilityPublic.FETCH_STATUS.SUCCESS +) { + return jest.spyOn(observabilityPublic, 'useFetcher').mockReturnValue({ + status, + data: payload, + refetch: () => null, + }); +} diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/selectors.ts b/x-pack/plugins/synthetics/public/hooks/use_capabilities.ts similarity index 50% rename from x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/selectors.ts rename to x-pack/plugins/synthetics/public/hooks/use_capabilities.ts index 5c7dc8360ec9ff..5cde14df84f0e4 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/state/monitor_management/selectors.ts +++ b/x-pack/plugins/synthetics/public/hooks/use_capabilities.ts @@ -4,9 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { SyntheticsAppState } from '../root_reducer'; -export const monitorListSelector = (state: SyntheticsAppState) => state.monitorList.data; +import { useKibana } from '@kbn/kibana-react-plugin/public'; -export const serviceLocationsSelector = (state: SyntheticsAppState) => - state.serviceLocations.locations; +export const useCanEditSynthetics = () => { + return !!useKibana().services?.application?.capabilities.uptime.save; +}; diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/action_bar/action_bar.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/action_bar/action_bar.tsx index 5672c96314dc98..af09f6965083bf 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/action_bar/action_bar.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/action_bar/action_bar.tsx @@ -21,17 +21,22 @@ import { useSelector } from 'react-redux'; import { FETCH_STATUS, useFetcher } from '@kbn/observability-plugin/public'; import { euiStyled } from '@kbn/kibana-react-plugin/common'; +import { showSyncErrors } from '../../../../apps/synthetics/components/monitors_page/management/show_sync_errors'; import { MONITOR_MANAGEMENT_ROUTE } from '../../../../../common/constants'; import { UptimeSettingsContext } from '../../../contexts'; import { setMonitor } from '../../../state/api'; -import { ConfigKey, SyntheticsMonitor, SourceType } from '../../../../../common/runtime_types'; +import { + ConfigKey, + SyntheticsMonitor, + SourceType, + ServiceLocationErrors, +} from '../../../../../common/runtime_types'; import { TestRun } from '../test_now_mode/test_now_mode'; import { monitorManagementListSelector } from '../../../state/selectors'; import { kibanaService } from '../../../state/kibana_service'; -import { showSyncErrors } from '../../../../apps/synthetics/components/monitor_management/show_sync_errors'; export interface ActionBarProps { monitor: SyntheticsMonitor; @@ -104,7 +109,11 @@ export const ActionBar = ({ }); setIsSuccessful(true); } else if (hasErrors && !loading) { - showSyncErrors(data.attributes.errors, locations, kibanaService.toasts); + showSyncErrors( + (data as { attributes: { errors: ServiceLocationErrors } })?.attributes.errors ?? [], + locations, + kibanaService.toasts + ); setIsSuccessful(true); } }, [data, status, isSaving, isValid, monitorId, hasErrors, locations, loading]); diff --git a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_list/monitor_list_container.tsx b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_list/monitor_list_container.tsx index 727f4f6dee72b2..6db399175aaa60 100644 --- a/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_list/monitor_list_container.tsx +++ b/x-pack/plugins/synthetics/public/legacy_uptime/components/monitor_management/monitor_list/monitor_list_container.tsx @@ -39,8 +39,8 @@ export const MonitorListContainer = ({ dispatchPageAction({ type: 'refresh' }); }, [dispatchPageAction]); - useTrackPageview({ app: 'uptime', path: 'manage-monitors' }); - useTrackPageview({ app: 'uptime', path: 'manage-monitors', delay: 15000 }); + useTrackPageview({ app: 'uptime', path: 'monitors' }); + useTrackPageview({ app: 'uptime', path: 'monitors', delay: 15000 }); const monitorList = useSelector(monitorManagementListSelector); diff --git a/x-pack/plugins/synthetics/public/plugin.ts b/x-pack/plugins/synthetics/public/plugin.ts index 127655f95e8a6e..826beb2cbfe68f 100644 --- a/x-pack/plugins/synthetics/public/plugin.ts +++ b/x-pack/plugins/synthetics/public/plugin.ts @@ -41,6 +41,7 @@ import { CasesUiStart } from '@kbn/cases-plugin/public'; import { CloudSetup } from '@kbn/cloud-plugin/public'; import { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import { PLUGIN } from '../common/constants/plugin'; +import { MONITORS_ROUTE } from '../common/constants/ui'; import { LazySyntheticsPolicyCreateExtension, LazySyntheticsPolicyEditExtension, @@ -262,8 +263,8 @@ function registerSyntheticsRoutesWithNavigation( defaultMessage: 'Monitors', }), app: 'synthetics', - path: '/manage-monitors', - matchFullPath: false, + path: MONITORS_ROUTE, + matchFullPath: true, ignoreTrailingSlash: true, }, ], diff --git a/x-pack/test/api_integration/apis/metrics_ui/inventory_threshold_alert.ts b/x-pack/test/api_integration/apis/metrics_ui/inventory_threshold_alert.ts index aa0c1af92f9516..230ae0e134f43c 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/inventory_threshold_alert.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/inventory_threshold_alert.ts @@ -172,6 +172,46 @@ export default function ({ getService }: FtrProviderContext) { }, }); }); + it('should work with a long threshold', async () => { + const results = await evaluateCondition({ + ...baseOptions, + condition: { + ...baseCondition, + metric: 'rx', + threshold: [107374182400], + comparator: Comparator.LT, + }, + esClient, + }); + expect(results).to.eql({ + 'host-0': { + metric: 'rx', + timeSize: 1, + timeUnit: 'm', + sourceId: 'default', + threshold: [107374182400], + comparator: '<', + shouldFire: true, + shouldWarn: false, + isNoData: false, + isError: false, + currentValue: 1666.6666666666667, + }, + 'host-1': { + metric: 'rx', + timeSize: 1, + timeUnit: 'm', + sourceId: 'default', + threshold: [107374182400], + comparator: '<', + shouldFire: true, + shouldWarn: false, + isNoData: false, + isError: false, + currentValue: 2000, + }, + }); + }); it('should work FOR LAST 5 minute', async () => { const options = { ...baseOptions, diff --git a/x-pack/test/api_integration/apis/metrics_ui/metric_threshold_alert.ts b/x-pack/test/api_integration/apis/metrics_ui/metric_threshold_alert.ts index 64b2b5211a1bf9..5d6f38b368f2e1 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/metric_threshold_alert.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/metric_threshold_alert.ts @@ -768,6 +768,49 @@ export default function ({ getService }: FtrProviderContext) { describe('with rate data', () => { before(() => esArchiver.load('x-pack/test/functional/es_archives/infra/alerts_test_data')); after(() => esArchiver.unload('x-pack/test/functional/es_archives/infra/alerts_test_data')); + it('should alert on rate with long threshold', async () => { + const params = { + ...baseParams, + criteria: [ + { + timeSize: 1, + timeUnit: 'm', + threshold: [107374182400], + comparator: Comparator.LT_OR_EQ, + aggType: Aggregators.RATE, + metric: 'value', + } as NonCountMetricExpressionParams, + ], + }; + const timeFrame = { end: rate.max }; + const results = await evaluateRule( + esClient, + params, + configuration, + 10000, + true, + logger, + void 0, + timeFrame + ); + expect(results).to.eql([ + { + '*': { + timeSize: 1, + timeUnit: 'm', + threshold: [107374182400], + comparator: '<=', + aggType: 'rate', + metric: 'value', + currentValue: 0.6666666666666666, + timestamp: '2021-01-02T00:05:00.000Z', + shouldFire: true, + shouldWarn: false, + isNoData: false, + }, + }, + ]); + }); describe('without groupBy', () => { it('should alert on rate', async () => { const params = { diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/common/alerts/get_cases.ts b/x-pack/test/cases_api_integration/spaces_only/tests/common/alerts/get_cases.ts index 739f8e5ec08926..691d132aa1edec 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/common/alerts/get_cases.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/common/alerts/get_cases.ts @@ -20,7 +20,7 @@ import { validateCasesFromAlertIDResponse } from '../../../../common/lib/validat // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); const authSpace1 = getAuthWithSuperUser(); const authSpace2 = getAuthWithSuperUser('space2'); @@ -32,26 +32,26 @@ export default ({ getService }: FtrProviderContext): void => { it('should return all cases with the same alert ID attached to them in space1', async () => { const [case1, case2, case3] = await Promise.all([ - createCase(supertest, getPostCaseRequest(), 200, authSpace1), - createCase(supertest, getPostCaseRequest(), 200, authSpace1), - createCase(supertest, getPostCaseRequest(), 200, authSpace1), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, authSpace1), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, authSpace1), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, authSpace1), ]); await Promise.all([ createComment({ - supertest, + supertest: supertestWithoutAuth, caseId: case1.id, params: postCommentAlertReq, auth: authSpace1, }), createComment({ - supertest, + supertest: supertestWithoutAuth, caseId: case2.id, params: postCommentAlertReq, auth: authSpace1, }), createComment({ - supertest, + supertest: supertestWithoutAuth, caseId: case3.id, params: postCommentAlertReq, auth: authSpace1, @@ -59,7 +59,7 @@ export default ({ getService }: FtrProviderContext): void => { ]); const cases = await getCasesByAlert({ - supertest, + supertest: supertestWithoutAuth, alertID: 'test-id', auth: authSpace1, }); @@ -70,26 +70,26 @@ export default ({ getService }: FtrProviderContext): void => { it('should return 1 case in space2 when 2 cases were created in space1 and 1 in space2', async () => { const [case1, case2, case3] = await Promise.all([ - createCase(supertest, getPostCaseRequest(), 200, authSpace1), - createCase(supertest, getPostCaseRequest(), 200, authSpace1), - createCase(supertest, getPostCaseRequest(), 200, authSpace2), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, authSpace1), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, authSpace1), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, authSpace2), ]); await Promise.all([ createComment({ - supertest, + supertest: supertestWithoutAuth, caseId: case1.id, params: postCommentAlertReq, auth: authSpace1, }), createComment({ - supertest, + supertest: supertestWithoutAuth, caseId: case2.id, params: postCommentAlertReq, auth: authSpace1, }), createComment({ - supertest, + supertest: supertestWithoutAuth, caseId: case3.id, params: postCommentAlertReq, auth: authSpace2, @@ -97,7 +97,7 @@ export default ({ getService }: FtrProviderContext): void => { ]); const casesByAlert = await getCasesByAlert({ - supertest, + supertest: supertestWithoutAuth, alertID: 'test-id', auth: authSpace2, }); diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/common/cases/delete_cases.ts b/x-pack/test/cases_api_integration/spaces_only/tests/common/cases/delete_cases.ts index 9de57a1b7abe24..d94cea06e57130 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/common/cases/delete_cases.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/common/cases/delete_cases.ts @@ -19,7 +19,7 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); const authSpace1 = getAuthWithSuperUser(); @@ -29,24 +29,47 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should delete a case in space1', async () => { - const postedCase = await createCase(supertest, getPostCaseRequest(), 200, authSpace1); - const body = await deleteCases({ supertest, caseIDs: [postedCase.id], auth: authSpace1 }); + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + authSpace1 + ); + const body = await deleteCases({ + supertest: supertestWithoutAuth, + caseIDs: [postedCase.id], + auth: authSpace1, + }); - await getCase({ supertest, caseId: postedCase.id, expectedHttpCode: 404, auth: authSpace1 }); + await getCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + expectedHttpCode: 404, + auth: authSpace1, + }); expect(body).to.eql({}); }); it('should not delete a case in a different space', async () => { - const postedCase = await createCase(supertest, getPostCaseRequest(), 200, authSpace1); + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + authSpace1 + ); await deleteCases({ - supertest, + supertest: supertestWithoutAuth, caseIDs: [postedCase.id], auth: getAuthWithSuperUser('space2'), expectedHttpCode: 404, }); // the case should still be there - const caseInfo = await getCase({ supertest, caseId: postedCase.id, auth: authSpace1 }); + const caseInfo = await getCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + auth: authSpace1, + }); expect(caseInfo.id).to.eql(postedCase.id); }); }); diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/common/cases/find_cases.ts b/x-pack/test/cases_api_integration/spaces_only/tests/common/cases/find_cases.ts index 6513fe25b28e9f..723e2330bdd93c 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/common/cases/find_cases.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/common/cases/find_cases.ts @@ -18,7 +18,7 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); const authSpace1 = getAuthWithSuperUser(); @@ -28,11 +28,11 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should return 3 cases in space1', async () => { - const a = await createCase(supertest, postCaseReq, 200, authSpace1); - const b = await createCase(supertest, postCaseReq, 200, authSpace1); - const c = await createCase(supertest, postCaseReq, 200, authSpace1); + const a = await createCase(supertestWithoutAuth, postCaseReq, 200, authSpace1); + const b = await createCase(supertestWithoutAuth, postCaseReq, 200, authSpace1); + const c = await createCase(supertestWithoutAuth, postCaseReq, 200, authSpace1); - const cases = await findCases({ supertest, auth: authSpace1 }); + const cases = await findCases({ supertest: supertestWithoutAuth, auth: authSpace1 }); expect(cases).to.eql({ ...findCasesResp, @@ -45,12 +45,12 @@ export default ({ getService }: FtrProviderContext): void => { it('should return 1 case in space2 when 2 cases were created in space1 and 1 in space2', async () => { const authSpace2 = getAuthWithSuperUser('space2'); const [, , space2Case] = await Promise.all([ - createCase(supertest, postCaseReq, 200, authSpace1), - createCase(supertest, postCaseReq, 200, authSpace1), - createCase(supertest, postCaseReq, 200, authSpace2), + createCase(supertestWithoutAuth, postCaseReq, 200, authSpace1), + createCase(supertestWithoutAuth, postCaseReq, 200, authSpace1), + createCase(supertestWithoutAuth, postCaseReq, 200, authSpace2), ]); - const cases = await findCases({ supertest, auth: authSpace2 }); + const cases = await findCases({ supertest: supertestWithoutAuth, auth: authSpace2 }); expect(cases).to.eql({ ...findCasesResp, diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/common/cases/get_case.ts b/x-pack/test/cases_api_integration/spaces_only/tests/common/cases/get_case.ts index 3ea6fac3772edb..0df628f93b28b4 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/common/cases/get_case.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/common/cases/get_case.ts @@ -19,7 +19,7 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); const authSpace1 = getAuthWithSuperUser(); @@ -29,17 +29,31 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should return a case in space1', async () => { - const postedCase = await createCase(supertest, getPostCaseRequest(), 200, authSpace1); - const theCase = await getCase({ supertest, caseId: postedCase.id, auth: authSpace1 }); + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + authSpace1 + ); + const theCase = await getCase({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + auth: authSpace1, + }); const data = removeServerGeneratedPropertiesFromCase(theCase); expect(data).to.eql({ ...postCaseResp(), created_by: nullUser }); }); it('should not return a case in the wrong space', async () => { - const postedCase = await createCase(supertest, getPostCaseRequest(), 200, authSpace1); + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + authSpace1 + ); await getCase({ - supertest, + supertest: supertestWithoutAuth, caseId: postedCase.id, auth: getAuthWithSuperUser('space2'), expectedHttpCode: 404, diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/common/cases/patch_cases.ts b/x-pack/test/cases_api_integration/spaces_only/tests/common/cases/patch_cases.ts index 361358dc40604f..ccf22279ec1979 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/common/cases/patch_cases.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/common/cases/patch_cases.ts @@ -19,7 +19,7 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); const authSpace1 = getAuthWithSuperUser(); @@ -29,9 +29,9 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should patch a case in space1', async () => { - const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); + const postedCase = await createCase(supertestWithoutAuth, postCaseReq, 200, authSpace1); const patchedCases = await updateCase({ - supertest, + supertest: supertestWithoutAuth, params: { cases: [ { @@ -54,9 +54,9 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should not patch a case in a different space', async () => { - const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); + const postedCase = await createCase(supertestWithoutAuth, postCaseReq, 200, authSpace1); await updateCase({ - supertest, + supertest: supertestWithoutAuth, params: { cases: [ { diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/common/cases/post_case.ts b/x-pack/test/cases_api_integration/spaces_only/tests/common/cases/post_case.ts index 4ac0573428445b..f78816dd7ba1d9 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/common/cases/post_case.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/common/cases/post_case.ts @@ -20,7 +20,7 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); const authSpace1 = getAuthWithSuperUser(); @@ -31,7 +31,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should post a case in space1', async () => { const postedCase = await createCase( - supertest, + supertestWithoutAuth, getPostCaseRequest({ connector: { id: '123', diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/common/cases/reporters/get_reporters.ts b/x-pack/test/cases_api_integration/spaces_only/tests/common/cases/reporters/get_reporters.ts index d3c3176f4649fa..983983f5d3b3e1 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/common/cases/reporters/get_reporters.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/common/cases/reporters/get_reporters.ts @@ -18,7 +18,7 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); const authSpace1 = getAuthWithSuperUser(); const authSpace2 = getAuthWithSuperUser('space2'); @@ -30,13 +30,16 @@ export default ({ getService }: FtrProviderContext): void => { it('should not return reporters when security is disabled', async () => { await Promise.all([ - createCase(supertest, getPostCaseRequest(), 200, authSpace2), - createCase(supertest, getPostCaseRequest(), 200, authSpace1), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, authSpace2), + createCase(supertestWithoutAuth, getPostCaseRequest(), 200, authSpace1), ]); - const reportersSpace1 = await getReporters({ supertest, auth: authSpace1 }); + const reportersSpace1 = await getReporters({ + supertest: supertestWithoutAuth, + auth: authSpace1, + }); const reportersSpace2 = await getReporters({ - supertest, + supertest: supertestWithoutAuth, auth: authSpace2, }); diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/common/cases/status/get_status.ts b/x-pack/test/cases_api_integration/spaces_only/tests/common/cases/status/get_status.ts index c4e46675e75490..b48543d3699f6f 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/common/cases/status/get_status.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/common/cases/status/get_status.ts @@ -20,7 +20,7 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); const authSpace1 = getAuthWithSuperUser(); const authSpace2 = getAuthWithSuperUser('space2'); @@ -40,13 +40,13 @@ export default ({ getService }: FtrProviderContext): void => { * closed: 1 */ const [, inProgressCase, postedCase] = await Promise.all([ - createCase(supertest, postCaseReq, 200, authSpace1), - createCase(supertest, postCaseReq, 200, authSpace1), - createCase(supertest, postCaseReq, 200, authSpace2), + createCase(supertestWithoutAuth, postCaseReq, 200, authSpace1), + createCase(supertestWithoutAuth, postCaseReq, 200, authSpace1), + createCase(supertestWithoutAuth, postCaseReq, 200, authSpace2), ]); await updateCase({ - supertest, + supertest: supertestWithoutAuth, params: { cases: [ { @@ -60,7 +60,7 @@ export default ({ getService }: FtrProviderContext): void => { }); await updateCase({ - supertest, + supertest: supertestWithoutAuth, params: { cases: [ { @@ -73,8 +73,14 @@ export default ({ getService }: FtrProviderContext): void => { auth: authSpace2, }); - const statusesSpace1 = await getAllCasesStatuses({ supertest, auth: authSpace1 }); - const statusesSpace2 = await getAllCasesStatuses({ supertest, auth: authSpace2 }); + const statusesSpace1 = await getAllCasesStatuses({ + supertest: supertestWithoutAuth, + auth: authSpace1, + }); + const statusesSpace2 = await getAllCasesStatuses({ + supertest: supertestWithoutAuth, + auth: authSpace2, + }); expect(statusesSpace1).to.eql({ count_open_cases: 1, diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/common/cases/tags/get_tags.ts b/x-pack/test/cases_api_integration/spaces_only/tests/common/cases/tags/get_tags.ts index 630628a13b6b9b..41074cd252735d 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/common/cases/tags/get_tags.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/common/cases/tags/get_tags.ts @@ -18,7 +18,7 @@ import { getPostCaseRequest } from '../../../../../common/lib/mock'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); const authSpace1 = getAuthWithSuperUser(); const authSpace2 = getAuthWithSuperUser('space2'); @@ -29,11 +29,16 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should return case tags in space1', async () => { - await createCase(supertest, getPostCaseRequest(), 200, authSpace1); - await createCase(supertest, getPostCaseRequest({ tags: ['unique'] }), 200, authSpace2); + await createCase(supertestWithoutAuth, getPostCaseRequest(), 200, authSpace1); + await createCase( + supertestWithoutAuth, + getPostCaseRequest({ tags: ['unique'] }), + 200, + authSpace2 + ); - const tagsSpace1 = await getTags({ supertest, auth: authSpace1 }); - const tagsSpace2 = await getTags({ supertest, auth: authSpace2 }); + const tagsSpace1 = await getTags({ supertest: supertestWithoutAuth, auth: authSpace1 }); + const tagsSpace2 = await getTags({ supertest: supertestWithoutAuth, auth: authSpace2 }); expect(tagsSpace1).to.eql(['defacement']); expect(tagsSpace2).to.eql(['unique']); diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/common/comments/delete_comment.ts b/x-pack/test/cases_api_integration/spaces_only/tests/common/comments/delete_comment.ts index 7e5abeb7edc2f2..e43c41809eb3a4 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/common/comments/delete_comment.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/common/comments/delete_comment.ts @@ -21,7 +21,7 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); const authSpace1 = getAuthWithSuperUser(); @@ -33,16 +33,16 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should delete a comment from space1', async () => { - const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); + const postedCase = await createCase(supertestWithoutAuth, postCaseReq, 200, authSpace1); const patchedCase = await createComment({ - supertest, + supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, auth: authSpace1, }); const comment = await deleteComment({ - supertest, + supertest: supertestWithoutAuth, caseId: postedCase.id, commentId: patchedCase.comments![0].id, auth: authSpace1, @@ -52,16 +52,16 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should not delete a comment from a different space', async () => { - const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); + const postedCase = await createCase(supertestWithoutAuth, postCaseReq, 200, authSpace1); const patchedCase = await createComment({ - supertest, + supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, auth: authSpace1, }); await deleteComment({ - supertest, + supertest: supertestWithoutAuth, caseId: postedCase.id, commentId: patchedCase.comments![0].id, expectedHttpCode: 404, diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/common/comments/find_comments.ts b/x-pack/test/cases_api_integration/spaces_only/tests/common/comments/find_comments.ts index 2569d035fabb5c..5bc9fd400de775 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/common/comments/find_comments.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/common/comments/find_comments.ts @@ -22,7 +22,7 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); const authSpace1 = getAuthWithSuperUser(); @@ -34,22 +34,27 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should find all case comments in space1', async () => { - const caseInfo = await createCase(supertest, getPostCaseRequest(), 200, authSpace1); + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + authSpace1 + ); await createComment({ - supertest, + supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentUserReq, auth: authSpace1, }); const patchedCase = await createComment({ - supertest, + supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentUserReq, auth: authSpace1, }); - const { body: caseComments } = await supertest + const { body: caseComments } = await supertestWithoutAuth .get(`${getSpaceUrlPrefix(authSpace1.space)}${CASES_URL}/${caseInfo.id}/comments/_find`) .expect(200); @@ -57,22 +62,27 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should not find any case comments in space2', async () => { - const caseInfo = await createCase(supertest, getPostCaseRequest(), 200, authSpace1); + const caseInfo = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + authSpace1 + ); await createComment({ - supertest, + supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentUserReq, auth: authSpace1, }); await createComment({ - supertest, + supertest: supertestWithoutAuth, caseId: caseInfo.id, params: postCommentUserReq, auth: authSpace1, }); - const { body: caseComments } = await supertest + const { body: caseComments } = await supertestWithoutAuth .get(`${getSpaceUrlPrefix('space2')}${CASES_URL}/${caseInfo.id}/comments/_find`) .expect(200); diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/common/comments/get_all_comments.ts b/x-pack/test/cases_api_integration/spaces_only/tests/common/comments/get_all_comments.ts index ea3766b733cdcf..9eba39d0a8545a 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/common/comments/get_all_comments.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/common/comments/get_all_comments.ts @@ -19,7 +19,7 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); const authSpace1 = getAuthWithSuperUser(); @@ -29,40 +29,44 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should get multiple comments for a single case in space1', async () => { - const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); + const postedCase = await createCase(supertestWithoutAuth, postCaseReq, 200, authSpace1); await createComment({ - supertest, + supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, auth: authSpace1, }); await createComment({ - supertest, + supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, auth: authSpace1, }); - const comments = await getAllComments({ supertest, caseId: postedCase.id, auth: authSpace1 }); + const comments = await getAllComments({ + supertest: supertestWithoutAuth, + caseId: postedCase.id, + auth: authSpace1, + }); expect(comments.length).to.eql(2); }); it('should not find any comments in space2', async () => { - const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); + const postedCase = await createCase(supertestWithoutAuth, postCaseReq, 200, authSpace1); await createComment({ - supertest, + supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, auth: authSpace1, }); await createComment({ - supertest, + supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, auth: authSpace1, }); const comments = await getAllComments({ - supertest, + supertest: supertestWithoutAuth, caseId: postedCase.id, auth: getAuthWithSuperUser('space2'), }); diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/common/comments/get_comment.ts b/x-pack/test/cases_api_integration/spaces_only/tests/common/comments/get_comment.ts index 048700993087db..ebffac50e131a0 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/common/comments/get_comment.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/common/comments/get_comment.ts @@ -19,7 +19,7 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); const authSpace1 = getAuthWithSuperUser(); @@ -29,15 +29,15 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should get a comment in space1', async () => { - const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); + const postedCase = await createCase(supertestWithoutAuth, postCaseReq, 200, authSpace1); const patchedCase = await createComment({ - supertest, + supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, auth: authSpace1, }); const comment = await getComment({ - supertest, + supertest: supertestWithoutAuth, caseId: postedCase.id, commentId: patchedCase.comments![0].id, auth: authSpace1, @@ -47,15 +47,15 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should not get a comment in space2 when it was created in space1', async () => { - const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); + const postedCase = await createCase(supertestWithoutAuth, postCaseReq, 200, authSpace1); const patchedCase = await createComment({ - supertest, + supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, auth: authSpace1, }); await getComment({ - supertest, + supertest: supertestWithoutAuth, caseId: postedCase.id, commentId: patchedCase.comments![0].id, auth: getAuthWithSuperUser('space2'), diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/common/comments/patch_comment.ts b/x-pack/test/cases_api_integration/spaces_only/tests/common/comments/patch_comment.ts index 286c0d7b6925df..395fa81dcb9686 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/common/comments/patch_comment.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/common/comments/patch_comment.ts @@ -22,7 +22,7 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); const authSpace1 = getAuthWithSuperUser(); @@ -34,9 +34,9 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should patch a comment in space1', async () => { - const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); + const postedCase = await createCase(supertestWithoutAuth, postCaseReq, 200, authSpace1); const patchedCase = await createComment({ - supertest, + supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, auth: authSpace1, @@ -44,7 +44,7 @@ export default ({ getService }: FtrProviderContext): void => { const newComment = 'Well I decided to update my comment. So what? Deal with it.'; const updatedCase = await updateComment({ - supertest, + supertest: supertestWithoutAuth, caseId: postedCase.id, req: { id: patchedCase.comments![0].id, @@ -63,9 +63,9 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should not patch a comment in a different space', async () => { - const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); + const postedCase = await createCase(supertestWithoutAuth, postCaseReq, 200, authSpace1); const patchedCase = await createComment({ - supertest, + supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, auth: authSpace1, @@ -73,7 +73,7 @@ export default ({ getService }: FtrProviderContext): void => { const newComment = 'Well I decided to update my comment. So what? Deal with it.'; await updateComment({ - supertest, + supertest: supertestWithoutAuth, caseId: postedCase.id, req: { id: patchedCase.comments![0].id, diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/common/comments/post_comment.ts b/x-pack/test/cases_api_integration/spaces_only/tests/common/comments/post_comment.ts index 0f268e3288c82b..1eccb0dba87594 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/common/comments/post_comment.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/common/comments/post_comment.ts @@ -20,7 +20,7 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); const authSpace1 = getAuthWithSuperUser(); @@ -30,9 +30,9 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should post a comment in space1', async () => { - const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); + const postedCase = await createCase(supertestWithoutAuth, postCaseReq, 200, authSpace1); const patchedCase = await createComment({ - supertest, + supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, auth: authSpace1, @@ -57,9 +57,9 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should not post a comment on a case in a different space', async () => { - const postedCase = await createCase(supertest, postCaseReq, 200, authSpace1); + const postedCase = await createCase(supertestWithoutAuth, postCaseReq, 200, authSpace1); await createComment({ - supertest, + supertest: supertestWithoutAuth, caseId: postedCase.id, params: postCommentUserReq, auth: getAuthWithSuperUser('space2'), diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/common/configure/get_configure.ts b/x-pack/test/cases_api_integration/spaces_only/tests/common/configure/get_configure.ts index 573b96d71af4ad..b1873d912890d0 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/common/configure/get_configure.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/common/configure/get_configure.ts @@ -21,7 +21,7 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); const authSpace1 = getAuthWithSuperUser(); @@ -31,17 +31,20 @@ export default ({ getService }: FtrProviderContext): void => { }); it('should return a configuration in space1', async () => { - await createConfiguration(supertest, getConfigurationRequest(), 200, authSpace1); - const configuration = await getConfiguration({ supertest, auth: authSpace1 }); + await createConfiguration(supertestWithoutAuth, getConfigurationRequest(), 200, authSpace1); + const configuration = await getConfiguration({ + supertest: supertestWithoutAuth, + auth: authSpace1, + }); const data = removeServerGeneratedPropertiesFromSavedObject(configuration[0]); expect(data).to.eql(getConfigurationOutput(false, { created_by: nullUser })); }); it('should not find a configuration when looking in a different space', async () => { - await createConfiguration(supertest, getConfigurationRequest(), 200, authSpace1); + await createConfiguration(supertestWithoutAuth, getConfigurationRequest(), 200, authSpace1); const configuration = await getConfiguration({ - supertest, + supertest: supertestWithoutAuth, auth: getAuthWithSuperUser('space2'), }); diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/common/configure/patch_configure.ts b/x-pack/test/cases_api_integration/spaces_only/tests/common/configure/patch_configure.ts index f61e8698c11915..c681718d50cb57 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/common/configure/patch_configure.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/common/configure/patch_configure.ts @@ -21,7 +21,7 @@ import { nullUser } from '../../../../common/lib/mock'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); const authSpace1 = getAuthWithSuperUser(); @@ -32,13 +32,13 @@ export default ({ getService }: FtrProviderContext): void => { it('should patch a configuration in space1', async () => { const configuration = await createConfiguration( - supertest, + supertestWithoutAuth, getConfigurationRequest(), 200, authSpace1 ); const newConfiguration = await updateConfiguration( - supertest, + supertestWithoutAuth, configuration.id, { closure_type: 'close-by-pushing', @@ -57,13 +57,13 @@ export default ({ getService }: FtrProviderContext): void => { it('should not patch a configuration in a different space', async () => { const configuration = await createConfiguration( - supertest, + supertestWithoutAuth, getConfigurationRequest(), 200, authSpace1 ); await updateConfiguration( - supertest, + supertestWithoutAuth, configuration.id, { closure_type: 'close-by-pushing', diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/common/configure/post_configure.ts b/x-pack/test/cases_api_integration/spaces_only/tests/common/configure/post_configure.ts index 161075616925cb..a3e7a8f58e24aa 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/common/configure/post_configure.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/common/configure/post_configure.ts @@ -20,7 +20,7 @@ import { nullUser } from '../../../../common/lib/mock'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); const authSpace1 = getAuthWithSuperUser(); @@ -31,7 +31,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should create a configuration in space1', async () => { const configuration = await createConfiguration( - supertest, + supertestWithoutAuth, getConfigurationRequest(), 200, authSpace1 diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/common/metrics/get_cases_metrics.ts b/x-pack/test/cases_api_integration/spaces_only/tests/common/metrics/get_cases_metrics.ts index 66fb3f4343e585..9ce6119060fe12 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/common/metrics/get_cases_metrics.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/common/metrics/get_cases_metrics.ts @@ -17,7 +17,7 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const es = getService('es'); - const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const kibanaServer = getService('kibanaServer'); const authSpace1 = getAuthWithSuperUser(); @@ -50,7 +50,7 @@ export default ({ getService }: FtrProviderContext): void => { describe('MTTR', () => { it('should calculate the mttr correctly on space 1', async () => { const metrics = await getCasesMetrics({ - supertest, + supertest: supertestWithoutAuth, features: ['mttr'], auth: authSpace1, }); @@ -61,7 +61,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should calculate the mttr correctly on space 2', async () => { const authSpace2 = getAuthWithSuperUser('space2'); const metrics = await getCasesMetrics({ - supertest, + supertest: supertestWithoutAuth, features: ['mttr'], auth: authSpace2, }); diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/common/user_actions/get_all_user_actions.ts b/x-pack/test/cases_api_integration/spaces_only/tests/common/user_actions/get_all_user_actions.ts index 199e53ebd1bb55..4b97d30ecf1f54 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/common/user_actions/get_all_user_actions.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/common/user_actions/get_all_user_actions.ts @@ -18,7 +18,7 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); const authSpace1 = getAuthWithSuperUser(); @@ -28,16 +28,30 @@ export default ({ getService }: FtrProviderContext): void => { }); it(`should get user actions in space1`, async () => { - const postedCase = await createCase(supertest, getPostCaseRequest(), 200, authSpace1); - const body = await getCaseUserActions({ supertest, caseID: postedCase.id, auth: authSpace1 }); + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + authSpace1 + ); + const body = await getCaseUserActions({ + supertest: supertestWithoutAuth, + caseID: postedCase.id, + auth: authSpace1, + }); expect(body.length).to.eql(1); }); it(`should not get user actions in the wrong space`, async () => { - const postedCase = await createCase(supertest, getPostCaseRequest(), 200, authSpace1); + const postedCase = await createCase( + supertestWithoutAuth, + getPostCaseRequest(), + 200, + authSpace1 + ); const body = await getCaseUserActions({ - supertest, + supertest: supertestWithoutAuth, caseID: postedCase.id, auth: getAuthWithSuperUser('space2'), }); diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/trial/cases/push_case.ts b/x-pack/test/cases_api_integration/spaces_only/tests/trial/cases/push_case.ts index bfb266e6f6c3a0..09cbc2b9ad18b2 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/trial/cases/push_case.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/trial/cases/push_case.ts @@ -23,6 +23,7 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); const authSpace1 = getAuthWithSuperUser(); @@ -48,13 +49,13 @@ export default ({ getService }: FtrProviderContext): void => { it('should push a case in space1', async () => { const { postedCase, connector } = await createCaseWithConnector({ - supertest, + supertest: supertestWithoutAuth, serviceNowSimulatorURL, actionsRemover, auth: authSpace1, }); const theCase = await pushCase({ - supertest, + supertest: supertestWithoutAuth, caseId: postedCase.id, connectorId: connector.id, auth: authSpace1, @@ -75,13 +76,13 @@ export default ({ getService }: FtrProviderContext): void => { it('should not push a case in a different space', async () => { const { postedCase, connector } = await createCaseWithConnector({ - supertest, + supertest: supertestWithoutAuth, serviceNowSimulatorURL, actionsRemover, auth: authSpace1, }); await pushCase({ - supertest, + supertest: supertestWithoutAuth, caseId: postedCase.id, connectorId: connector.id, auth: getAuthWithSuperUser('space2'), diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/get_configure.ts b/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/get_configure.ts index 405323005e64e9..c68437968e0da9 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/get_configure.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/get_configure.ts @@ -8,7 +8,7 @@ import http from 'http'; import expect from '@kbn/expect'; import { ConnectorTypes } from '@kbn/cases-plugin/common/api'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; import { @@ -27,6 +27,7 @@ import { nullUser } from '../../../../common/lib/mock'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const actionsRemover = new ActionsRemover(supertest); const authSpace1 = getAuthWithSuperUser(); @@ -50,7 +51,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should return a configuration with a mapping from space1', async () => { const connector = await createConnector({ - supertest, + supertest: supertestWithoutAuth, req: { ...getServiceNowConnector(), config: { apiUrl: serviceNowSimulatorURL }, @@ -60,7 +61,7 @@ export default ({ getService }: FtrProviderContext): void => { actionsRemover.add('space1', connector.id, 'action', 'actions'); await createConfiguration( - supertest, + supertestWithoutAuth, getConfigurationRequest({ id: connector.id, name: connector.name, @@ -105,7 +106,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should not return a configuration with a mapping from a different space', async () => { const connector = await createConnector({ - supertest, + supertest: supertestWithoutAuth, req: { ...getServiceNowConnector(), config: { apiUrl: serviceNowSimulatorURL }, diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/get_connectors.ts b/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/get_connectors.ts index 0ca47597e7b6bd..b08b6f21390d41 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/get_connectors.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/get_connectors.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; import { @@ -25,6 +25,7 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const actionsRemover = new ActionsRemover(supertest); const authSpace1 = getAuthWithSuperUser(); const space = getActionsSpace(authSpace1.space); @@ -36,35 +37,35 @@ export default ({ getService }: FtrProviderContext): void => { it('should return the correct connectors in space1', async () => { const snConnector = await createConnector({ - supertest, + supertest: supertestWithoutAuth, req: getServiceNowConnector(), auth: authSpace1, }); const snOAuthConnector = await createConnector({ - supertest, + supertest: supertestWithoutAuth, req: getServiceNowOAuthConnector(), auth: authSpace1, }); const emailConnector = await createConnector({ - supertest, + supertest: supertestWithoutAuth, req: getEmailConnector(), auth: authSpace1, }); const jiraConnector = await createConnector({ - supertest, + supertest: supertestWithoutAuth, req: getJiraConnector(), auth: authSpace1, }); const resilientConnector = await createConnector({ - supertest, + supertest: supertestWithoutAuth, req: getResilientConnector(), auth: authSpace1, }); const sir = await createConnector({ - supertest, + supertest: supertestWithoutAuth, req: getServiceNowSIRConnector(), auth: authSpace1, }); @@ -76,7 +77,10 @@ export default ({ getService }: FtrProviderContext): void => { actionsRemover.add(space, jiraConnector.id, 'action', 'actions'); actionsRemover.add(space, resilientConnector.id, 'action', 'actions'); - const connectors = await getCaseConnectors({ supertest, auth: authSpace1 }); + const connectors = await getCaseConnectors({ + supertest: supertestWithoutAuth, + auth: authSpace1, + }); const sortedConnectors = connectors.sort((a, b) => a.name.localeCompare(b.name)); expect(sortedConnectors).to.eql([ @@ -174,37 +178,37 @@ export default ({ getService }: FtrProviderContext): void => { it('should not return any connectors when looking in the wrong space', async () => { const snConnector = await createConnector({ - supertest, + supertest: supertestWithoutAuth, req: getServiceNowConnector(), auth: authSpace1, }); const snOAuthConnector = await createConnector({ - supertest, + supertest: supertestWithoutAuth, req: getServiceNowOAuthConnector(), auth: authSpace1, }); const emailConnector = await createConnector({ - supertest, + supertest: supertestWithoutAuth, req: getEmailConnector(), auth: authSpace1, }); const jiraConnector = await createConnector({ - supertest, + supertest: supertestWithoutAuth, req: getJiraConnector(), auth: authSpace1, }); const resilientConnector = await createConnector({ - supertest, + supertest: supertestWithoutAuth, req: getResilientConnector(), auth: authSpace1, }); const sir = await createConnector({ - supertest, + supertest: supertestWithoutAuth, req: getServiceNowSIRConnector(), auth: authSpace1, }); @@ -217,7 +221,7 @@ export default ({ getService }: FtrProviderContext): void => { actionsRemover.add(space, resilientConnector.id, 'action', 'actions'); const connectors = await getCaseConnectors({ - supertest, + supertest: supertestWithoutAuth, auth: getAuthWithSuperUser('space2'), }); diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/patch_configure.ts b/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/patch_configure.ts index 084e05921fb47f..e984cab68f9ddd 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/patch_configure.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/patch_configure.ts @@ -29,6 +29,7 @@ import { nullUser } from '../../../../common/lib/mock'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); const authSpace1 = getAuthWithSuperUser(); const space = getActionsSpace(authSpace1.space); @@ -55,7 +56,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should patch a configuration connector and create mappings in space1', async () => { const connector = await createConnector({ - supertest, + supertest: supertestWithoutAuth, req: { ...getServiceNowConnector(), config: { apiUrl: serviceNowSimulatorURL }, @@ -67,7 +68,7 @@ export default ({ getService }: FtrProviderContext): void => { // Configuration is created with no connector so the mappings are empty const configuration = await createConfiguration( - supertest, + supertestWithoutAuth, getConfigurationRequest(), 200, authSpace1 @@ -82,7 +83,7 @@ export default ({ getService }: FtrProviderContext): void => { }); const newConfiguration = await updateConfiguration( - supertest, + supertestWithoutAuth, configuration.id, { ...reqWithoutOwner, @@ -125,7 +126,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should not patch a configuration connector when it is in a different space', async () => { const connector = await createConnector({ - supertest, + supertest: supertestWithoutAuth, req: { ...getServiceNowConnector(), config: { apiUrl: serviceNowSimulatorURL }, @@ -137,7 +138,7 @@ export default ({ getService }: FtrProviderContext): void => { // Configuration is created with no connector so the mappings are empty const configuration = await createConfiguration( - supertest, + supertestWithoutAuth, getConfigurationRequest(), 200, authSpace1 @@ -152,7 +153,7 @@ export default ({ getService }: FtrProviderContext): void => { }); await updateConfiguration( - supertest, + supertestWithoutAuth, configuration.id, { ...reqWithoutOwner, diff --git a/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/post_configure.ts b/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/post_configure.ts index 13d2cc60707278..4a8ffe56d35eb8 100644 --- a/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/post_configure.ts +++ b/x-pack/test/cases_api_integration/spaces_only/tests/trial/configure/post_configure.ts @@ -28,6 +28,7 @@ import { nullUser } from '../../../../common/lib/mock'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const es = getService('es'); const authSpace1 = getAuthWithSuperUser(); const space = getActionsSpace(authSpace1.space); @@ -54,7 +55,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should create a configuration with a mapping in space1', async () => { const connector = await createConnector({ - supertest, + supertest: supertestWithoutAuth, req: { ...getServiceNowConnector(), config: { apiUrl: serviceNowSimulatorURL }, @@ -65,7 +66,7 @@ export default ({ getService }: FtrProviderContext): void => { actionsRemover.add(space, connector.id, 'action', 'actions'); const postRes = await createConfiguration( - supertest, + supertestWithoutAuth, getConfigurationRequest({ id: connector.id, name: connector.name, diff --git a/x-pack/test/fleet_api_integration/apis/epm/setup.ts b/x-pack/test/fleet_api_integration/apis/epm/setup.ts index 6d79a1c0a85c4c..00d964cb64e7ce 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/setup.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/setup.ts @@ -74,7 +74,7 @@ export default function (providerContext: FtrProviderContext) { }); it('should upgrade package policy on setup if keep policies up to date set to true', async () => { - const oldVersion = '1.9.0'; + const oldVersion = '1.11.0'; await supertest .post(`/api/fleet/epm/packages/system/${oldVersion}`) .set('kbn-xsrf', 'xxxx') diff --git a/x-pack/test/fleet_api_integration/config.ts b/x-pack/test/fleet_api_integration/config.ts index d5284c23f7fb73..d73904d792955a 100644 --- a/x-pack/test/fleet_api_integration/config.ts +++ b/x-pack/test/fleet_api_integration/config.ts @@ -15,7 +15,7 @@ import { defineDockerServersConfig } from '@kbn/test'; // example: https://beats-ci.elastic.co/blue/organizations/jenkins/Ingest-manager%2Fpackage-storage/detail/snapshot/74/pipeline/257#step-302-log-1. // It should be updated any time there is a new Docker image published for the Snapshot Distribution of the Package Registry. export const dockerImage = - 'docker.elastic.co/package-registry/distribution:e1a3906e0c9944ecade05308022ba35eb0ebd00a'; + 'docker.elastic.co/package-registry/distribution:93ffe45d8c4ae11365bc70b1038643121049b9fe'; export const BUNDLED_PACKAGE_DIR = '/tmp/fleet_bundled_packages'; diff --git a/x-pack/test/functional/apps/dashboard/group1/index.ts b/x-pack/test/functional/apps/dashboard/group1/index.ts index f829002448f334..5e44cae7529051 100644 --- a/x-pack/test/functional/apps/dashboard/group1/index.ts +++ b/x-pack/test/functional/apps/dashboard/group1/index.ts @@ -11,7 +11,5 @@ export default function ({ loadTestFile }: FtrProviderContext) { describe('dashboard', function () { loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./preserve_url')); - loadTestFile(require.resolve('./reporting')); - loadTestFile(require.resolve('./drilldowns')); }); } diff --git a/x-pack/test/functional/apps/dashboard/group3/config.ts b/x-pack/test/functional/apps/dashboard/group3/config.ts new file mode 100644 index 00000000000000..d927f93adeffd0 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/group3/config.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const functionalConfig = await readConfigFile(require.resolve('../../../config.base.js')); + + return { + ...functionalConfig.getAll(), + testFiles: [require.resolve('.')], + }; +} diff --git a/x-pack/test/functional/apps/dashboard/group1/drilldowns/dashboard_to_dashboard_drilldown.ts b/x-pack/test/functional/apps/dashboard/group3/drilldowns/dashboard_to_dashboard_drilldown.ts similarity index 100% rename from x-pack/test/functional/apps/dashboard/group1/drilldowns/dashboard_to_dashboard_drilldown.ts rename to x-pack/test/functional/apps/dashboard/group3/drilldowns/dashboard_to_dashboard_drilldown.ts diff --git a/x-pack/test/functional/apps/dashboard/group1/drilldowns/dashboard_to_url_drilldown.ts b/x-pack/test/functional/apps/dashboard/group3/drilldowns/dashboard_to_url_drilldown.ts similarity index 100% rename from x-pack/test/functional/apps/dashboard/group1/drilldowns/dashboard_to_url_drilldown.ts rename to x-pack/test/functional/apps/dashboard/group3/drilldowns/dashboard_to_url_drilldown.ts diff --git a/x-pack/test/functional/apps/dashboard/group1/drilldowns/explore_data_chart_action.ts b/x-pack/test/functional/apps/dashboard/group3/drilldowns/explore_data_chart_action.ts similarity index 100% rename from x-pack/test/functional/apps/dashboard/group1/drilldowns/explore_data_chart_action.ts rename to x-pack/test/functional/apps/dashboard/group3/drilldowns/explore_data_chart_action.ts diff --git a/x-pack/test/functional/apps/dashboard/group1/drilldowns/explore_data_panel_action.ts b/x-pack/test/functional/apps/dashboard/group3/drilldowns/explore_data_panel_action.ts similarity index 100% rename from x-pack/test/functional/apps/dashboard/group1/drilldowns/explore_data_panel_action.ts rename to x-pack/test/functional/apps/dashboard/group3/drilldowns/explore_data_panel_action.ts diff --git a/x-pack/test/functional/apps/dashboard/group1/drilldowns/index.ts b/x-pack/test/functional/apps/dashboard/group3/drilldowns/index.ts similarity index 100% rename from x-pack/test/functional/apps/dashboard/group1/drilldowns/index.ts rename to x-pack/test/functional/apps/dashboard/group3/drilldowns/index.ts diff --git a/x-pack/test/functional/apps/dashboard/group3/index.ts b/x-pack/test/functional/apps/dashboard/group3/index.ts new file mode 100644 index 00000000000000..98ccd85b7c15dd --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/group3/index.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('dashboard', function () { + loadTestFile(require.resolve('./reporting')); + loadTestFile(require.resolve('./drilldowns')); + }); +} diff --git a/x-pack/test/functional/apps/dashboard/group1/reporting/README.md b/x-pack/test/functional/apps/dashboard/group3/reporting/README.md similarity index 100% rename from x-pack/test/functional/apps/dashboard/group1/reporting/README.md rename to x-pack/test/functional/apps/dashboard/group3/reporting/README.md diff --git a/x-pack/test/functional/apps/dashboard/group1/reporting/__snapshots__/download_csv.snap b/x-pack/test/functional/apps/dashboard/group3/reporting/__snapshots__/download_csv.snap similarity index 100% rename from x-pack/test/functional/apps/dashboard/group1/reporting/__snapshots__/download_csv.snap rename to x-pack/test/functional/apps/dashboard/group3/reporting/__snapshots__/download_csv.snap diff --git a/x-pack/test/functional/apps/dashboard/group1/reporting/download_csv.ts b/x-pack/test/functional/apps/dashboard/group3/reporting/download_csv.ts similarity index 100% rename from x-pack/test/functional/apps/dashboard/group1/reporting/download_csv.ts rename to x-pack/test/functional/apps/dashboard/group3/reporting/download_csv.ts diff --git a/x-pack/test/functional/apps/dashboard/group1/reporting/index.ts b/x-pack/test/functional/apps/dashboard/group3/reporting/index.ts similarity index 100% rename from x-pack/test/functional/apps/dashboard/group1/reporting/index.ts rename to x-pack/test/functional/apps/dashboard/group3/reporting/index.ts diff --git a/x-pack/test/functional/apps/dashboard/group1/reporting/reports/baseline/large_dashboard_preserve_layout.png b/x-pack/test/functional/apps/dashboard/group3/reporting/reports/baseline/large_dashboard_preserve_layout.png similarity index 100% rename from x-pack/test/functional/apps/dashboard/group1/reporting/reports/baseline/large_dashboard_preserve_layout.png rename to x-pack/test/functional/apps/dashboard/group3/reporting/reports/baseline/large_dashboard_preserve_layout.png diff --git a/x-pack/test/functional/apps/dashboard/group1/reporting/reports/baseline/sample_data_ecommerce_76.png b/x-pack/test/functional/apps/dashboard/group3/reporting/reports/baseline/sample_data_ecommerce_76.png similarity index 100% rename from x-pack/test/functional/apps/dashboard/group1/reporting/reports/baseline/sample_data_ecommerce_76.png rename to x-pack/test/functional/apps/dashboard/group3/reporting/reports/baseline/sample_data_ecommerce_76.png diff --git a/x-pack/test/functional/apps/dashboard/group1/reporting/reports/baseline/small_dashboard_preserve_layout.png b/x-pack/test/functional/apps/dashboard/group3/reporting/reports/baseline/small_dashboard_preserve_layout.png similarity index 100% rename from x-pack/test/functional/apps/dashboard/group1/reporting/reports/baseline/small_dashboard_preserve_layout.png rename to x-pack/test/functional/apps/dashboard/group3/reporting/reports/baseline/small_dashboard_preserve_layout.png diff --git a/x-pack/test/functional/apps/dashboard/group1/reporting/screenshots.ts b/x-pack/test/functional/apps/dashboard/group3/reporting/screenshots.ts similarity index 100% rename from x-pack/test/functional/apps/dashboard/group1/reporting/screenshots.ts rename to x-pack/test/functional/apps/dashboard/group3/reporting/screenshots.ts diff --git a/x-pack/test/functional/config.base.js b/x-pack/test/functional/config.base.js index fcb7a22e81d4e2..b7e58ba13b151c 100644 --- a/x-pack/test/functional/config.base.js +++ b/x-pack/test/functional/config.base.js @@ -15,7 +15,7 @@ import { pageObjects } from './page_objects'; // example: https://beats-ci.elastic.co/blue/organizations/jenkins/Ingest-manager%2Fpackage-storage/detail/snapshot/74/pipeline/257#step-302-log-1. // It should be updated any time there is a new Docker image published for the Snapshot Distribution of the Package Registry. export const dockerImage = - 'docker.elastic.co/package-registry/distribution:e1a3906e0c9944ecade05308022ba35eb0ebd00a'; + 'docker.elastic.co/package-registry/distribution:93ffe45d8c4ae11365bc70b1038643121049b9fe'; // the default export of config files must be a config provider // that returns an object with the projects config values diff --git a/x-pack/test/functional/services/observability/alerts/common.ts b/x-pack/test/functional/services/observability/alerts/common.ts index 8b7d15e96cb262..54ce60ddec8480 100644 --- a/x-pack/test/functional/services/observability/alerts/common.ts +++ b/x-pack/test/functional/services/observability/alerts/common.ts @@ -52,6 +52,15 @@ export function ObservabilityAlertsCommonProvider({ return await pageObjects.common.navigateToUrlWithBrowserHistory( 'observability', '/alerts/rules', + '', + { ensureCurrentUrl: false } + ); + }; + + const navigateToRuleDetailsByRuleId = async (ruleId: string) => { + return await pageObjects.common.navigateToUrlWithBrowserHistory( + 'observability', + `/alerts/rules/${ruleId}`, '?', { ensureCurrentUrl: false } ); @@ -336,5 +345,6 @@ export function ObservabilityAlertsCommonProvider({ getAlertsFlyoutViewRuleDetailsLinkOrFail, getRuleStatValue, navigateToRulesPage, + navigateToRuleDetailsByRuleId, }; } diff --git a/x-pack/test/functional_synthetics/config.js b/x-pack/test/functional_synthetics/config.js index cf529226de8958..932d1c47239518 100644 --- a/x-pack/test/functional_synthetics/config.js +++ b/x-pack/test/functional_synthetics/config.js @@ -17,7 +17,7 @@ import { pageObjects } from './page_objects'; // example: https://beats-ci.elastic.co/blue/organizations/jenkins/Ingest-manager%2Fpackage-storage/detail/snapshot/74/pipeline/257#step-302-log-1. // It should be updated any time there is a new Docker image published for the Snapshot Distribution of the Package Registry that updates Synthetics. export const dockerImage = - 'docker.elastic.co/package-registry/distribution:e1a3906e0c9944ecade05308022ba35eb0ebd00a'; + 'docker.elastic.co/package-registry/distribution:93ffe45d8c4ae11365bc70b1038643121049b9fe'; // the default export of config files must be a config provider // that returns an object with the projects config values diff --git a/x-pack/test/observability_functional/apps/observability/index.ts b/x-pack/test/observability_functional/apps/observability/index.ts index ec1f2e089e7326..bd0d822e6234eb 100644 --- a/x-pack/test/observability_functional/apps/observability/index.ts +++ b/x-pack/test/observability_functional/apps/observability/index.ts @@ -9,16 +9,17 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ loadTestFile }: FtrProviderContext) { describe('ObservabilityApp', function () { - loadTestFile(require.resolve('./alerts')); - loadTestFile(require.resolve('./alerts/add_to_case')); - loadTestFile(require.resolve('./alerts/alert_disclaimer')); - loadTestFile(require.resolve('./alerts/alert_status')); - loadTestFile(require.resolve('./alerts/pagination')); - loadTestFile(require.resolve('./alerts/rule_stats')); - loadTestFile(require.resolve('./alerts/state_synchronization')); - loadTestFile(require.resolve('./alerts/table_storage')); + loadTestFile(require.resolve('./pages/alerts')); + loadTestFile(require.resolve('./pages/alerts/add_to_case')); + loadTestFile(require.resolve('./pages/alerts/alert_disclaimer')); + loadTestFile(require.resolve('./pages/alerts/alert_status')); + loadTestFile(require.resolve('./pages/alerts/pagination')); + loadTestFile(require.resolve('./pages/alerts/rule_stats')); + loadTestFile(require.resolve('./pages/alerts/state_synchronization')); + loadTestFile(require.resolve('./pages/alerts/table_storage')); loadTestFile(require.resolve('./exploratory_view')); loadTestFile(require.resolve('./feature_controls')); - loadTestFile(require.resolve('./alerts/rules_page')); + loadTestFile(require.resolve('./pages/rules_page')); + loadTestFile(require.resolve('./pages/rule_details_page')); }); } diff --git a/x-pack/test/observability_functional/apps/observability/alerts/add_to_case.ts b/x-pack/test/observability_functional/apps/observability/pages/alerts/add_to_case.ts similarity index 97% rename from x-pack/test/observability_functional/apps/observability/alerts/add_to_case.ts rename to x-pack/test/observability_functional/apps/observability/pages/alerts/add_to_case.ts index 5e80a5769b44d0..918133ca53dfc8 100644 --- a/x-pack/test/observability_functional/apps/observability/alerts/add_to_case.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/alerts/add_to_case.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default ({ getService, getPageObjects }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/observability_functional/apps/observability/alerts/alert_disclaimer.ts b/x-pack/test/observability_functional/apps/observability/pages/alerts/alert_disclaimer.ts similarity index 95% rename from x-pack/test/observability_functional/apps/observability/alerts/alert_disclaimer.ts rename to x-pack/test/observability_functional/apps/observability/pages/alerts/alert_disclaimer.ts index d63739da47d5b8..b54f36e020183f 100644 --- a/x-pack/test/observability_functional/apps/observability/alerts/alert_disclaimer.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/alerts/alert_disclaimer.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default ({ getService, getPageObject }: FtrProviderContext) => { describe('Observability alert experimental disclaimer', function () { diff --git a/x-pack/test/observability_functional/apps/observability/alerts/alert_status.ts b/x-pack/test/observability_functional/apps/observability/pages/alerts/alert_status.ts similarity index 97% rename from x-pack/test/observability_functional/apps/observability/alerts/alert_status.ts rename to x-pack/test/observability_functional/apps/observability/pages/alerts/alert_status.ts index c7514962c84f73..5e70382418f238 100644 --- a/x-pack/test/observability_functional/apps/observability/alerts/alert_status.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/alerts/alert_status.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { ALERT_STATUS_RECOVERED, ALERT_STATUS_ACTIVE } from '@kbn/rule-data-utils'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; const ALL_ALERTS = 40; const ACTIVE_ALERTS = 10; diff --git a/x-pack/test/observability_functional/apps/observability/alerts/index.ts b/x-pack/test/observability_functional/apps/observability/pages/alerts/index.ts similarity index 98% rename from x-pack/test/observability_functional/apps/observability/alerts/index.ts rename to x-pack/test/observability_functional/apps/observability/pages/alerts/index.ts index 5afdb0b00c774d..8fb90ccc9338c9 100644 --- a/x-pack/test/observability_functional/apps/observability/alerts/index.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/alerts/index.ts @@ -6,8 +6,8 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../ftr_provider_context'; -import { asyncForEach } from '../helpers'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { asyncForEach } from '../../helpers'; const ACTIVE_ALERTS_CELL_COUNT = 78; const RECOVERED_ALERTS_CELL_COUNT = 180; diff --git a/x-pack/test/observability_functional/apps/observability/alerts/pagination.ts b/x-pack/test/observability_functional/apps/observability/pages/alerts/pagination.ts similarity index 98% rename from x-pack/test/observability_functional/apps/observability/alerts/pagination.ts rename to x-pack/test/observability_functional/apps/observability/pages/alerts/pagination.ts index cffbfb6f4227cb..0c1c63ea66acbe 100644 --- a/x-pack/test/observability_functional/apps/observability/alerts/pagination.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/alerts/pagination.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; import { ALERT_STATUS_ACTIVE } from '@kbn/rule-data-utils'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; const ROWS_NEEDED_FOR_PAGINATION = 10; const DEFAULT_ROWS_PER_PAGE = 50; diff --git a/x-pack/test/observability_functional/apps/observability/alerts/rule_stats.ts b/x-pack/test/observability_functional/apps/observability/pages/alerts/rule_stats.ts similarity index 89% rename from x-pack/test/observability_functional/apps/observability/alerts/rule_stats.ts rename to x-pack/test/observability_functional/apps/observability/pages/alerts/rule_stats.ts index 6dabf813f1d56d..443e0616cabe24 100644 --- a/x-pack/test/observability_functional/apps/observability/alerts/rule_stats.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/alerts/rule_stats.ts @@ -6,15 +6,15 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../ftr_provider_context'; -import { ObjectRemover } from '../../../../functional_with_es_ssl/lib/object_remover'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; +import { ObjectRemover } from '../../../../../functional_with_es_ssl/lib/object_remover'; import { createAlert as createRule, disableAlert as disableRule, muteAlert as muteRule, -} from '../../../../functional_with_es_ssl/lib/alert_api_actions'; -import { generateUniqueKey } from '../../../../functional_with_es_ssl/lib/get_test_data'; -import { asyncForEach } from '../helpers'; +} from '../../../../../functional_with_es_ssl/lib/alert_api_actions'; +import { generateUniqueKey } from '../../../../../functional_with_es_ssl/lib/get_test_data'; +import { asyncForEach } from '../../helpers'; export default ({ getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/observability_functional/apps/observability/alerts/state_synchronization.ts b/x-pack/test/observability_functional/apps/observability/pages/alerts/state_synchronization.ts similarity index 98% rename from x-pack/test/observability_functional/apps/observability/alerts/state_synchronization.ts rename to x-pack/test/observability_functional/apps/observability/pages/alerts/state_synchronization.ts index 1860197b43e5b3..fe9751dc9c738f 100644 --- a/x-pack/test/observability_functional/apps/observability/alerts/state_synchronization.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/alerts/state_synchronization.ts @@ -6,7 +6,7 @@ */ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default ({ getPageObjects, getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); diff --git a/x-pack/test/observability_functional/apps/observability/alerts/table_storage.ts b/x-pack/test/observability_functional/apps/observability/pages/alerts/table_storage.ts similarity index 97% rename from x-pack/test/observability_functional/apps/observability/alerts/table_storage.ts rename to x-pack/test/observability_functional/apps/observability/pages/alerts/table_storage.ts index 649465f6a01733..4a8c90abb2ce73 100644 --- a/x-pack/test/observability_functional/apps/observability/alerts/table_storage.ts +++ b/x-pack/test/observability_functional/apps/observability/pages/alerts/table_storage.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../../ftr_provider_context'; +import { FtrProviderContext } from '../../../../ftr_provider_context'; export default ({ getService, getPageObject }: FtrProviderContext) => { describe('Observability alert table state storage', function () { diff --git a/x-pack/test/observability_functional/apps/observability/pages/rule_details_page.ts b/x-pack/test/observability_functional/apps/observability/pages/rule_details_page.ts new file mode 100644 index 00000000000000..7bef4578142e48 --- /dev/null +++ b/x-pack/test/observability_functional/apps/observability/pages/rule_details_page.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default ({ getService }: FtrProviderContext) => { + const testSubjects = getService('testSubjects'); + const observability = getService('observability'); + const supertest = getService('supertest'); + const find = getService('find'); + const retry = getService('retry'); + const RULE_ENDPOINT = '/api/alerting/rule'; + + async function createRule(rule: any): Promise { + const ruleResponse = await supertest.post(RULE_ENDPOINT).set('kbn-xsrf', 'foo').send(rule); + expect(ruleResponse.status).to.eql(200); + return ruleResponse.body.id; + } + async function deleteRuleById(ruleId: string) { + const ruleResponse = await supertest + .delete(`${RULE_ENDPOINT}/${ruleId}`) + .set('kbn-xsrf', 'foo'); + expect(ruleResponse.status).to.eql(204); + return true; + } + + describe('Observability Rule Details page', function () { + this.tags('includeFirefox'); + + let uptimeRuleId: string; + const uptimeRuleName = 'uptime'; + + let logThresholdRuleId: string; + const logThresholdRuleName = 'error-log'; + + before(async () => { + await observability.users.restoreDefaultTestUserRole(); + const uptimeRule = { + params: { + search: '', + numTimes: 5, + timerangeUnit: 'm', + timerangeCount: 15, + shouldCheckStatus: true, + shouldCheckAvailability: true, + availability: { range: 30, rangeUnit: 'd', threshold: '99' }, + }, + consumer: 'alerts', + schedule: { interval: '1m' }, + tags: [], + name: uptimeRuleName, + rule_type_id: 'xpack.uptime.alerts.monitorStatus', + notify_when: 'onActionGroupChange', + actions: [], + }; + const logThresholdRule = { + params: { + timeSize: 5, + timeUnit: 'm', + count: { value: 75, comparator: 'more than' }, + criteria: [{ field: 'log.level', comparator: 'equals', value: 'error' }], + }, + consumer: 'alerts', + schedule: { interval: '1m' }, + tags: [], + name: logThresholdRuleName, + rule_type_id: 'logs.alert.document.count', + notify_when: 'onActionGroupChange', + actions: [], + }; + uptimeRuleId = await createRule(uptimeRule); + logThresholdRuleId = await createRule(logThresholdRule); + }); + after(async () => { + await deleteRuleById(uptimeRuleId); + await deleteRuleById(logThresholdRuleId); + }); + + describe('Navigate to the new Rule Details page', () => { + it('should navigate to the new rule details page by clicking on the rule from the rules table', async () => { + await observability.alerts.common.navigateToRulesPage(); + await retry.waitFor( + 'Rules table to be visible', + async () => await testSubjects.exists('rulesList') + ); + await find.clickByLinkText(logThresholdRuleName); + await retry.waitFor( + 'Rule details to be visible', + async () => await testSubjects.exists('ruleDetails') + ); + }); + + it('should navigate to the new rule details page by URL', async () => { + await observability.alerts.common.navigateToRuleDetailsByRuleId(uptimeRuleId); + await retry.waitFor( + 'Rule details to be visible', + async () => await testSubjects.exists('ruleDetails') + ); + }); + }); + + describe('Page components', () => { + before(async () => { + await observability.alerts.common.navigateToRuleDetailsByRuleId(logThresholdRuleId); + }); + it('show the rule name as the page title', async () => { + await retry.waitFor( + 'Rule name to be visible', + async () => await testSubjects.exists('ruleName') + ); + const ruleName = await testSubjects.getVisibleText('ruleName'); + expect(ruleName).to.be(logThresholdRuleName); + }); + + it('shows the rule status section in the rule summary', async () => { + await testSubjects.existOrFail('ruleSummaryRuleStatus'); + }); + + it('shows the rule definition section in the rule summary', async () => { + await testSubjects.existOrFail('ruleSummaryRuleDefinition'); + }); + + it('maps correctly the rule type with the human readable rule type', async () => { + const ruleType = await testSubjects.getVisibleText('ruleSummaryRuleType'); + expect(ruleType).to.be('Log threshold'); + }); + }); + + describe('User permissions', () => { + before(async () => { + await observability.alerts.common.navigateToRuleDetailsByRuleId(logThresholdRuleId); + }); + it('should show the more (...) button if user has permissions', async () => { + await retry.waitFor( + 'More button to be visible', + async () => await testSubjects.exists('moreButton') + ); + }); + + it('should shows the rule edit and delete button if user has permissions', async () => { + await testSubjects.click('moreButton'); + await testSubjects.existOrFail('editRuleButton'); + await testSubjects.existOrFail('deleteRuleButton'); + }); + + it('should not let user edit/delete the rule if he has no permissions', async () => { + await observability.users.setTestUserRole( + observability.users.defineBasicObservabilityRole({ + logs: ['read'], + }) + ); + await observability.alerts.common.navigateToRuleDetailsByRuleId(logThresholdRuleId); + await testSubjects.missingOrFail('moreButton'); + }); + }); + }); +}; diff --git a/x-pack/test/observability_functional/apps/observability/alerts/rules_page.ts b/x-pack/test/observability_functional/apps/observability/pages/rules_page.ts similarity index 100% rename from x-pack/test/observability_functional/apps/observability/alerts/rules_page.ts rename to x-pack/test/observability_functional/apps/observability/pages/rules_page.ts diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/health_route.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/health_route.ts index 5578ee1ea3f722..1bd158019c6f4f 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/health_route.ts +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/health_route.ts @@ -225,7 +225,7 @@ export default function ({ getService }: FtrProviderContext) { expect(typeof proposed.avg_required_throughput_per_minute_per_kibana).to.eql('number'); }); - it('should return an estimation of task manager capacity', async () => { + it('should return an estimation of task manager capacity as an array', async () => { const { workload: { value: workload }, } = (await getHealth()).stats; @@ -240,15 +240,6 @@ export default function ({ getService }: FtrProviderContext) { expect(typeof workload.capacity_requirements.per_day).to.eql('number'); expect(Array.isArray(workload.estimated_schedule_density)).to.eql(true); - - // test run with the default poll_interval of 3s and a monitored_aggregated_stats_refresh_rate of 5s, - // so we expect the estimated_schedule_density to span a minute (which means 20 buckets, as 60s / 3s = 20) - // Note: Due to an issue in ES, sometimes it returns 21 buckets for the active time span - // which causes miscalculation of the expected result (22) - expect( - workload.estimated_schedule_density.length === 20 || - workload.estimated_schedule_density.length === 22 - ).to.be(true); }); it('should return the task manager runtime stats', async () => { diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts index 840c36a558ba06..ea05bc19b6dd1f 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_list.ts @@ -41,7 +41,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await fleetButton.click(); await testSubjects.existOrFail('createPackagePolicy_pageTitle'); expect(await testSubjects.getVisibleText('createPackagePolicy_pageTitle')).to.equal( - 'Add Endpoint Security integration' + 'Add Endpoint and Cloud Security integration' ); }); it('navigates back to the policy list page', async () => {